去年线上出过一次事故,缓存服务一报警,订单服务跟着超时,整个链路像多米诺骨牌一样往下塌。复盘的时候我把自己关在小黑屋里,对着Redis一连问了十二个问题,从基础原理问到线上排障。后来发现,这十二个问题不光保住了那次复盘的脸面,也成了我之后面试候选人和排查故障的固定清单。今天原封不动整理出来,每一个问题都附上我自己的分析思路和踩坑教训,希望能帮你把Redis从“会用”推向“用明白”。
1. 第1问:为什么Redis这么快?——“内存快”不是完整答案
1.1 “内存快”为什么不是标准答案
很多人张口就来“因为Redis是内存数据库”,这个回答不能说错,但只能得一半分。内存确实快,DDR4内存的随机访问延迟大约在几十到一百纳秒级别,而普通SSD的随机读延迟是几十微秒,机械硬盘得几毫秒。但问题是,同样部署在内存里的其他系统,不见得能跑出Redis这种单实例十万级QPS的吞吐量。所以面试官真正想听的,是Redis在“软件层面”做了什么。
1.2 单线程、IO多路复用和高效数据结构,三件套缺一不可
先纠正一个误区:Redis并不是完全单线程。从6.0开始,网络IO读写引入了多线程,但命令执行仍然是单线程的,你可以把整个模型理解成“前台接待可以有多名,真正拍板办事的只有一个人”。
单线程执行命令意味着没有锁竞争、没有上下文切换、没有并发修改同一个数据结构的问题,这是它能稳的关键。但单线程也怕阻塞,一旦某个命令执行时间太长,后面所有命令都要排队。这时候“IO多路复用”就上场了,Redis基于epoll机制在一个线程里同时盯着成千上万个连接,只要连接上有数据可读可写就立刻处理,避免了“每个连接一个线程”那种资源爆炸的模型。
再加上SDS、跳表、压缩列表这些针对不同场景设计的数据结构,内存访问加上极低的时间复杂度,才组合出最终的高性能。我习惯用一句话概括:快是结果,是“内存存储 + 单线程模型 + 多路复用IO + 高效数据结构”四者的合力,单独归功于内存是不公平的。
1.3 快的前提是“别拖后腿”,这几个命令我劝你少用
单线程模型下,O(N)级别的命令就是灾难。KEYS *这种扫描全库的命令,我在线上一次都不敢用,真要遍历就用SCAN游标分批处理。还有针对大Key的DEL、SUNION、SORT这种复杂度高的命令,执行期间会把整个实例拖垮。
提示:如果你要删除一个大Hash或者大List,别直接
DEL。先用HLEN或LLEN评估规模,再用HSCAN分批取出来删。Redis 4.0以后提供了UNLINK命令,它是异步删除,主线程不会阻塞,这个才是处理大Key的正确姿势。
2. 第2问:五大基础数据类型,你真的选对了吗?
2.1 从使用场景反推数据结构,比背命令更靠谱
String是最常见但最容易用错的类型。它除了存字符串,还能存数字、二进制内容。但如果你要存一个对象的多个字段,用String拼JSON就会遇到“改一个字段要整个读出来重新序列化”的尴尬。这时候Hash就合适多了,底层类似一个字典,HGET、HSET可以单独操作某个字段,修改用户昵称不用动整条数据。
List用在一个“有序队列”的场景,比如简单消息队列,左侧LPUSH右侧BRPOP,还能实现阻塞读。但它不是个严格意义上的消息系统,消息没有确认机制,消费端挂了消息就丢了。真要可靠投递,Redis 5.0引入的Stream是更好的选择,支持消费者组和ACK确认。
Set是无序集合,适合做去重、交集并集运算,比如“共同好友”“随机抽奖”。ZSet是带分数的有序集合,底层是跳表加哈希表,排行榜、延时队列、限流窗口都靠它。
2.2 底层编码方式决定内存和性能的平衡
同一种类型,Redis内部会根据数据规模切换编码方式。拿Hash举例,字段少、值小的时候用listpack(老版本叫ziplist的压缩列表),内存省但读写要逐个解析;字段多了就升级成hashtable,空间换时间。这套自动升级机制用户无感知,但你要知道它存在——如果你的Hash长期在临界值附近徘徊,频繁转换会带来额外的性能抖动和内存碎片。
ZSet在小数据量时用ziplist,超过zset-max-ziplist-entries阈值(默认128)转成跳表。跳表的查询是O(logN),插入删除也稳定,这就是排行榜能扛住高并发的原因。
2.3 扩展类型里藏着的实用技巧
很多人不知道Redis还有BitMap、HyperLogLog、GEO。比如“用户签到”这种布尔型记录,用BitMap一个字节存8个用户,万级用户也才几KB内存;UV统计不在乎精确时用HyperLogLog,标准误差只有0.81%,内存占用恒定在12KB左右。选类型的原则我一直是:先算内存量和操作复杂度,再决定用什么类型,而不是顺手就String一把梭。
3. 第3问:RDB和AOF打架,你选谁?——持久化机制详解
3.1 RDB快照:恢复快,但丢数据的窗口你得认
RDB是把某个时间点的全量数据拍成二进制快照。Redis用fork子进程的方式生成快照,就算数据量巨大也不会阻塞主线程。这里有个关键名词“写时复制”,fork出来的子进程和主进程共享内存页,子进程做快照时主进程若改了某个内存页,系统会复制一份再改,保证快照看到的是fork那一刻的数据。
RDB的恢复速度是所有方案里最快的,因为就是加载一个二进制文件。代价是数据丢失窗口大:如果你配置save 900 1,意味着900秒内有1次写操作才触发,极端情况下可能丢最近15分钟的数据。而且大实例fork的时候,如果内存页修改特别频繁,复制开销会让主进程卡顿,这就是所谓“fork抖动”。
3.2 AOF:每笔写操作都记日志,完整性好但文件膨胀
AOF默认每秒刷盘一次(appendfsync everysec),最多丢一秒数据。它的核心问题有两个:一是写入性能相对RDB低,AOF文件是文本协议日志,二是日志越来越大,所以要靠AOF重写机制瘦身。重写的本质是把当前内存里的数据用最精简的命令重新生成一份AOF,这个动作同样由子进程完成,主线程只负责把重写期间的新命令记录到缓冲区。
如果你追求最稳,用appendfsync always每次写操作都刷盘,性能会明显下降但最多丢一条命令。我自己的生产经验是:只要不是银行转账级别的强一致场景,everysec是性价比最高的选择。
3.3 生产环境到底怎么配?混合持久化了解一下
很多人纠结RDB和AOF二选一,其实Redis 4.0以后提供了混合持久化:AOF文件开头是RDB格式的快照,后面追加增量命令。开启aof-use-rdb-preamble yes后,既有RDB的恢复速度,又有AOF的完整性。
我给的参考配置是:RDB作为兜底备份(保留最近几份快照丢到冷存储),AOF开启everysec刷盘,二者同时开。重启时Redis会优先加载AOF,因为它的数据更完整。
| 对比项 | RDB | AOF |
|---|---|---|
| 数据完整性 | 可能丢分钟级数据 | everysec丢1秒,always丢1条 |
| 恢复速度 | 快 | 慢,需重放日志 |
| 文件大小 | 紧凑 | 膨胀,需重写 |
| 对性能影响 | fork瞬间可能卡顿 | 刷盘策略影响写入性能 |
| 生产建议 | 做备份冷备 | 主持久化手段 |
提示:备份不只是配置持久化,我通常还会写个定时脚本,用
redis-cli --rdb定时把RDB文件转储到对象存储或者远端。真遇到服务器磁盘全挂的极端情况,你还能从冷备里救回来。
4. 第4问:键过期了怎么办?内存淘汰和过期删除别再傻傻分不清
4.1 过期键的“被动+主动”双通道删除
Redis给每个带过期时间的key维护了一个过期字典。键过期后并不是立刻删除,而是靠两种机制配合:惰性删除和定期删除。
惰性删除是指访问一个key时先检查是否过期,过期就删掉再返回空。这种策略省CPU,但会导致过期key一直占着内存不释放。定期删除则是Redis每100毫秒(默认hz 10)抽查一批设置了过期时间的key,删除其中已过期的,循环往复。官方注释里写得很清楚,每次抽查上限约20个、如果过期比例超过25%就继续抽查,避免一次性删太多导致阻塞。
4.2 内存淘汰策略:不是“删过期键”,而是“腾空间”
内存淘汰和过期删除完全是两回事。当Redis达到maxmemory上限后,新写入命令会触发淘汰策略,它可能把一个没过期的正常key直接挤掉。这就是为什么有些缓存莫名其妙就没了,数据像是“凭空消失”。
8种策略里最常用的是allkeys-lru和volatile-ttl。allkeys-lru会对所有key按LRU近似算法淘汰,适合把所有数据都当缓存的场景。volatile-ttl只淘汰设置了过期时间的key,且优先淘汰剩余存活时间最短的,适合“有过期时间的数据是缓存、没设置的不能丢”的场景。
4.3 我踩过的坑:allkeys-lru把热点全挤没了
有次上线后缓存命中率急剧下降,查了半天才发现配置写的是allkeys-random,热点key和数据冷门key被随机淘汰,命中率当然雪崩。后来我还发现很多人忽略了一点:volatile-*系列的淘汰策略不会动那些“没设置过期时间”的key。如果你代码里某些缓存忘了设置TTL,内存满时它们就成了“钉子户”,淘汰策略根本不敢碰,最终的结果是大量新数据写不进去。
生产上我的建议是:业务缓存全部设置TTL,内存淘汰策略用allkeys-lru。如果存在“不能丢、无过期时间”的数据,单独Redis实例存放,和缓存实例做物理隔离,别用一个实例混跑。
5. 第5问:缓存穿透、击穿、雪崩三兄弟,你能一眼分辨吗?
5.1 穿透:查了个根本不存在的数据
恶意请求或者代码bug导致查询一个数据库里也不存在的ID,缓存没命中,就会一路打到数据库。如果这个ID是随机变化且不存在的,数据库会被空查拖垮。
三板斧:第一,接口层做参数校验,非法ID直接拒绝;第二,用布隆过滤器判断ID是否存在,不存在就直接返回,不需要碰Redis和数据库;第三,把“查不到”的结果也缓存起来,设置短TTL,比如5分钟,防止同一个空值反复击穿。
5.2 击穿:热点key过期那一瞬间,流量全怼到DB
和穿透不同,击穿是针对一个“特别热”的key。缓存里明明有数据,偏偏在过期的那一秒,几十万请求同时来查,缓存没有,全部穿透到数据库。
处理方案业界通用的是互斥锁和逻辑过期。互斥锁的思路是:缓存miss后先拿分布式锁,只有拿到锁的线程才去查数据库回填缓存,其他线程等待后重新读缓存。逻辑过期更容易理解:缓存value里塞一个逻辑过期时间,后台异步线程负责刷新,读线程永远不会miss,但有可能读到旧数据。
提示:击穿场景下我强烈建议用逻辑过期方案,因为互斥锁虽然严谨,但热点key过期期间请求全部阻塞,延迟会飙升。而逻辑过期最多让少部分请求读到几秒前的旧数据,绝大多数场景完全可接受。
5.3 雪崩:大面积key同时过期,Redis直接被打穿
雪崩有两种:一是大量key设置了相同TTL,同一时间集体失效;二是Redis实例本身宕机,缓存整体不可用。前者的解法很朴素,TTL加随机值,比如基础60秒再加0到300秒的随机抖动;或者把过期时间分散到一天的不同时段。后者的解法是Redis高可用(哨兵、集群)加多级缓存兜底,本地缓存(如Caffeine)扛掉一部分流量,数据库限流熔断。
这三兄弟容易混,我记方法是:穿透是“查不存在”,击穿是“一个热点key过期”,雪崩是“大面积key过期或实例挂掉”。处理手段也从“防御参数校验”到“单点加锁”,再到“全局高可用”,完全不是一回事。
6. 第6问:缓存和数据库一致性,先更新谁才对?
6.1 为什么“先更新数据库再更新缓存”看着合理其实很坑
最常见的一个错误做法是更新数据库后直接SET缓存。问题出在并发场景:两个线程同时更新同一条数据,线程A先更新DB为值1,线程B后更新DB为值2,但线程B的缓存写操作先执行完,线程A的缓存写操作后执行完,最终DB是值2,缓存却是值1——永久的脏数据。
6.2 Cache Aside模式的核心是“删除缓存,而不是更新缓存”
业界最经典的做法是Cache Aside:读请求先读缓存,miss后读DB再写缓存;写请求先更新DB,然后删除缓存。为什么是删除而不是更新?因为删掉后,下一次读请求miss就会从DB重新加载,天然避免了“并发写顺序不一致”的问题。
那“先更新DB再删缓存”和“先删缓存再更新DB”选哪个?理论上都有窗口。先删缓存的情况下,线程A删了缓存、还没更新DB,线程B读缓存miss,查出DB旧值回填缓存,等线程A更新DB后,缓存里又是旧值。所以就有了“延迟双删”:先删缓存,再更新DB,然后隔几百毫秒再删一次缓存,把并发读回填的旧缓存清掉。这个方案在绝大多数场景能把不一致窗口缩小到毫秒级。
6.3 删除失败了怎么办?别把命脉压在缓存服务上
延迟双删有个硬伤:如果第二次删除失败,缓存里还是会残留旧值。我在生产环境常用的兜底方案是监听MySQL的binlog变更,把写操作的订阅消息推给一个异步任务,由它专门负责删缓存。这样即使业务代码里删缓存失败,异步任务会重试,直到删除成功。
提示:一致性从来是分级的。淘宝首页的推荐列表和电商库存的余额,对一致性的要求完全不同。做架构设计时先问清楚业务能容忍多长时间的最终一致,再决定要不要上延迟双删和binlog异步兜底。为了极致一致性而牺牲性能和复杂度,往往得不偿失。
7. 第7问:Redis分布式锁,setnx一下就完事了?
7.1 经典写法:SET key value NX EX才是标准姿势
很多人还在用两段式:SETNX成功后再EXPIRE设置过期时间。这两条命令不是原子的,如果SETNX之后进程崩溃,锁就永远不释放了,这是最常见的生产bug。
正确的做法是用一条命令搞定:SET lock_key unique_value NX EX 30。NX保证只有键不存在时才能设置成功,EX设置自动过期。这里还有两个细节:value必须是唯一标识(比如UUID),用来确认锁是自己的;过期时间必须有,防止持有锁的进程崩溃导致死锁。
7.2 释放锁要用Lua脚本,因为“判断+删除”必须原子
释放锁时不能直接DEL key。假如线程A的锁过期了,线程B拿到锁,此时线程A才执行完业务,一个DEL就把线程B的锁删了。所以标准流程是:先比较value是否自己的,再删除。但比较和删除是两个操作,中间同样有被插入的风险,必须用Lua脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end7.3 锁续期:业务没跑完,锁先过期了怎么办
设置30秒过期,但业务逻辑执行了40秒,锁自动释放,另一个线程进来,两个线程同时执行临界区,分布式锁形同虚设。这个问题用Redisson的看门狗机制解决:默认锁过期时间30秒,拿到锁的后台线程每隔10秒检查一次,如果锁还持有就自动续期到30秒。自己实现也不难,启动一个定时任务在锁剩余时间的1/3处续期即可。
再说深一层:RedLock就是那位国外大牛提出的多节点防主从切换丢锁方案,但业界争议很大。我的看法是,如果用单节点Redis做分布式锁,一定要接受“主从切换瞬间可能丢锁”这个事实。对锁极度敏感的业务(比如金额扣减),要么上RedLock多写几个节点,要么直接用ZooKeeper这类强一致协调服务,别在Redis一棵树上吊死。
8. 第8问:主从复制是怎么工作的?全量同步和增量同步傻傻分不清
8.1 从节点启动时的全量复制是怎么回事
主从拓扑里,从节点启动后发PSYNC ? -1命令,表示“我什么都不知道,全量给我吧”。主节点收到后开始后台生成RDB快照,同时把新收到的写命令写入复制积压缓冲区。RDB生成完发给从节点,从节点加载RDB,然后主节点再把缓冲区里的增量命令发给从节点,最终达到一致。
这里有一个很多人忽视的点:全量复制不只是传RDB文件,还包含“快照生成期间积累的增量命令”。如果主节点是4GB的内存,但生成快照的几秒里写了1万条命令,这1万条命令也必须完整传给从节点,漏一条主从数据就对不上。
8.2 断线重连为什么走“部分重同步”
主从之间的连接断开后,从节点重连会带上自己的复制偏移量offset和主节点的runid。主节点检查复制积压缓冲区repl-backlog里是否还存着从节点缺失的数据,如果缺失的数据还在缓冲区内,就只把缺失部分发给从节点,这就是部分重同步,成本远低于全量复制。
repl-backlog-size默认1MB,太小的话从节点断线一会儿就超过了缓冲范围,只能降级成全量复制。我当时把生产环境的repl-backlog-size调到了64MB,因为业务高峰期主库一秒产生的写命令可能超过几十MB。
8.3 从节点延迟和全量复制风暴
主从复制有一个天然问题:延迟。从节点读取到的数据可能落后主节点几百毫秒甚至几秒,如果你把读流量打到从节点,必须接受数据延迟。我在项目里是把从节点定位成“异步分析、报表查询、备份”,绝对不承担账务类强实时读。
再提一个高阶坑:如果多个从节点同时断线重连,它们会同时向主节点索要全量复制,主节点瞬间fork多个子进程生成多份RDB,内存和磁盘IO都可能被打爆,这就是“全量复制风暴”。解决方案是主节点下挂多个从节点时,控制从节点个数、用树形拓扑分层复制,让部分从节点不直接连主节点,而是级联挂到其他从节点下。
9. 第9问:哨兵到底是干嘛的?它怎么知道主节点挂了?
9.1 哨兵的三件套:监控、通知、自动故障转移
哨兵不是一条队列,也不是一个代理层,它是独立运行的一组进程,职责是盯着主从节点。它每隔1秒向被监控的实例发送PING,如果超过down-after-milliseconds(默认30秒)没收到有效回复,就判定主节点为“主观下线”。注意“主观”这个词,因为单个哨兵看到的可能只是网络分区,不代表主节点真的挂了。
当多个哨兵都判定主节点主观下线,且数量达到配置的quorum(比如配置sentinel monitor mymaster 127.0.0.1 6379 2中的2),就会升级为“客观下线”,触发故障转移。
9.2 选主的规则:优先级、数据完整性、runid,一个都不能少
客观下线后,哨兵们要选出一个新的主节点。选举规则依次是:
- 优先级:
slave-priority配置越小的从节点越优先,0表示永不提升为主 - 复制进度:和数据最接近主节点的从节点,也就是复制偏移量最大的优先
- runid:都满足时,按runid字典序,小的优先
选出新主后,哨兵会向其他从节点发送SLAVEOF命令让它们改认新主,然后通知客户端新主地址。整个过程客户端无感知,前提是配置了正确的哨兵连接方式。
9.3 脑裂场景下如何保命
主节点没真挂,但哨兵和主节点之间网络分区,哨兵那边触发了主从切换,选了一个从节点当新主。此时旧主还在正常工作,继续接收写入数据,等网络恢复后,旧主的写数据因为和新主不一致被当成从节点强制全量同步——这段时间的写入就丢了。
这就是经典的脑裂数据丢失问题。解决办法是给主节点加“最小写可用性”限制,配置min-replicas-to-write 1和min-replicas-max-lag 10,意思是主节点至少要有一个可同步且延迟不超过10秒的从节点才接受写请求。一旦脑裂导致旧主写不出去,它就自动拒绝写请求,保住数据的最后防线。
10. 第10问:Cluster集群的槽位和重定向机制,你真的懂吗?
10.1 16384个槽怎么分,数据怎么路由
Cluster集群把整个键空间划分为16384个槽位。写入一个key时,先计算CRC16(key) % 16384得到槽位,再由槽位归属的节点处理。每个节点只负责一部分槽,槽位分配通过CLUSTER ADDSLOTS完成,也可以让集群自动分配。
缓存客户端连接任意节点都行,如果请求的key不在当前节点,节点会返回MOVED错误,里面带上正确节点的IP和端口,客户端拿到后重新请求。这是和单机Redis最大的差异:客户端必须内置槽路由逻辑,否则每次都得走一次重定向,性能打折。
10.2 扩容迁移的实操:reshard到底迁了什么
给集群加一个节点,核心流程是:新节点加入集群,然后从其他节点迁一部分槽到新节点。官方提供的redis-cli --cluster reshard命令会让交互式指定迁移槽数和新节点,底层做的事是把槽里的key逐条迁移到目标节点。
迁移期间有个细节需要注意:如果客户端在迁移中途访问一个正在迁移的key,源节点会返回ASK重定向,告诉客户端“数据已经在挪去目标节点的路上了,你去目标节点找”。我看过不少文档没把MOVED和ASK区别讲清楚,其实很简单:MOVED表示槽的归属权彻底变了,以后都找新节点;ASK表示槽还在源节点,只是正在迁移的这一个key暂时在目标节点,下一次访问还得问源节点。
10.3 集群的限制,用不好真的会踩雷
Cluster集群对多key操作有严格限制:MGET、事务、Lua脚本里的多个key,必须属于同一个槽。不同槽的key没法在一个命令里处理。解决思路是hash tag:在key里加一对花括号{},Redis只对花括号内的部分计算槽位,把相关数据固定到同一个槽。比如user{10086}:profile和user{10086}:orders会落到同一个槽,就能用事务合Lua了。
另一个坑是集群模式下不能使用SELECT切换数据库,所有键都在db0。我见过老项目从单机迁到集群,代码里带着select(1)直接报错。迁移前先清理这类操作,比上线后再排查成本低得多。
11. 第11问:线上Redis连接超时、内存突增,完整排查链路长什么样
11.1 那个熟悉的“Redis command timed out”到底是谁的问题
很多用Spring Boot的团队都见过这个报错:io.lettuce.core.RedisCommandTimeoutException: Command timed out after 10 second(s)。网上搜到的解释五花八门,我自己经历过几轮排查后总结出最可能的几个原因:
- Redis服务端慢命令阻塞:比如大key的
DEL、大集合的SORT,单线程模型下这些命令一执行,所有连接都在排队,命令自然超时 - 客户端连接池耗尽:Lettuce默认是单连接复用,但如果你用
共享连接不当,或者并发太高,命令在客户端的队列里排队等待 - 网络问题:跨机房访问、带宽打满、防火墙丢包
排查的第一步不是改代码,而是先看Redis侧INFO commandstats和SLOWLOG GET,确认服务端是否有慢命令。我遇到的一次就是某接口在循环里执行了上百次EXISTS,Lettuce的连接被这个循环占满,其他请求全部饿死。
11.2 内存突增和BigKey定位三板斧
内存突增先看是业务增长还是数据异常,我一般按这个顺序来:
INFO memory看used_memory和碎片率,MEMORY STATS看各部分占用- 用
redis-cli --bigkeys扫描大key,它会依次统计每个类型最大的key - 用
redis-cli --memkeys或者SCAN配合DEBUG OBJECT看具体key的编码和内存占用
BigKey的危害不止内存,它是慢查询和集群间数据倾斜的双重源头。在Cluster环境里,一个大Hash如果全落在同一个槽,对应节点的数据量就会明显高过其他人。解决方式是拆分或者换结构,比如把大Hash拆成多个小Hash,用HSCAN位移分桶。
11.3 生产巡检命令和容器部署的细节
日常巡检我固定跑这几条:INFO看内存和连接数、SLOWLOG GET 50看慢查询、INFO stats看过期key淘汰情况、MONITOR只在低峰期偶发用一下——生产环境开MONITOR会显著降低性能,我从不让它长时间运行。
关于Docker部署Redis,热搜词里也有人问主从怎么搭,我提醒三件事:第一,数据目录一定要挂载宿主机卷,否则容器重建数据全没;第二,主从容器之间要用自定义网络通信,别用默认bridge模式然后靠IP拼凑;第三,Docker的日志驱动默认json-file,Redis的日志量一旦起来会撑爆磁盘,建议限制max-log-size或改用local驱动。
12. 第12问:RedisTemplate序列化那些事,为什么你存的键全是乱码
12.1 “乱码”不是Redis的问题,是序列化器的选择问题
用Spring Data Redis操作Redis时,最经典的现象是:你用RedisTemplate插入的数据,在redis-cli里看起来像\xAC\xED\x00\x05t\x00\x05name这种二进制乱码。原因很简单,RedisTemplate默认用的是JdkSerializationRedisSerializer,把对象按JDK序列化成二进制字节后存进去,可读性全靠命令行工具打交道时基本为零。
解决方法是显式配置序列化器。我通常这样配:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key用String序列化,保证可读性 template.setKeySerializer(new StringRedisSerializer()); // value用JSON序列化,兼顾跨语言和可读性 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; }12.2 两个Jackson序列化器的区别,用错会埋雷
Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer长得像,行为不同。前者不存类型信息,反序列化时必须手动指定目标类型;后者会在JSON里多写一个@class字段,自动恢复成原来的类型,但代价是JSON体积变大,而且如果实体类改名或迁移包名,老的缓存数据反序列化就会失败。
我的建议是:业务DTO缓存的,用GenericJackson2JsonRedisSerializer省心;但如果你的实体类结构会演进,记得做好版本兜底,要么上线前清缓存,要么实现自定义的反序列化容错。我踩过最惨的坑是重构了实体包路径,没清理线上缓存,结果用户请求一进来全在反序列化报错。
12.3 可视化工具和连接排错
数据乱码也常被误以为是可视化工具的问题,其实它们是两码事。另一款开源的Another Redis Desktop Manager在平时看排查时好用,因为可以直接看TTL、执行SLOWLOG、浏览各库的key,但工具本身不带容错,键名是\xAC\xED的,它也照原样显示。先保证序列化配置正确,再谈工具好不好用。
这里我把自己的排查顺序列一下:先查redis-cli ping确认服务存活,再看INFO clients连接数是否打满,然后SLOWLOG和MONITOR定位慢命令,最后检查代码里的序列化配置和模板调用方式。按这条路走,90%的“Redis连接超时”“数据乱码”“缓存不生效”问题都能在半小时内找到根因。
十二个问题说完了。其实Redis的每个知识点都藏着“为什么”的追问,面试官问你是表象,你自己能往底层多钻一层,很多坑根本不会踩到。这套清单我每隔半年都会重新过一遍,每过一遍都会有新的体会——尤其是线上出了故障之后再回头看,那些原理性的东西才是真正救命的。