带过不少实习生,也面试过不少应届生,Redis 问一圈基本就能摸清一个人的工程底子。问数据类型能背出五种,但一问到序列化乱码、缓存击穿、分布式锁续期,十个人里有八个开始含糊。这篇东西不是把 Redis 官方文档再抄一遍,而是站在 Java 后端工程的角度,把实习生入职后真正会碰到、线上容易踩坑的那些知识点串一遍:数据类型怎么选、序列化为什么乱码、连接超时怎么排查、分布式锁怎么落地、缓存三大问题和一致性怎么处理。全程用项目里真实发生的场景说话,看完直接能用到你的 demo 和第一个线上需求里。
1. 先想明白:Redis 在 Java 工程里到底扮演什么角色
很多实习生上来就敲命令,SET 一个值然后 GET 回来,觉得自己会 Redis 了。但工程里 Redis 从来不孤立存在,它是因为解决某个真实问题才被引入的。先把这个"为什么"想清楚,后面所有知识点才有落点。
1.1 为什么是 Redis 而不是本地 Map
你写单机 demo 的时候,随手用一个ConcurrentHashMap也能做缓存,看起来比 Redis 省事多了。但一旦应用部署成多实例,本地 Map 的问题立刻暴露:每个 JVM 进程各自缓存一份数据,用户请求打到 A 实例拿到旧值,打到 B 实例拿到新值,数据对不上。更别提缓存失效时,每个实例都要各自回源数据库,压力被放大了 N 倍。
Redis 解决的正是这个问题:它是独立部署的进程,所有应用实例共享同一份数据状态。用个生活化的类比——本地 Map 就像每个人手上一张草稿纸,Redis 是团队共用的白板,谁写都能被所有人看见,而且白板自己负责存储、淘汰和持久化。
所以第一节课不是学命令,是建立"Redis 是共享状态层"的心智模型。后面所有数据一致性、分布式锁、缓存治理的问题,本质上都是在围绕这个共享模型做并发控制。
1.2 Redis 作为中间件的常见落地场景
从工程角度看,Redis 在 Java 项目里至少承担以下几种角色,实习生至少要能说出前三类:
| 角色 | 典型场景 | 核心命令 |
|---|---|---|
| 缓存层 | 热点数据、接口结果缓存 | GET/SET/EXPIRE |
| 分布式锁 | 多实例互斥操作,比如库存扣减、定时任务互斥 | SET NX EX |
| 计数器 | 点赞数、访问量、限流窗口计数 | INCR/DECR |
| 排行榜 | 积分榜、热度榜 | ZADD/ZREVRANGE |
| 会话存储 | 集群环境共享 Session | HSET/HGETALL |
| 简单队列 | 异步解耦、延迟任务 | LPUSH/BRPOP |
这里多说一句,类似"用 Redis 做消息队列"这种事情,小流量临时用可以,但 Redis 的 List 做队列不保证消息不丢(消费者挂了消息就丢了),也没有真正的 ACK 机制。生产环境要可靠消息投递,还是老老实实上专业 MQ。实习生如果能主动说出"Redis 做队列只适合低可靠性场景",面试官对这个回答的印象分通常会高不少。
2. 五种基础数据类型:背命令只是入门,用对场景才是进阶
Redis 的五种基础数据类型——String、Hash、List、Set、ZSet——是必须过关的。但工程向的要求不止于"知道有哪些",而是"看到业务需求能立刻反应出该用哪种"。我把每种类型的工程注意点拆开讲。
2.1 String:最常见的类型,坑也最隐蔽
String 能存字符串、数字、二进制数据,底层是动态字符串。日常缓存 JSON 字符串就靠它,配合 EXPIRE 设置过期时间。除了缓存,String 还有三个高频用法:
- 计数器:
INCR是原子操作,点赞数、PV 统计、限流都靠它。你写get再set在并发下必然丢更新,必须用原子命令。 - 分布式 ID 片段:
INCRBY生成自增序列,配合日期前缀生成订单号。 - 位图统计:
SETBIT可以做用户签到、在线状态统计,一个用户只占 1 bit,千万级用户也才 1MB 多。
工程里最容易踩的坑是 value 过大。Redis 是单线程处理命令的,一个几百 KB 甚至几 MB 的大 key 在网络传输和内存分配上都会阻塞其他命令,这就是线上常见的"慢查询"来源。经验值是单个 value 尽量控制在几十 KB 以内,超过 100KB 就要考虑拆分了。
另外注意批量操作:循环 N 次 SET 会产生 N 次网络 RTT,改用MSET/MGET或 Pipeline 能把性能提升一个量级。实习生在代码评审里被提醒"这里要合并请求",说的基本都是这个。
2.2 Hash:对象缓存首选,省内存但又坑
Hash 存的是 field-value 映射,特别适合存一个对象的多个字段。比如用户信息,你可以用HSET user:1001 name "张三" age 25,然后只更新某个字段而不动整个对象。
用 Hash 而不是 String 存 JSON 的核心好处:一是内存效率,当 Hash 字段少的时候,Redis 底层用 ziplist 压缩存储,比 JSON 字符串省不少内存;二是字段级更新,改一个字段不用读改写整个 JSON,避免并发覆盖。
但 Hash 有两个工程注意点:第一,field 数量超过hash-max-ziplist-entries(默认 128)后结构会转为 hashtable,内存优势消失,所以"大 Hash"要谨慎;第二,HGETALL会把所有字段一次性拉出来,一个大 Hash 的 HGETALL 跟 String 大 key 一样是慢查询来源,只取需要的字段用HMGET。
2.3 List:队列和时间线
List 是双向链表,左边进右边出,天然适合做队列。LPUSH+BRPOP的组合就是阻塞队列的经典用法:生产者往左 push,消费者用BRPOP阻塞等待右侧弹出,既省轮询又能实现简单的异步解耦。
另一个场景是时间线/消息流,比如用户的最近动态,用LPUSH把新内容推到队头,LRANGE 0 9取最新 10 条,配合LTRIM只保留最近 N 条——这比查数据库然后排序高效多了。
坑点在上面提过:List 做队列不可靠。消费者处理到一半挂了,已经弹出的消息就彻底丢了;想要至少一次投递,得自己引入"待确认队列",复杂度立刻上来。实习生不要一上来就设计方案用 Redis 做核心队列,先评估可靠性要求。
2.4 Set:去重和集合运算就是它的主场
Set 无序、元素唯一,交集并集差集是它区别于其他类型的核心能力。场景非常直接:
- 点赞/收藏:
SADD article:123:likes userId,SISMEMBER判断是否点过赞; - 标签系统:文章打标签,按标签筛选;
- 共同好友/关注:
SINTER user:1:follow user:2:follow一次算出共同关注; - 在线用户去重:用户上线
SADD,下线SREM,SCARD拿到在线人数。
注意SINTER的时间复杂度是 O(N*M),两个大集合求交会阻塞,线上可以先在业务层做集合大小预估,或者用SINTERSTORE把结果缓存起来。
2.5 ZSet:带权重的 Set,排行榜和延迟队列都靠它
ZSet 每个元素带一个 double 类型的 score,按 score 排序。排行榜是它的代表作:ZADD leaderboard 100 userA,ZREVRANGE leaderboard 0 9 WITHSCORES取前 10,用户分数更新直接ZINCRBY,完全不用你操心排序逻辑。
延迟队列也是 ZSet 的杀手锏场景。score 存任务的执行时间戳,生产者ZADD delay_queue 任务,消费者用一个轮询线程不停ZRANGEBYSCORE delay_queue 0 now LIMIT 0 1,取到就处理,没取到就睡一会。配合 Lua 脚本做原子取和删,就能实现一个轻量延迟队列。很多开源项目里定时任务调度就是这么做的底子。
用 ZSet 需要记一个复杂度:ZADD是 O(logN),千万级元素的高频写入会带来 CPU 压力。排行榜类需求,建议设置合理的冷热分层——只把近一周分数变化的人放进 Redis,历史数据归档到数据库。
2.6 Key 设计与过期策略:新人不重视,线上吃大亏
面试官很喜欢问"一个订单缓存 key 你会怎么设计"。标准的工程答案是"业务:模块:标识",比如order:detail:1001、user:profile:uuid。好处是:可读性强,按前缀能一眼看出业务归属;方便按前缀批量管理(虽然不推荐频繁KEYS,但前缀能帮你扫出所有相关 key);避免不同业务的 key 撞车。
过期策略是另一个重点。缓存类 key 几乎都应该设置 TTL,否则 Redis 内存慢慢涨,最终触发淘汰策略把不该淘汰的数据挤掉。但要注意,同一业务的 key 如果设置相同过期时间,到点集体失效会导致缓存雪崩,所以常见做法是 TTL 加一个随机偏移。实习生如果能主动说出"过期时间加 300~600 秒随机值",就说明真想过这个问题。
3. 工程向必踩的坑:序列化、连接配置与可视化工具
这节的内容是新手最容易懵、老手闭着眼也能答的区域。几乎每个实习生第一次用 Spring Data Redis 都会遇到乱码、超时、连不上这些问题,我把常见坑位挨个排一遍。
3.1 Spring Data Redis 序列化乱码:key 变成 \xAC\xED 之谜
实习生态最经典的"灵异事件":用 RedisTemplate 存了一个 key,打开桌面管理工具一看,key 显示成一串乱码,\xAC\xED\x00\x05t\x00\x03xxx这种,而用命令行直接 SET 的值又读不出来。
原因很明确:Spring Data Redis 默认的序列化器是JdkSerializationRedisSerializer,key 和 value 都经过 Java 对象序列化,这个序列化产物以\xAC\xED开头。你肉眼看到的"字符串 key",在 Redis 里存的是经过 JDK 序列化嵌套的字节数组。
解决思路是覆盖默认配置,让 key 用字符串序列化,value 用 JSON 序列化:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }这里有两个细节要提醒。第一,GenericJackson2JsonRedisSerializer序列化对象时会带上@class类型信息,反序列化才能还原具体类型,但这也意味着如果类的全限定名变了,老数据会反序列化失败。第二,项目里如果同时用了StringRedisTemplate和RedisTemplate,必须保证 key 的序列化方式一致,否则互相读不到数据。StringRedisTemplate默认就是 String 序列化,你只要让 RedisTemplate 的 key 也用 String 序列化,两边就能打通。
3.2 连接超时与 Lettuce 的坑:RedisCommandTimeoutException
Spring Boot 2.x 之后默认用的是 Lettuce 客户端。实习生线上最常见的报错长这样:
io.lettuce.core.RedisCommandTimeoutException: Command timed out after 10 second(s)看到这个不要急着怀疑 Redis 挂了,先按顺序排查:
第一,Redis 服务端打满。Redis 单线程处理命令,如果有大 key 慢查询,后续所有命令排队,客户端等待超时。用redis-cli --latency和SLOWLOG GET确认是否存在慢命令。
第二,Lettuce 的连接池配置不合理。这里是最容易踩的:Lettuce 默认基于 Netty 的共享连接,理论上单连接可以并发,但 Spring Boot 2.x 某些版本里,如果大量线程阻塞在同一个连接上等待响应,仍然会超时。这时候需要显式配置连接池:
spring: data: redis: host: localhost port: 6379 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 3s注意timeout不要设置得太长,否则调用方线程会长时间阻塞。3 秒是比较保险的工程值,配合熔断降级,比无限等下去健康得多。
第三,业务线程池被打满。如果调用 Redis 的线程池(比如 Tomcat 请求线程)全部阻塞在 Redis IO 等待上,新请求进不来,现象看起来也是"连接超时"。用jstack看线程栈,凡是大量WAITING在 Lettuce 的 Future 上的,基本就是慢查询导致的雪崩。
3.3 安装与连接工具:别在环境上浪费一上午
实习生入职第一天经常被"装环境"卡住,我直接给最快路径:
- macOS:
brew install redis,然后brew services start redis,默认端口 6379; - Windows:官方不提供原生版本,用 WSL 装 Linux 版,或者用 Memurai 这类兼容实现。别下那些来路不明的"Windows 版 redis.exe",安全性没保障;
- Docker 部署主从:这是生产最常用的方式,一条命令起一个从节点:
# 主节点 docker run -d --name redis-master -p 6379:6379 redis:7 redis-server --appendonly yes # 从节点,注意在容器内执行主从复制 docker run -d --name redis-slave -p 6380:6379 redis:7 redis-server --slaveof <主节点IP> 6379--slaveof虽然在新版本改叫--replicaof了,但老的参数依然兼容,很多网上教程还在用,直接抄没问题。主从的意义在于读写分离和数据冗余,但注意主从是异步复制,主节点挂了没切换的话数据可能有秒级丢失,想要高可用还得上哨兵或集群。
连接工具方面,Redis Desktop Manager 是老牌 GUI,但新版商用收费,免费用户可以选开源替代 Another Redis Desktop Manager,功能基本够用,连接配置、key 树形浏览、命令行一应俱全。实习生装上之后一定要会做一件事:切换 DB 和搜索 key,排查问题能快很多。
4. 分布式锁:面试必问,工程必用
分布式锁大概是面试里被问得最多的 Redis 场景,同时也是工程上最容易出问题的点。我按"从简单实现到成熟方案"的顺序讲,顺带把每一层为什么升级说清楚。
4.1 为什么需要分布式锁
单机多线程下,你可以用synchronized或ReentrantLock保证互斥。但多实例部署后,两个 JVM 进程里的线程不再共享锁对象,Java 原生的锁管不到"别的机器上的线程"。比如秒杀扣库存,两个实例同时读到库存 10,各自减一,最终库存变成 9,这就是超卖。
分布式锁的本质是:用一个所有实例都能访问到的"标记",谁抢到标记谁执行。Redis 因为高性能和原子命令,成了实现这个标记最常用的载体。
4.2 基于 SET NX EX 的正确姿势
早期网上教程写的是先SETNX key value,再EXPIRE key 30。这个方案有个致命问题:两步不是原子的,如果 SETNX 成功但 EXPIRE 的时候进程崩溃,锁就永远不会释放,所有线程被卡死。
正确做法是单条原子命令:
// 加锁:key 不存在才设置成功,同时设置过期时间 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(30));这里有两个关键点。
第一,value必须放一个全局唯一的标识(比如 UUID 或 requestId)。为什么?因为要防止"误删别人的锁"。线程 A 持有锁超时了,锁自动过期,线程 B 拿到锁开始执行;此时 A 终于执行完,如果它直接DEL lockKey,把 B 的锁给删了,C 又拿到锁,三个线程同时执行临界区。正确的释放逻辑是先比对 value 是自己的才删,而且比对+删除要用 Lua 保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end第二,过期时间设多少?设短了,业务没执行完锁就断了;设长了,没拿到锁的线程等太久。工程上常见的做法是设置一个"预计业务耗时的 3~5 倍"作为兜底,同时配合续期机制,而不是试图给一个完美值。
4.3 锁过期了怎么办:看门狗续期与 Redisson
业务执行时间是不可预测的,锁 30 秒过期,但业务跑了 50 秒,临界区就直接失守了。怎么解决?答案是"自动续期"。
Redisson 的解决方案非常优雅:加锁后,后台有一个 watch dog(看门狗)线程,默认每 10 秒检查一次,如果锁还持有且业务没结束,就把锁的过期时间重置为 30 秒。业务完成释放锁,看门狗线程随之关闭。
实习生面试被问到"分布式锁怎么续期",能说出 Redisson 的看门狗机制,就是加分项。但要注意,Redisson 的看门狗默认只在"持有锁的线程还活着"时续期,线程挂了锁自然过期,不会死锁。千万不要自己写一个 while 循环盲目续期,那就是给自己埋雷。
Redission 的用法很简单:
RLock lock = redissonClient.getLock("order:pay:1001"); boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS); if (locked) { try { // 业务逻辑 } finally { lock.unlock(); } }至于 RedLock(多节点加锁算法),我的建议是新人不碰为妙。它依赖多个独立 Redis 节点同时加锁成功,工程上引入了额外复杂度和时钟依赖,业界对它的争论也一直没有定论。大多数业务场景里,单节点 Redis + Redisson 已经足够,真要那么高的可靠性,不如上 ZooKeeper 或 etcd 的分布式锁。
4.4 锁之外必须补的课:幂等性兜底
分布式锁不是万能的。锁可以保护临界区互斥,但网络抖动、超时重试、消息重复投递这些场景,锁根本管不住。真正可靠的做法是"锁+幂等性"双保险:业务操作设计成天然幂等,或者建一张去重表,用数据库唯一索引兜底。
举一个真实的例子:支付回调里要更新订单状态,用 Redis 锁保证了同一订单的多次回调不会并发处理。但万一锁超时了呢?万一 Redis 出了网络问题呢?此时第二次回调又进来了,更新语句重复执行两次。如果更新逻辑本身是"状态机校验"(只有待支付才允许变成已支付),或者数据库层面有幂等键约束,就不会出大乱子。所以经验丰富的架构师常说的那句话是:"锁是性能方案,幂等才是正确性方案。"
5. 缓存治理:穿透、击穿、雪崩与数据一致性
缓存能扛住大量读请求,但它也把"一致性"这个难题带进来了。工程上最经典的三类问题和一套一致性方案,面试官百问不厌,线上也百踩不厌。
5.1 缓存穿透:查了一个不存在的东西
缓存穿透很简单:请求一个缓存里没有、数据库里也不存在的 key(比如恶意攻击者拿一串不存在的 ID 循环请求)。缓存查不到,只能去查数据库,数据库也没有,于是缓存永远写不进去,每次请求都打穿到 DB。高并发下数据库直接被拖垮。
两个常用解法:
- 空值缓存:查不到也往 Redis 写一个空值或特殊标记,TTL 设置短一点(比如 60 秒)。后续同样的请求直接命中空值缓存,不再穿透。缺点是冷数据会占据一定内存,但通常可控。
- 布隆过滤器:启动时把存在的 ID 全部加载到布隆过滤器里,请求来了先判断"这个 ID 是否存在",不存在直接拒绝。布隆过滤器可能误判(说存在但实际不存在),但绝不会漏判(说不存在就是真不存在),所以它能挡住绝大多数无效请求。注意布隆过滤器本身需要维护,数据频繁增删时要考虑同步成本。
工程上两者经常配合:布隆过滤器做第一层拦截,空值缓存做第二层兜底。
5.2 缓存击穿:热点 key 过期的一瞬间
击穿和穿透的区别很多人搞混。击穿是指:一个超高热度的 key(比如热搜词、爆款商品),突然过期了,同一瞬间有几千个请求涌进来,全部穿透到数据库。数据库被压垮,而 Redis 里根本来不及写入新值。
经典解法是互斥重建:当缓存 miss 时,不是所有线程都去查库,而是先尝试获取一个分布式锁(或 JVM 内锁),只有一个线程去查库并回填缓存,其他线程短暂等待后直接读取缓存。
String value = redis.get(key); if (value == null) { // 加锁,只让一个线程去重建缓存 if (lock.tryLock()) { try { value = db.query(key); redis.set(key, value, ttl); } finally { lock.unlock(); } } else { // 或其他线程 sleep 后重试读缓存 } }逻辑简单,但要说清"为什么是重建而不是永久不过期"——热点 key 设置"逻辑过期"(value 里存业务过期时间)可以避免物理过期瞬间的冲击,但会引入返回旧数据的容忍期。两个方案各有权衡,面试时能说出取舍就行。
5.3 缓存雪崩:一大片 key 同时失效
雪崩是击穿的放大版:大量 key 在同一时间过期,或者 Redis 实例整体宕机,导致所有请求全部落到数据库。这比击穿更难处理,因为它是成片的。
工程手段:
- 过期时间加随机偏移,避免同一批 key 集体到点;
- 做多级缓存:本地 Caffeine 做 L1,Redis 做 L2,L1 兜住短时流量;
- Redis 高可用:持久化 + 主从 + 哨兵,实例挂了尽快恢复;
- 业务降级:数据库扛不住时,直接返回默认值或限流,而不是把 DB 打死。
实习生最容易理解的是加随机偏移这招,实施成本最低:redis.set(key, value, 60 + new Random().nextInt(600))。但面试时如果能补一句"雪崩的防御要分层:过期时间、多级缓存、实例高可用",深度就出来了。
5.4 缓存与数据库一致性:先删缓存还是先更新库
这是老生常谈但依然让新人头大的问题。业界最流行的是 Cache Aside Pattern(旁路缓存),规则就两句话:读的时候先读缓存,miss 再读库并回填;写的时候先更新数据库,再删除缓存。
为什么不先删缓存再更新库?因为删缓存后、更新库前这扇窗口里,如果有读请求进来,拿到数据库旧值写回缓存,就把脏值缓存住了。先更新库再删缓存也一样有窗口,但概率更小——更新库成功后、删缓存失败前,缓存里还是旧值。解决"删缓存失败"的办法是引入重试:把删除失败的消息丢到消息队列,后台慢慢重试。
更严格一点的做法是延迟双删:先更新数据库,删除缓存,然后延迟几百毫秒再删一次。目的是把"读请求回填旧值"这种可能的脏数据窗口内再清理一遍。但双删不是银弹,延迟时间定多少、业务容忍多少不一致,都得具体分析。
我个人在实践中更推荐另一种思路:不追求强一致,而是让数据尽快趋近一致。方式包括:监听数据库 binlog(用 Canal 之类),变更发生后再主动失效缓存。这样即使应用层某次删除失败了,binlog 订阅最终也会把缓存清掉,一致性由"最终"两个字兜住。对于绝大多数业务场景,最终一致性完全够用。
6. 排障速查:实习生常问的问题我都整理成了表
最后这部分是实战记录。很多问题实习生刚入职遇到会慌,我把高频项按"现象、原因、排查手段、解决"整理成速查表,直接抄作业就行。
| 故障现象 | 可能原因 | 排查手段 | 解决方向 |
|---|---|---|---|
缓存 key 是乱码\xAC\xED | 用了 JDK 默认序列化器 | 看 RedisTemplate 配置 | key 改用 StringRedisSerializer |
| RedisCommandTimeoutException | 大 key 慢查询 / 连接池耗尽 / 网络抖动 | SLOWLOG GET、redis-cli --latency、jstack | 拆分大 key、配置连接池、缩短 timeout |
| 内存暴涨 | 未设置 TTL / 大 key / 淘汰策略不当 | INFO memory、redis-cli --bigkeys | 设置过期时间、拆分大 key、配置 maxmemory |
| 删了数据库但缓存还是旧值 | 先删缓存后更新库的窗口期 | 看业务写入顺序 | 改为 Cache Aside 先更新库再删缓存 |
| 多个实例同时执行定时任务 | 缺少分布式互斥 | 日志看执行时间重叠 | 加分布式锁 |
| 缓存命中率低 | 过期时间太短 / 大量 empty key | INFO stats看 keyspace_hits | 拉长 TTL、空值也缓存 |
| 主从切换后数据缺失 | 异步复制有延迟 | INFO replication看 master_repl_offset | 接受秒级丢失或换强一致方案 |
6.1 内存淘汰与持久化:两个必背的配置维度
不设置maxmemory的 Redis 就像一个没有回收机制的垃圾桶,满了之后继续写会直接报 OOM。工程上通常会设置上限,并指定淘汰策略。常见的有allkeys-lru(所有 key 按 LRU 淘汰)、allkeys-lfu(按访问频率淘汰)、volatile-ttl(优先淘汰 TTL 短的)。选型原则是:缓存场景更看重"最近最常访问"就用 LRU 或 LFU;如果你的 Redis 里有不能丢的数据,那就别用 allkeys 系列的淘汰策略,避免把非缓存数据清掉。
持久化上,RDB(快照)恢复快但丢数据窗口大(按快照间隔丢),AOF(追加日志)最多丢一秒(取决于 always/everysec 策略)但文件大、恢复慢。工程实践通常是两者都开:RDB 用于快速重启,AOF 用于兜底数据,同时设置appendfsync everysec做性能和可靠性的折中。这些配好之后,INFO persistence能查到最近持久化状态,线上排查必备。
6.2 SCAN 而非 KEYS:大键空间扫描的正确姿势
实习生排查线上 Redis 时,第一反应往往是KEYS user:*把所有 key 捞出来。KEYS命令会阻塞 Redis 单线程直到返回全部结果,线上几百万 key 一敲,Redis 直接卡住几秒,这是禁止在生产执行的。
替代方案是SCAN cursor:它每次返回一小批 key(默认 10 个左右)和一个新游标,循环迭代直到游标返回 0。它不阻塞 Redis,虽然可能在迭代期间 key 有变化导致结果不精确,但用于日常排查完全够用。同样的思路也适用HSCAN、SSCAN、ZSCAN。
如果有条件,更规范的做法是部署 Redis 的监控采集(比如 Prometheus + redis_exporter),把 key 数量、内存、命中率都做成指标,不想排查的时候才临时 SCAN。
6.3 一条面试加分但工程更值钱的原则:先看慢查询,再谈优化
我想说的最后一件事是:实习生 debug 性能问题,不要上来就猜测。Redis 提供SLOWLOG GET 10,一条命令就能看到最近 10 个慢命令及耗时。凡是超过slowlog-log-slower-than(默认 10000 微秒)的都会被记录下来。实际排障中,大量超时问题根源就是某条大集合操作或大 key 读写。先看慢查询,对症下药;而不是盲目重启、盲目调超时参数。
根据我个人带实习生的经验,Redis 这块的成长路径特别清晰:第一周能背命令和数据类型的场景,第一月能在真实 bug 里认出序列化乱码和超时问题,第一年能独立设计一套缓存治理方案和分布式锁。关键是每次踩坑之后把现象、原因、排查手段记下来,形成自己的排障手册。这篇文章里的速查表,你完全可以直接复制进自己的笔记,持续往里补——等你补满了几十条真实案例,Redis 对你来说就不再是"面试考点",而是真正的工程工具。