Redis Key 命名规范:一台 32G 的服务器是怎么被搞垮的

前几天生产环境出现报警,我一看告警:Redis 内存使用率 98%,OOM killer 已经开始杀进程了。

第一反应是查 bigkeys。上去一跑,好家伙——redis-cli --bigkeys 输出翻了好几屏都没翻完。仔细一看,一堆 key 长得五花八门:

user_1024_token
login:count:189023847
12345:info
order_info_20240321_8899123:status:new
session:abc123xyz
tmp_data_20240321150322

有的 key 末尾带时间戳,有的用下划线分段,有的直接用数字开头。最离谱的是有一个 key 叫 a——就一个字母。我查了下,这个 key 对应的 value 是 2MB 的 JSON 字符串,显然是某位同事写代码的时候随手命名的。

顺便说一下,--bigkeys 是 Redis 自带的一个分析工具,它会扫描整个实例,按数据类型统计最大的那些 key,帮你定位内存大户。

那天的复盘会开得很沉重。问题的根因不是 Redis 内存不够,而是 key 完全没规范:缓存永不过期、key 名字用了时间戳(每天都在涨,永远不会复用)、同一个业务的数据散落在十几个不同的 key 命名风格里。最后加内存只是治标,不改命名规范,下次该炸还是炸。

所以今天想聊聊 Redis key 命名这件事。这玩意看着简单,但踩过坑的人都知道,前期不规划好,后面运维起来是真的要命。

一、为什么说 key 命名不是小事?

很多新手觉得 key 命名就是给数据起个名字,能区分就行。其实不然。

Redis 是个 key-value 数据库,key 就是它唯一的数据索引。你没表结构、没 schema、没外键,全靠 key 来组织和查找数据。key 的设计直接决定了:

  • 排查问题的效率——线上出问题了,你能不能用一条命令就定位到相关 key
  • 批量操作的可行性——清缓存、迁移数据、统计内存,能不能按前缀一把操作
  • 团队协作的顺畅度——新同事接手,看 key 就能猜出是干啥的
  • 避免冲突的概率——两个业务用了同一个 key 名,数据覆盖了找谁去

说白了,key 的命名规范就是你用 Redis 的方式的基础架构。基础打不好,上层全是坑。

二、命名法的核心思想:分层域名式

大家现在普遍认可的最佳实践,是模仿域名系统的分层结构,用冒号(:)做分隔符:

业务域:子域:具体标识

比如:

auth:session:u10086
cache:user:profile:10086
rate:login:18812345678
lock:order:pay:ORD20240321001

为什么选冒号不选下划线或者斜杠?几个原因:

第一,Redis 官方工具支持冒号层级。 redis-cli--bigkeys 输出里,相同前缀的 key 会被聚合统计。你按下划线命名的话,这个功能就废了一半。

第二,可视化工具按前缀分组。 比如 RedisInsight、Another Redis Desktop Manager 这些工具,都能自动识别冒号分隔的层级并折叠展示。下划线命名的 key 在工具里就是一长串扁平的列表,看着就头大。

第三,SCAN 命令支持 MATCH 模式匹配。 SCAN 0 MATCH auth:session:* COUNT 1000 就能扫描所有会话 key。如果你用 auth_session_ 这种下划线前缀,也能扫,但缺少了按中间层级过滤的能力。

SCAN 是生产环境遍历 Redis key 的正确方式。千万别用 KEYS *——KEYS 会阻塞 Redis 单线程,几百万 key 的时候直接卡死好几秒。SCAN 是游标式的,每次返回一批,不阻塞。

第四,层级语义清晰。 auth:session:u10086 从前往后读:这是认证域→会话子域→用户 10086 的会话。就算不了解业务的人,也能猜到个七七八八。

三、域(Namespace)的设计

有了分层结构之后,下一步就是划分顶层域。不同公司的划分各有不同,但大致上都可以归为这几类。我按最常见的情况列一下:

3.1 认证与会话域(auth:

存登录态、Token、会话信息。这类 key 的特点是:生命周期短、访问频繁、安全敏感

auth:session:u10086          → 用户 10086 的会话数据(Hash)
auth:token:refresh:jti_xxx   → Refresh Token 信息
auth:token:blacklist:jti_yyy → 已注销的 Token(提前过期)

这类 key 必须设置 TTL,而且不能太长。比如会话 key 设 30 分钟过期,每次请求续期。万一忘了设过期时间,用户明明已经退出了,会话 key 还在那躺着,就是一个安全隐患。

3.2 缓存域(cache:

业务缓存的大头。缓存 key 有一个特点:删除逻辑跟着业务走

cache:user:profile:10086       → 用户 10086 的资料
cache:product:detail:P10086    → 商品详情
cache:category:tree            → 类目树(全局缓存)

缓存 key 的过期时间设计是个学问:

  • 热点数据(比如商品详情):TTL 设短一点,比如 5-10 分钟。数据更新时主动删除缓存,让下次查询重新加载。
  • 冷数据(比如用户的历史订单列表):TTL 可以设长一些,比如 30 分钟。
  • 全局数据(比如类目树、配置信息):一般不设 TTL,靠主动失效。数据变了发个消息通知所有实例删除缓存。

主动失效和被动过期的区别:主动失效是代码里显式调用 DEL 删除 key,下次查询时缓存没命中,重新加载。被动过期是 Redis 到了 TTL 时间自动删除。主动失效的时效性更好,但需要业务代码配合。

3.3 分布式锁域(lock:

分布式锁相关的 key。这类 key 的特点是:短暂存在、需要自动释放、不能冲突

lock:order:pay:ORD20240321001  → 订单支付锁
lock:account:transfer:10086    → 账户转账锁
lock:task:sync:product         → 同步任务锁

锁 key 的命名很有讲究。因为锁的作用范围要恰到好处:

  • 太宽:lock:order——那所有订单操作都串行化了,性能直接崩
  • 太窄:lock:order:pay:ORD20240321001:user:10086——太长了,而且加上了不相关的维度

合适的粒度应该是:只锁住真正需要互斥的资源。比如支付一个订单,拦的是同一笔订单不要被重复支付,所以 lock:order:pay:{orderId} 就是恰到好处的粒度。

用 Redisson 做分布式锁的话,key 会被自动加上 redisson_lock: 前缀。知道这个细节对排查问题很有帮助——你查 key 列表时看到的 redisson_lock:lock:order:pay:xxx 不是你自己写的 key 名。

3.4 限流域(rate:

限流计数的 key,配合 Lua 脚本或者 Redisson 的 RRateLimiter 使用。

rate:login:18812345678         → 登录限流:每分钟最多 10 次
rate:sms:verify:18812345678    → 短信验证码限流:每分钟最多 3 次
rate:api:user:10086            → API 调用限流:每分钟最多 60 次
rate:global:ip:10.0.0.1        → 全局 IP 限流:每分钟最多 200 次

限流 key 的设计有个核心原则:窗口期必须合理

拿登录限流举例:rate:login:18812345678 的 TTL 是 60 秒。60 秒内超过 10 次就拒绝。但问题是——60 秒后计数器重置,攻击者又可以重新试。所以限流最好是滑动窗口,或者配合黑名单机制,连续失败 5 次直接锁账号 30 分钟。

滑动窗口的 key 设计会用到时间戳:

rate:login:18812345678:2024032115  → 按分钟+小时分段

但这里有个陷阱:如果你把分钟加进 key 名,那每分钟会生成一个新 key。如果限流维度多(IP + 用户 + 接口),key 数量会爆炸。所以一般不用 key 本身做窗口切割,而是利用 Lua 脚本里的数据结构做滑动窗口。

3.5 验证码域(verify:

存短信验证码、邮箱验证码等一次性凭证。

verify:sms:18812345678:LOGIN      → 登录验证码
verify:sms:18812345678:RESET_PWD  → 重置密码验证码
verify:email:test@example.com:BIND → 绑定邮箱验证码

验证码 key 的 TTL 必须准确等于验证码的有效期(通常是 5 分钟)。不能多不能少——设长了有安全风险,设短了用户收不到就用不了了。

而且验证码 key 涉及的并发很特殊:用户可能等不及收短信就点了"重新发送"。这时候你的策略是什么?

  • 方案一:直接删旧验证码,发新的。用户最后收到的验证码就是有效的那个。
  • 方案二:验证码有效期过了才能重新发。这更安全,但用户体验差一些。

具体的策略因业务而异,但 key 设计上一定要能支持:通过 {手机号+场景} 找到用户当前的验证码verify:sms:18812345678:LOGIN 就是这个作用——同一个手机号+场景,永远只保留最新的一条。

3.6 序列号域(seq:

用 Redis 生成自增 ID 或流水号。

seq:order:no                      → 订单号序列(INCR)
seq:claim:no                      → 理赔单号序列
seq:customer:tenant001            → 租户 001 的客户编号序列

序列 key 有个问题:INCR 一直涨,Redis 的 String 类型最大支持 512MB,但一个数字占不了多少空间。真正要注意的是:序列 key 不需要回收,但会一直占用内存。虽然每个序列号 key 占不了几个字节(一个 Long 8 字节 + key 本身的字节),但如果你有几千个序列号维度,加起来也可观了。

另外,INCR 是原子的,不怕并发,但要注意 INCR 一个不存在的 key 时,Redis 会把它初始化为 0 再递增。所以第一个 INCR 返回 1。这符合大多数场景的预期。

INCR 是 Redis 的一个原子操作:如果 key 不存在,先初始化为 0,然后执行加 1,返回结果。整个过程不会被其他命令打断。适合做计数器、序列号生成器。

3.7 频道域(channel:

Pub/Sub 的频道名或者 Stream 的消费者组名。

channel:cache:product:invalidate   → 商品缓存失效频道
channel:data:sync:order            → 订单数据同步频道
channel:system:config:change       → 配置变更通知

Pub/Sub 的 key 是一个频道名,不存数据,但命名同样重要。想象一下:你有个微服务集群,每个实例都订阅了 channel:cache:product:invalidate。某个实例改了商品数据,往这个频道发一条消息,所有实例收到后删除本地缓存。如果频道名起得乱七八糟,订阅关系就理不清了。

Pub/Sub 和 Stream 的区别:Pub/Sub 是 fire-and-forget,发送的消息如果没被订阅者收到就丢了。Stream 是持久化的,消费者可以断线重连后从断点续读。如果你的消息必须被处理(比如订单状态变更通知),用 Stream。如果只是"告诉一声,没收到也没关系"(比如缓存失效),用 Pub/Sub 就够了。

四、集中管理:告别"到处写死字符串"

上面说了那么多规范,但规范不落地就是一纸空文。最落地的方式就是:把所有 Redis key 的定义集中到一个地方管理

看看没有集中管理的代码长什么样:

// ❌ 反面教材:散落在各个 Service 里
@Service
public class UserService {
    
    public UserVO getUser(Long userId) {
        // key 是随手写的,其他人根本不知道这里还有个缓存
        String cacheKey = "user:" + userId;
        String json = redisTemplate.opsForValue().get(cacheKey);
        // ...
    }
    
    public void updateUser(UserUpdateRequest request) {
        // 更新数据后清理缓存
        // 注意:这里清理的 key 跟上面 get 方法里拼的 key 可能不一样!
        String cacheKey = "user_info_" + request.getId();
        redisTemplate.delete(cacheKey);
        // ...
    }
}

两个方法里拼出来的 key 格式不一致,缓存清理永远清不到正确的位置。这种 bug 特别隐晦——不是必现的,只有缓存命中的时候数据才对,缓存过期后重新加载又对了。查 bug 查到想砸键盘。

集中管理之后:

// ✅ RedisKeyConstant.java:所有 key 定义集中在这里
public final class RedisKeyConstant {

    /**
     * 会话数据:auth:session:{userId}
     * 存储用户登录会话信息,TTL = 30 分钟
     */
    public static final String PREFIX_AUTH_SESSION = "auth:session:";

    /**
     * 用户资料缓存:cache:user:profile:{userId}
     * 缓存用户基本信息,变更时主动失效
     */
    public static final String PREFIX_CACHE_USER_PROFILE = "cache:user:profile:";

    /**
     * 商品详情缓存:cache:product:detail:{productNo}
     */
    public static final String PREFIX_CACHE_PRODUCT_DETAIL = "cache:product:detail:";

    /**
     * 登录限流:rate:login:{phoneNumber}
     * TTL = 60 秒,超过 10 次拒绝
     */
    public static final String PREFIX_RATE_LOGIN = "rate:login:";

    /**
     * 分布式锁-订单支付:lock:order:pay:{orderNo}
     * 防止重复支付
     */
    public static final String PREFIX_LOCK_ORDER_PAY = "lock:order:pay:";

    /**
     * 验证码-短信:verify:sms:{phoneNumber}:{scene}
     * scene 为 LOGIN / RESET_PWD / BIND 等
     * TTL = 5 分钟
     */
    public static final String PREFIX_VERIFY_SMS = "verify:sms:";

    /**
     * 订单号序列:seq:order:no
     * 用 INCR 生成,每日重置
     */
    public static final String PREFIX_SEQ_ORDER = "seq:order:no";

    /**
     * Pub/Sub 频道-缓存失效
     */
    public static final String CHANNEL_CACHE_INVALIDATE = "channel:cache:invalidate";
    
    // ========== 组装方法 ==========
    
    public static String buildAuthSessionKey(String userId) {
        return PREFIX_AUTH_SESSION + userId;
    }

    public static String buildCacheUserProfileKey(Long userId) {
        return PREFIX_CACHE_USER_PROFILE + userId;
    }

    public static String buildRateLoginKey(String phone) {
        return PREFIX_RATE_LOGIN + phone;
    }

    public static String buildLockOrderPayKey(String orderNo) {
        return PREFIX_LOCK_ORDER_PAY + orderNo;
    }

    public static String buildVerifySmsKey(String phone, String scene) {
        return PREFIX_VERIFY_SMS + phone + ":" + scene;
    }

    // 私有构造器,防止实例化
    private RedisKeyConstant() {}
}

然后业务代码里这样用:

// ✅ 正确用法:可读性强,风格统一
String cacheKey = RedisKeyConstant.buildCacheUserProfileKey(userId);
UserCacheDTO cached = redisTemplate.opsForValue().get(cacheKey);

// 更新后删除缓存,key 格式保证一致
redisTemplate.delete(RedisKeyConstant.buildCacheUserProfileKey(userId));

集中管理的好处不用我多说:

  1. 一眼看完所有 key——想了解系统用了哪些 Redis key,打开这个文件就全知道了
  2. 命名一致性——不会出现 user:xxxuser_info_xxx 两种风格
  3. 修改方便——哪天要改前缀了,一个文件搞定
  4. 方便代码审查——review 的时候看一眼 key 的格式对不对,不用追到业务代码里找

我见过有些团队更进一步,把 TTL 也定义在一起:

/**
 * 认证会话 key,TTL = 30 分钟(1800 秒)
 * 会话活跃时滑动续期
 */
public static final long TTL_AUTH_SESSION = 1800;

/**
 * 短信验证码 key,TTL = 5 分钟(300 秒)
 */
public static final long TTL_VERIFY_SMS = 300;

/**
 * 登录限流 key,TTL = 1 分钟(60 秒)
 */
public static final long TTL_RATE_LIMIT = 60;

这样运维看代码的时候,一眼就知道每个 key 活多久,心里有数。

五、Key 的过期策略:别让你的 Redis 变成垃圾场

上面提到了很多次 TTL,这里单独展开说一下。

Redis 不像 MySQL 那样有固定的表结构,你写进去的 key 如果不主动删除或者设过期时间,它就永远待在那。这是一个非常容易被忽视的内存泄漏点。

最常见的场景:缓存 key 忘记设 TTL

// ❌ 危险:没有 TTL
redisTemplate.opsForValue().set("cache:product:detail:" + productNo, json);

// ✅ 安全:明确设置过期时间
redisTemplate.opsForValue().set(
    RedisKeyConstant.buildCacheProductDetailKey(productNo), 
    json, 
    10, 
    TimeUnit.MINUTES
);

你可能会想"这个数据量不大,不设 TTL 也没事"。确实,单个 key 不设 TTL 占不了多少内存。但架不住积累——一天 1000 个 key,一年就是 36 万个 key。我见过一个案例,业务代码里一个缓存 key 忘了设 TTL,两年后 Redis 里躺了 2000 多万个 key,占了几十 G 内存。运维查了三天才定位到问题。

而且没有 TTL 的 key 还有个隐藏问题:如果缓存的数据变了,而旧 key 没被清理,用户看到的就是脏数据。

设 TTL 也不是随便设的,有几个原则:

  • 缓存类 key:TTL = 你能接受的最长数据不一致时间。比如商品详情 5-10 分钟是合理的,因为很少频繁修改。但库存就不能设太长了,因为库存变化很频繁。
  • 限流类 key:TTL = 窗口期 + 一点余量。比如 1 分钟窗口就设 70 秒。
  • 锁类 key:TTL = 你能接受的锁最长持有时间。Redisson 默认 30 秒 + 看门狗续期。
  • 验证码类 key:TTL = 验证码有效期,精确到秒,不能多。
  • 序列号类 key:不需要 TTL,因为序列号是持久化的。

关于 TTL 和主动删除的配合:很多时候 TTL 是兜底机制——缓存应该在数据变更时主动删除,这样其他请求能立即加载新数据。但如果主动删除漏掉了(bug、网络问题等),TTL 就是最后的保险,保证数据最终能更新。

六、Key 设计中的那些坑

经验多了你会发现,Redis key 设计里的坑比想象中多得多。我列几个常见的:

坑一:Key 太长

// ❌ 反面
String key = "cache:order:payment:history:user:" + userId 
    + ":page:" + pageNo + ":size:" + pageSize 
    + ":sort:" + sortBy + ":order:" + sortOrder;

假设这个 key 有 80 个字节,你存了 1000 万条记录,光是 key 就占了 800MB 内存(还不算 value 的开销)。RedisObject 本身还有额外开销(至少 16 字节)。

建议:key 的总长度控制在 50 字节以内。如果查询维度太多,可以考虑把一些过滤条件放到 key 之外——比如只存 user:{userId}:orders,过滤和分页用 lua 脚本或者客户端处理。

坑二:Key 包含可变前缀但没有规划

// ❌ 大忌:每天生成新的 key 前缀
String key = "cache:daily:report:" + LocalDate.now();

这条代码运行一年后,你的 Redis 里就有 365 个 key,里面 364 个都是没人访问的冷数据。如果你不主动清理,它们会一直占据内存。

建议:按时间分片的 key 必须配套过期策略,或者用定时任务清理历史数据。

坑三:不同环境用同一套 Key

开发、测试、生产环境的 Redis 要是没分开,那就是灾难。开发同学在 debug 的时候把某个 key 删了,结果生产环境也受影响。

建议:环境隔离有三种方案:

  1. 物理隔离:不同环境用不同的 Redis 实例——最安全
  2. 逻辑隔离:通过前缀区分,比如 dev:cache:xxxprod:cache:xxx
  3. 数据库隔离:如果用的是 Redis Cluster 或者支持多 DB 的版本,用不同的 DB 编号

方案 1 是最推荐的。如果预算有限,至少用方案 2。

坑四:直接存 JSON 作为 key

// ❌ 我在代码审查里见过真实的案例
String key = request.toString();

request.toString() 可能生成一个几百字节的字符串,作为 key 存到 Redis 里。这不仅浪费内存,而且 SCAN 的时候你会看到一堆天书一样的 key。

建议:key 必须是有语义的、可读的字符串。

坑五:不同数据类型混用

同一个 key 今天存 String,明天改成 Hash,这会导致反序列化异常。尤其是用 Redisson 之类的客户端时,序列化器会缓存 key 的类型信息。

坑六:Key 中包含用户输入

// ❌ 用户输入直接拼到 key 里
String key = "cache:search:" + request.getKeyword();

如果用户输入包含特殊字符(比如冒号、换行符),你的 key 格式就乱了。更严重的是,如果用户输入超长字符串(比如几 MB 的文本),key 就被撑爆了。

建议:用户输入要经过处理再拼 key——比如哈希、截断、或者只提取 ID 类字段。

坑七:大小写混用

Redis key 是大小写敏感的。User:10086user:10086 是两个不同的 key。团队的代码规范和沟通不到位,就会出现一半用大驼峰、一半用小写的情况,运维排查的时候眼睛都要看瞎。

七、按场景设计的 Key 值数据结构

命名规范说了很多,但 key 只是半个故事——另一半是 value 的数据结构。这里用几个场景说一下数据类型的选择跟 key 设计也有关系。

场景一:用户会话

应该用 Hash 还是 String 存?

// ❌ String:序列化整个对象
String key = "auth:session:u10086";
String value = JSON.toJSONString(sessionObj);  // 包含 userId, role, loginTime, expireTime...
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);

// 要改其中一个字段?得全量读出来,反序列化,改完再全量写回去
// ✅ Hash:按字段存,修改一个字段不影响其他
String key = "auth:session:u10086";
redisTemplate.opsForHash().put(key, "userId", "10086");
redisTemplate.opsForHash().put(key, "role", "admin");
redisTemplate.opsForHash().put(key, "loginTime", String.valueOf(System.currentTimeMillis()));
redisTemplate.expire(key, 30, TimeUnit.MINUTES);

// 只续期:读一个字段即可
String role = (String) redisTemplate.opsForHash().get(key, "role");

Hash 的优势是字段级别操作,适合频繁修改某个字段的场景。会话的场景里,每次请求都需要续期,用 Hash 可以只读一个字段来判断会话是否存在,比 String 反序列化整个对象轻量多了。

场景二:排行榜

用 ZSet,key 就是排行榜的名称:

// 商品销量排行:ZADD product:sales:ranking score member
redisTemplate.opsForZSet().add("rank:product:sales:monthly:202403", "P10086", 1520);
redisTemplate.opsForZSet().add("rank:product:sales:monthly:202403", "P10087", 980);

// 获取 Top 10
Set<String> top10 = redisTemplate.opsForZSet()
    .reverseRange("rank:product:sales:monthly:202403", 0, 9);

ZSet 的 key 就是排行榜的"表名",member 是排行对象,score 是排行的分数。这种场景下 key 的设计其实是在模拟表名——rank:{domain}:{period}:{periodValue}

场景三:消息队列

// List 做简单队列
String queueKey = "queue:order:pay:notify";
redisTemplate.opsForList().leftPush(queueKey, orderJson);  // 生产者
String msg = redisTemplate.opsForList().rightPop(queueKey);  // 消费者

如果你的队列有优先级要求,用 ZSet:

// 优先级队列:score 越小优先级越高
redisTemplate.opsForZSet().add("queue:order:urgent", orderJson, 1);   // 高优先级
redisTemplate.opsForZSet().add("queue:order:urgent", orderJson, 10);  // 低优先级

八、运维视角:好 Key 设计怎么救命

上面说了很多代码层面的东西。现在从运维的角度看看,一个好的 key 设计到底能帮你省多少事。

8.1 快速定位问题

线上 Redis 告警了——内存飙高、QPS 突增。规范命名的 key 可以让你快速缩小范围:

# 按前缀分析内存占用
redis-cli --bigkeys | grep "auth:session"

# 按前缀统计 key 数量
redis-cli SCAN 0 MATCH "lock:*" COUNT 10000

# 批量查询 TTL
redis-cli --scan --pattern "cache:user:profile:*" | head -20 | xargs redis-cli ttl

不规范的话,你遇到的情况是:一堆看不懂的 key,不知道哪个业务在用,不敢删、不敢动,运维和开发互相甩锅。

8.2 监控和告警

监控系统可以按 key 前缀配置告警阈值:

# 告警:auth:session:* key 数量超过 1 万
# 告警:lock:* key 数量超过 1000(怀疑有死锁)
# 告警:cache:* 内存占用超过 10GB

这个实现不复杂——用 INFO keyspace 拿不到 key 前缀维度的统计,但可以通过定期执行 SCAN + 按前缀分组来采集指标,或者用 Redis 的 --stat 模式配合脚本做聚合。

没有规范前缀的话,监控告警就无从谈起。所有 key 都混在一起,你根本不知道哪里异常。

8.3 数据迁移和清理

哪天要迁移 Redis 集群了,或者给某个业务的数据做冷热分离:

# 批量导出某个前缀的所有 key
redis-cli --scan --pattern "cache:report:*" > report_keys.txt

# 批量设置 TTL(给那些没设过期时间的 key 打补丁)
redis-cli --scan --pattern "cache:temp:*" | while read key; do
    redis-cli expire "$key" 86400
done

8.4 内存分析

可以用 redis-rdb-tools 把 RDB 文件解析成 CSV,然后用 Excel 或者 SQL 做分析:

# 解析 RDB 到 CSV
rdb -c memory /var/lib/redis/dump.rdb > memory_report.csv

# 按 key 前缀分组统计
cat memory_report.csv | awk -F: '{print $1}' | sort | uniq -c | sort -rn

没有规范前缀的话,这种分析基本不可行——你没法按业务维度聚合数据,只能看到一个个孤立的 key。

RDB 是 Redis 的持久化快照文件,包含了某个时间点上所有 key-value 数据。rdb 工具是 Python 写的一个开源工具,可以把 RDB 文件解析成可读的格式,用来做内存分析特别方便。

九、一套可以直接用的 Key 规范

说一千道一万,不如直接给出一套可以照抄的规范。以下是我个人总结的 Redis key 设计清单:

格式规范

项目 规范
格式 {domain}:{subdomain}:{identifier}
分隔符 冒号 :
字符集 小写字母 + 数字 + 冒号
编码 纯 ASCII,不要有中文
最大长度 建议不超过 50 字节
命名风格 全小写,单词间用冒号分隔

域(一级 Namespace)

域名 用途 示例
auth 认证与会话 auth:session:{userId}
cache 业务缓存 cache:user:profile:{userId}
lock 分布式锁 lock:order:pay:{orderNo}
rate 限流计数 rate:login:{phone}
verify 验证码 verify:sms:{phone}:{scene}
seq 序列号 seq:order:no
channel Pub/Sub 频道 channel:cache:invalidate
queue 队列 queue:order:notify
rank 排行榜 rank:product:sales:daily
config 配置 config:system:payment

必做事项

  • 所有 key 定义集中在一个常量类中
  • 每个 key 都有明确的 TTL(除了序列号等需要持久化的)
  • 每个 key 都有 Javadoc/注释说明用途和过期时间
  • 缓存 key 在数据变更时有主动失效逻辑
  • 不同环境(dev/test/prod)用不同的 Redis 实例或前缀
  • key 中不要包含用户输入的原始内容
  • 避免生成长度超过 50 字节的 key
  • 一个 key 只使用一种数据类型
  • 集成的缓存框架(如 Spring Cache)自定义了 key 前缀

反例速查

// ❌ 不要这么做
"user_" + userId                        // 用下划线,无法利用冒号层级
"User:" + userId                        // 大写开头,大小写混用
"cache/user/" + userId                  // 用斜杠,非主流
userId + ":info"                        // ID 开头,不利于前缀查找
"cache:user:" + request.getKeyword()    // 用户输入直接拼 key
"cache:temp:" + System.currentTimeMillis()  // 时间戳做 key,无限增长

十、稍微提一嘴:Redis 7.0 之后的 Key 管理

写这篇文章的时候,Redis 7.x 已经比较成熟了。Redis 7.0 引入了几个跟 key 管理相关的新特性,简单提一下:

Function 代替 Lua 脚本:Redis Function 允许你把脚本管理在 Redis 侧,不需要在客户端传递脚本内容。如果你的业务大量使用 Lua,可以看看这个特性。

Key 级别的通知:Redis 7.0 对 key 空间通知做了改进,可以更方便地监听 key 的过期、删除等事件。不过这个功能默认是关闭的,需要 CONFIG SET notify-keyspace-events 开启。

温馨提示:notify-keyspace-events 开启后会消耗额外的 CPU,因为每次 key 变更都要发布事件。只在确实需要的时候才开启,不要为了"万一以后要用"而提前打开。

总结

写到最后总结一下。Redis key 命名这事,说小很小——不就起个名字吗?说大很大——你的 Redis 稳定不稳定,排查问题快不快,团队协作顺不顺,跟 key 命名都有关系。

我见过太多项目,前期图省事,key 随意命名。等到业务量上来、问题频发的时候,想改命名已经来不及了——改了旧 key 数据就丢了,不改又乱成一锅粥。最后只能加机器、加内存,用钱买运维的债。

所以我的建议很简单:项目第一天就定好规范,写到团队的编码规范文档里,代码审查的时候把 key 命名作为一个检查项。 这花不了多少时间,但省下的却是后期无尽的运维头疼。

核心四句话:

  1. 分层命名,冒号分隔——auth:session:u10086 而不是 user_session_10086
  2. 集中管理,一个类管所有 key——所有人写代码都引用常量类
  3. 每个 key 都要有 TTL——没有 TTL 的 key 要问清楚是不是真的不需要
  4. 团队约定好规范,写进代码审查——规范不改,到最后 Redis 就是垃圾场

希望这篇文章能帮你少踩一些坑。毕竟被半夜的告警电话叫醒的感觉,有过一次就不想有第二次了。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐