如何在Redis缓存中实现类文件夹层级结构?(C# WebAPI场景)
嘿,我太懂你这种需求了——Redis里能有类似文件夹的层级缓存结构,不管是查询、清理还是维护都会顺手太多,总比那种东拼西凑的树形键好打理。结合C# WebAPI的场景,我给你几个实用的实现方案,都是我实际项目里用过的,靠谱得很:
方案1:键命名规范 + SCAN命令(最轻量化)
这是最简单的实现方式,不用额外的数据结构,就是给Redis键加上层级前缀,用冒号:分隔,比如你示例里的结构可以写成:
a:key1b:c:key2b:c:key3d:...
这种方式的核心是用Redis的SCAN命令匹配前缀,来模拟“打开文件夹”获取所有子键的操作。在C#里用StackExchange.Redis库的话,代码大概是这样:
代码示例
using StackExchange.Redis; public class RedisHierarchyCache { private readonly IDatabase _db; public RedisHierarchyCache(IConnectionMultiplexer redis) { _db = redis.GetDatabase(); } // 存储键值对,自动带上层级前缀 public void Set(string hierarchyPath, string key, string value, TimeSpan? expiry = null) { var fullKey = $"{hierarchyPath}:{key}"; _db.StringSet(fullKey, value, expiry); } // 获取某个层级下的所有子键 public IEnumerable<string> GetAllKeysInHierarchy(string hierarchyPath) { var prefix = string.IsNullOrEmpty(hierarchyPath) ? "" : $"{hierarchyPath}:"; var server = _db.Multiplexer.GetServer(_db.Multiplexer.GetEndPoints().First()); // SCAN命令遍历匹配前缀的键,避免KEYS命令的性能问题 foreach (var key in server.Keys(pattern: $"{prefix}*")) { yield return key.ToString(); } } // 删除整个层级下的所有键 public void DeleteHierarchy(string hierarchyPath) { var keys = GetAllKeysInHierarchy(hierarchyPath).ToList(); if (keys.Any()) { _db.KeyDelete(keys.Select(k => (RedisKey)k).ToArray()); } } }
注意点
- 用
SCAN而不是KEYS,因为KEYS会阻塞Redis,生产环境绝对不能用 - 如果层级很深,前缀匹配的效率依然不错,适合中小规模的缓存场景
方案2:哈希表(Hash)存储层级节点(更结构化)
如果想要更“正经”的层级结构,可以把每个“文件夹”当成一个Hash表,Hash的键就是文件夹路径,Hash的字段就是这个文件夹下的子键(或者子文件夹),值可以是缓存内容,或者子文件夹的标识。
比如对于b:c这个文件夹,我们创建一个Hash键b:c,字段是key2、key3,对应的值就是缓存内容。这样要获取整个文件夹的内容,直接HGETALL就能拿到,不用遍历所有键。
代码示例
public void SetInHashHierarchy(string folderPath, string key, string value, TimeSpan? expiry = null) { var hashKey = folderPath; _db.HashSet(hashKey, key, value); // 给整个文件夹设置过期时间(可选) if (expiry.HasValue) { _db.KeyExpire(hashKey, expiry.Value); } } // 获取整个文件夹的所有缓存内容 public Dictionary<string, string> GetFolderContent(string folderPath) { var hashEntries = _db.HashGetAll(folderPath); return hashEntries.ToDictionary(entry => entry.Name.ToString(), entry => entry.Value.ToString()); } // 删除某个文件夹下的单个键 public void DeleteKeyInFolder(string folderPath, string key) { _db.HashDelete(folderPath, key); }
注意点
- Hash表的单个键最多能存2^32-1个字段,足够大部分场景用
- 适合缓存同层级的结构化数据,比如某个用户下的所有订单缓存
方案3:有序集合(Sorted Set)维护层级路径(支持复杂查询)
如果需要更复杂的层级操作,比如获取某个节点的所有父节点、子节点,或者递归遍历整个树,可以用Sorted Set来存储所有层级路径,分数可以设为路径的深度或者排序值。
比如把所有路径存进去:a、b、b:c、b:c:key2、b:c:key3、d,然后用ZRANGEBYLEX命令来匹配前缀,获取所有子节点。
代码示例
public void AddHierarchyPath(string path) { // 用路径作为成员,分数设为路径的深度(用冒号分割的数量) var depth = path.Split(':').Length; _db.SortedSetAdd("hierarchy_paths", path, depth); } // 获取某个节点的所有子节点(递归) public IEnumerable<string> GetAllChildren(string parentPath) { var prefix = string.IsNullOrEmpty(parentPath) ? "" : $"{parentPath}:"; // 用字典序匹配前缀的所有路径 var range = _db.SortedSetRangeByLex("hierarchy_paths", $"[{prefix}", $"[{prefix}\xff"); return range.Select(r => r.ToString()); }
注意点
- 这种方式适合需要频繁做层级遍历、路径查询的场景
- 需要额外维护这个Sorted Set,每次添加/删除缓存键的时候都要同步更新路径
最后,我个人最推荐方案1,因为它最简单,不需要额外维护数据结构,而且完全满足你要的文件夹层级感,在大部分WebAPI场景下足够用了。如果你的缓存结构特别复杂,再考虑方案2或3。
内容的提问来源于stack exchange,提问作者Ali_Nass

