Redis 算是后端最常用的中间件,大部分业务缓存、限流、分布式锁都依赖它。但很多人只停留在会用 set/get,对底层序列化、过期剔除、命令特性细节不了解。
本地测试数据量小、并发低,完全看不出问题,一旦上生产、跑大批量数据和高并发,各种诡异问题接踵而至。整理几个近期线上真实踩过的隐性问题,全部可复现、可根治。
- StringRedisTemplate 存数字,自动类型转换导致判空失效
日常开发中,统计次数、计数缓存,很多人会直接用 String 模板存数字。
// 写入缓存
stringRedisTemplate.opsForValue().set("user:count:1001", "0");
// 业务自增
Long count = stringRedisTemplate.opsForValue().increment("user:count:1001", 1);
// 取值判断
String value = stringRedisTemplate.opsForValue().get("user:count:1001");
if ("0".equals(value)) {
// 重置逻辑
}
线上隐性问题:
increment 命令底层要求 value 是纯数字字符串,执行自增后,Redis 内的数据依旧是字符串类型,看似没问题。
但如果手动后台改库、或者代码初始化赋值为 00、空格数字,后续自增依旧正常,但是代码硬判 "0" 会直接失效,导致业务逻辑不走。
更坑的场景:初始化不赋值,直接 increment。
Redis 对不存在的 key,默认视为 0 自增,完全不会报错,新手根本察觉不到初始化缺失。
规范写法:
所有数字缓存取值后,统一转 Long/Integer 再判断,杜绝字符串硬匹配。
- 缓存过期时间不固定,导致批量 key 同时失效雪崩
绝大多数项目缓存写法:固定过期时间,比如统一 30 分钟、1 小时过期。
// 错误写法:统一过期时间
stringRedisTemplate.opsForValue().set("goods:info:" + id, jsonStr, 1, TimeUnit.HOURS);
线上问题:
项目启动、批量预热缓存、夜间数据刷新,会导致大量 key 同一时间过期。过期瞬间,大量请求直接穿透到数据库,瞬间打满 DB 连接,引发缓存雪崩。
本地测试单条数据完全感知不到风险。
解决方案:固定时间 + 随机抖动
// 基础1小时,随机0-10分钟浮动,打散过期时间
long expireTime = 3600 + new Random().nextInt(600);
stringRedisTemplate.opsForValue().set("goods:info:" + id, jsonStr, expireTime, TimeUnit.SECONDS);
- RedisTemplate 默认序列化导致乱码、key 前缀异常
很多新手直接使用原生 RedisTemplate 不配置序列化器,线上隐患极大。
// 默认配置,不推荐
@Autowired
private RedisTemplate redisTemplate;
问题根因:
RedisTemplate 默认使用 JdkSerializationRedisSerializer,会对 key、value 进行 JDK 序列化。
最终 Redis 中存储的 key 会带一串序列化前缀 \xAC\xED\x00\x05sr...,肉眼乱码。
直接后果:
- Redis 客户端无法直观查看数据
- 无法通过模糊匹配、批量删除 key
- 和 StringRedisTemplate 混用,导致同一个 key 两边读写不互通
标准配置方案:全局统一 Jackson2Json 序列化,key 统一 String 序列化
- 无视 Redis 大小写,线上批量查不到数据
很多开发者默认 Redis 的 key 不区分大小写,这是典型误区。
核心知识点:Redis 的 key、field 是严格区分大小写的。
user:info:1001 和 USER:INFO:1001 是两个完全不同的 key。
踩坑场景:
本地开发统一小写,测试环境参数偶尔大写,线上出现部分缓存命中、部分失效,数据问题极其诡异,排查耗时极长。
强制规范:
所有 Redis key 代码层统一转小写,封装工具类统一处理,彻底杜绝大小写问题。
- 使用 keys 批量查询,线上直接阻塞 Redis
本地清理测试数据,习惯用 keys 、keys user: 模糊匹配。
很多人会把这段逻辑写到代码里做批量删除、批量查询。
致命问题:
keys 命令是单线程全量遍历,无索引、无分页。线上数据量几十万上百万时,执行一次 keys 会阻塞主线程数秒,期间所有读写 Redis 命令全部卡死,直接导致服务雪崩。
生产替代方案:必须使用 scan 迭代遍历
scan 基于游标迭代,非阻塞、分片查询,不会影响 Redis 主线程,是生产唯一合法的批量查询方式。
- 分布式锁只加不释放,导致死锁
初级写法:手动 set 锁,业务执行完手动 delete。
// 加锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock:order:1001", "1");
if (lock) {
// 执行业务
doBusiness();
// 释放锁
stringRedisTemplate.delete("lock:order:1001");
}
线上大坑:
业务代码抛出异常、服务宕机,直接跳过 delete 逻辑,锁永久存在,没人能解锁,形成永久死锁。
正确写法:加锁必须自带过期时间,保证死锁自动释放
// 原子加锁 + 过期时间,杜绝死锁
Boolean lock = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:order:1001", "1", 30, TimeUnit.SECONDS);
进阶场景必须使用 Redisson 可重入锁,兼顾续约、原子性、容错。
总结
Redis 大部分线上故障,都不是中间件本身问题,而是开发者不熟悉底层执行机制,用本地测试逻辑硬套生产环境。
序列化规范、过期打散、禁用危险命令、锁超时兜底,这几个细节搞定,能规避 90% 的 Redis 线上诡异问题。