
说在前面Redis 这两年在面试里几乎成了必考项而且问得越来越细。基础数据类型、缓存穿透、缓存雪崩这些算送分题真正拉开差距的往往是对线程模型、内存管理和事务边界的理解。这篇文章把我自己整理的一套 Redis 知识体系串了一遍偏面试向也兼顾实战适合准备跳槽的同学系统过一遍也适合日常用 Redis 做缓存、分布式锁但没时间啃源码的开发者查漏补缺。我尽量用讲人话的方式写该给配置给配置该给参数给参数不绕弯子。你读完之后再去面试最怕的不是被问倒而是被问到细节时只有模糊印象这篇就是帮你把那些模糊印象补扎实的。1. 基础问题先搞清楚 Redis 到底是什么1.1 一个绕不开的问题Redis 为什么快面试官十有八九会问“Redis 为什么快”这题其实是在考察你对 Redis 设计哲学的理解。我习惯把它拆成三层回答第一层数据在内存里。Redis 是基于内存的键值数据库读写不走磁盘这是一切性能的根基。同样是查一条数据MySQL 可能要走 B 树索引、回表、刷盘Redis 直接查内存里的哈希表这个数量级差距是物理层面决定的。第二层单线程执行命令。Redis 的命令执行是单线程的避免了多线程场景下的锁竞争、上下文切换、CPU 缓存失效这些开销。它不是不能多线程而是认为在内存操作这个场景下单线程配合 IO 多路复用已经是性价比最高的方案。第三层IO 多路复用。Redis 用 epoll 同时监听大量客户端连接把网络 IO 的等待时间压缩到最低。关于这一点我在第二章会展开讲。这三层答完面试官一般会满意。但如果你想显得更专业还可以补一句Redis 的底层数据结构也做了大量优化比如压缩列表、跳表、整数集合这些编码方式让它在存储小数据时能省内存又提速度。这个点在后面内存管理部分会细说。1.2 数据类型不止五个是一堆基础题里必考“Redis 有哪些数据类型”很多人张口就是五个String、List、Hash、Set、ZSet。对但不够。站在 2024 年往回看Redis 的常用类型应该是这十种类型底层编码常见典型场景Stringint、embstr、raw缓存、计数器、分布式锁、SessionListquicklist消息队列、时间线、简单栈/队列Hashlistpack、hashtable对象存储、购物车、用户属性Setintset、hashtable去重、标签、抽奖、关注关系ZSetlistpack、skiplist排行榜、延时队列、限流滑动窗口Bitmap基于String的位操作签到、在线状态统计HyperLogLog特殊编码UV 统计、基数估算GEO基于ZSet附近的人、位置计算Streamlistpack、rax树消息队列、事件驱动架构模块系统RedisModules自定义图数据库、布隆过滤器等扩展能力我重点说几个面试常问的String 是 Redis 最基本也是使用率最高的类型。它的 value 可以是字符串、整数、浮点数可以用 INCR、DECR 做原子计数。注意 Redis 的 String 是二进制安全的意味着你可以存序列化后的对象、图片二进制流一切皆字节流。底层编码里如果一个字符串能被解释为 64 位有符号整数且长度小于 20Redis 会用 int 编码直接存整数如果字符串长度短用 embstr 编码内存连续一次分配超过阈值就用 raw 编码。ZSet 是面试高频。底层是跳表加哈希表哈希表负责 O(1) 查成员对应的分数跳表负责按分数排序和范围查询。跳表这种数据结构在很多大厂中间件里都有应用理解了跳表后续看 LevelDB、Kafka 的索引设计会顺很多。Stream 是 Redis 5.0 引入的专门消息队列类型。面试题里经常问“Redis 能不能做消息队列”标准答案就是List 可以做简单队列但不支持消费组Pub/Sub 是即发即弃消费者挂了就丢Stream 才是正儿八经的队列支持持久化、消费组、ACK 确认。用它做 MQ 的场景越来越多尤其在不想引入 Kafka 这种重量级组件的内部系统里。1.3 缓存三大问题穿透、击穿、雪崩这部分基本是送分题但送分题反而容易答不完整。我总结一套固定答法缓存穿透查询一个不存在的数据缓存没有数据库也没有导致请求直接打到数据库。攻击者可以构造大量不存在的 key 来打垮数据库。解决方案有四个按常用程度排序一是缓存空对象即使查不到也缓存一个 null设置短过期时间二是布隆过滤器在缓存前面加一道拦截过滤掉一定不存在的 key三是参数校验比如 id 为负数直接返回错误四是限流降级兜底保护。缓存击穿某个热点 key 过期的一瞬间大量请求同时打到数据库。注意和穿透的区别穿透是查不存在的数据击穿是查一个确实存在但刚好过期的热点数据。解决方案一是互斥锁用 SETNX 让一个请求去重建缓存其他请求等待二是热点 key 不设置过期时间改为逻辑过期由后台任务更新三是预热提前把热点数据加载到缓存里。缓存雪崩大量 key 在同一时间过期或者 Redis 服务直接宕机导致海量请求打到数据库。解决方案一是过期时间加随机数避免大批 key 同时过期二是服务降级和熔断保护 MySQL三是集群和高可用用哨兵或集群模式保证 Redis 本身不挂。顺带一提很多公司在面试之后会延伸问“缓存一致性”Cache Aside 模式先更新数据库再删除缓存。这是最常用的方式但也有缺陷比如删除缓存失败会导致脏数据。延迟双删先删缓存再更新 DB等几百毫秒再删一次缓存用来解决并发读写下的旧数据回填问题。订阅 binlog 异步删除通过 Canal 监听 MySQL 变更用 MQ 异步删缓存这是强一致诉求下最稳的方案。1.4 持久化RDB 和 AOF 怎么选一个容易被忽略的基础题是持久化。Redis 是内存数据库但宕机之后数据不能全丢所以必须有持久化机制。RDBRedis Database Backup把内存里的全量数据拍一张快照存到磁盘。优点是文件紧凑、恢复速度快适合做备份和灾难恢复缺点是快照之间有窗口期如果 Redis 崩溃会丢失最后一次快照之后的所有变更。AOFAppend Only File把每次写命令追加到日志文件里。优点是数据可靠性高默认每秒刷盘appendfsync everysec最多丢一秒数据缺点是文件体积大恢复速度比 RDB 慢而且高并发下 AOF 的刷盘对性能有一定影响。Redis 4.0 之后默认开启了混合持久化也就是 AOF 重写时先生成一份 RDB 快照作为基底再追加增量命令。这样既保证了恢复速度又降低了数据丢失风险。我的建议是如果 Redis 只做缓存数据丢了可以从数据库重建RDB 就够了如果 Redis 里存了重要业务数据生产环境必须开启 AOF并把 appendfsync 设为 everysec。具体怎么配、怎么和主从复制配合我后面内存管理和问题排查部分会继续提到。2. 线程模型单线程背后的多路复用2.1 单线程是事实但不是全部很多人背结论背得很熟Redis 是单线程的。这话对但有歧义。准确说法是Redis 执行命令的主线程只有一个这个线程负责解析客户端命令、执行命令、返回结果。但从 Redis 6.0 开始网络 IO 部分引入了多线程也就是说读写 socket 可以由多个 IO 线程并行处理命令的执行仍然是在主线程里串行完成的。为什么命令执行必须是单线程原因我在前面提过主要是避免锁竞争和上下文切换。但更本质的是Redis 的所有数据结构都是非线程安全的如果让多个线程同时执行命令那就要给每个数据结构加锁复杂度上升性能反而下降。作者 Antirez 在多次分享里都表达过Redis 的瓶颈从来不是 CPU而是内存和网络带宽单线程完全够用多线程带来的收益不值得去换复杂度。面试的时候你可以先旗帜鲜明地说“Redis 命令执行是单线程的”然后再补一句“但 6.0 之后 IO 读写可以多线程别理解成整个 Redis 都是单线程”这样显得你读过源码而不是只看过博客。2.2 文件事件处理器和 IO 多路复用Redis 的网络层核心是文件事件处理器它由四部分组成socket、IO 多路复用程序、事件分派器、事件处理器。流程可以这样理解客户端连接 Redis 时socket 会产生可读事件。Redis 把这堆 socket 全部交给底层的多路复用程序去监听。内核会告诉你哪些 socket 有数据来了。然后 Redis 依次把这些事件放入队列事件分派器从队列里取出事件交给对应的命令请求处理器去执行。这里最关键的机制就是 IO 多路复用。老生常谈的对比是 select、poll 和 epollselect需要遍历所有 fd每次调用都要把 fd 集合从用户态拷贝到内核态fd 数量有限制默认 1024O(n) 复杂度。poll解决了 fd 数量限制但还是要遍历全量 fdO(n)。epoll用红黑树维护 fd用就绪链表记录有事件发生的 fd内核只返回有事件的集合O(1)。这是 Linux 上性能最好的方案。打个比方select 像是保安挨个敲住户门问有没有事epoll 是每家装了一个门铃有事了你按下门铃物业直接上门处理。Redis 在 Linux 上用 epoll在 macOS 上用 kqueue这些都算 IO 多路复用实现。面试里常追问的细节是epoll 的 ET 和 LT 模式区别。Redis 用的是水平触发LT因为编码简单不容易出现漏事件的问题。如果要展示深度你可以说“Redis 事件循环的主体是 aeMain每次循环调用 aeApiPoll 等待事件拿到事件后逐个处理在命令执行完之后还有一个 beforeSleep 回调用来处理写缓冲区里的数据。”2.3 6.0 多线程 IO 到底改了什么这部分是最近两年面试的新考点。Redis 6.0 引入的 IO 多线程核心目的是解决网络 IO 瓶颈。在高并发场景下单线程读写 socket 会成为性能瓶颈因为每次读写都有系统调用而系统调用涉及用户态和内核态切换。开启方式是在 redis.conf 里配置io-threads 4 io-threads-do-reads yes注意默认是关闭的io-threads 默认就是 1。官方建议如果 Redis 实例的 CPU 使用率已经接近 100%可以尝试开启如果 CPU 本来就有富余开了可能没有明显提升甚至因为线程上下文切换带来额外开销。还有一个关键点是IO 多线程只负责网络数据包的读写比如把收到的请求分发到不同线程去解析再把解析后的命令放到主线程执行队列或者把主线程产生的响应数据交给多个 IO 线程写回客户端。命令本身的执行、内存数据的修改仍然只发生在主线程里。因此如果你在面试中答“Redis 6.0 支持多线程了所以是并发执行命令的”那是错的。正确答案是多线程只处理网络 IO命令执行依然是单线程的这也是为了保证数据结构的线程安全性。2.4 单线程模型下最怕的是什么单线程有一个天然弱点只要有一个命令执行时间过长后面所有请求都要排队等待。这就是传说中的“阻塞点”。我整理几个生产环境最容易踩的坑KEYS 命令匹配全库 key数据量大时会阻塞几秒到几十秒。生产环境一律用 SCAN 代替SCAN 是游标式遍历每次返回有限数量。大 Key一个 key 的值特别大比如几 MB 甚至几十 MB 的 String或者一个包含百万元素的 Hash对它做读写、删除、序列化都会占用主线程很长时间。可以用redis-cli --bigkeys来扫描大 key或者用MEMORY USAGE key查具体占用。复杂命令SORT、ZUNIONSTORE、ZINTERSTORE、SINTER 等在数据量大时都可能是慢查询。要开启慢查询日志slowlog监控。FLUSHALL / FLUSHDB清空库在主线程执行数据量大时阻塞严重。可以先禁用命令需要清库时用 SCAN 删除或分批清理。大量超时连接如果客户端频繁断连、重连主线程要处理大量的连接建立和销毁事件也会让 CPU 飙升。我见过一次事故凌晨高峰期有人误执行了 KEYS user:*Redis 直接卡了 8 秒QPS 骤降后面雪崩了一连串服务。从那之后我在所有项目的 Redis 配置里都会加一条rename-command KEYS 从机制上禁掉这个命令。这是很有效的保险措施。3. 内存管理从分配到淘汰3.1 Redis 内存都花在哪里如果说数据类型是 Redis 的脸面内存管理就是 Redis 的命门。一个 Redis 实例能存多少数据取决于内存大小和每个 key 的编码效率。首先要知道 memory 是怎么花掉的。通过INFO memory命令可以看到内存的详细分布。我们主要关注几个字段字段含义used_memoryRedis 分配器分配的总内存字节包含数据、客户端缓冲、复制积压缓冲、AOF 缓冲等used_memory_humanused_memory 的可读格式used_memory_rss操作系统角度看 Redis 进程占用的内存RSSused_memory_peak历史内存使用峰值mem_fragmentation_ratioRSS / used_memory 的比值内存碎片率maxmemory最大可用内存配置内存主要花在五个部分数据内存key-value 本身占用的空间。这部分受编码方式和数据类型影响差异很大。客户端缓冲区每个连接到 Redis 的客户端都会占用内存。一个空闲连接大约占 10KB1 万个连接就是 100MB这个数字很惊人。复制积压缓冲区主从复制时主节点用来缓存写命令的环形缓冲区用 repl-backlog-size 配置默认 1MB。AOF 缓冲区AOF 刷盘前暂存写命令的内存。其他内部结构比如过期字典、哨兵模式下运行的监控信息等。我的排查经验是生产环境里客户端连接数膨胀经常被忽视。某次线上 Redis 内存涨到 3GB 但数据量不大一查 was 8 万多个连接没释放内存全被连接耗光了。处理办法就是设置timeout配置让空闲连接自动断开同时从客户端连接池调小最大空闲连接数。3.2 底层编码为什么有的 Hash 特别省内存很多人不理解为什么 Redis 能在同样数据量下比 MySQL 省那么多内存。关键是底层编码在动态切换。拿 Hash 举例当字段数量少且字段值都比较短时Redis 使用 listpack 编码7.0 之前是 ziplist这种编码把数据紧凑地排布在连续内存里没有指针开销省得非常夸张。当字段数量超过hash-max-listpack-entries默认 128或者某个字段值超过hash-max-listpack-value默认 64 字节时才转为 hashtable 编码。List 使用 quicklist它是由多个 listpack 节点组成的链表每个节点里存一批元素既保证了插入删除的高效又尽量节省内存。Set 的底层有两种intset 和 hashtable。当所有元素都是整数且数量不超过set-max-intset-entries默认 512时用 intset这是最省内存的编码一旦插入非整数或者元素数量超了转为 hashtable。ZSet 类似数据少时用 listpack数据量大时用 skiplist hashtable。知道这些编码有什么用面试会问更重要的是指导实践。比如你有一个 Hash 存用户信息如果你知道字段不会太多、值不会太长就不用管它Redis 自动优化但如果你存的是大批量的小对象可以用 Hash 来分组存储每个字段对应一个对象比散成一个个 String key 内存省很多。这就是 Redis 官方也推荐的“Hash 小对象”优化技巧。3.3 内存淘汰策略8 种策略怎么选如果 Redis 设置了 maxmemory当内存达到上限时新的写入命令会触发淘汰策略。Redis 的淘汰策略从 4.0 之后一共有 8 种策略作用范围说明noeviction不淘汰达到内存上限后写入直接报错默认策略allkeys-lru所有 key按照 LRU 算法淘汰最久未使用的 keyvolatile-lru带过期时间的 key只淘汰设置了 TTL 的 key按 LRUallkeys-lfu所有 key按 LFU 算法淘汰访问频率最低的 key4.0volatile-lfu带过期时间的 key只淘汰设置了 TTL 的 key按 LFUallkeys-random所有 key随机淘汰任意 keyvolatile-random带过期时间的 key只淘汰带 TTL 的 key随机volatile-ttl带过期时间的 key优先淘汰剩余 TTL 最短的 keyLRU 和 LFU 的区别要记清楚LRU 是“最近最少使用”倾向于淘汰长时间没被访问的 keyLFU 是“最不经常使用”倾向于淘汰访问频率最低的 key。LRU 的问题是一个 key 可能是周期性热点比如每天中午被大量访问其余时间没人用LRU 可能会误删它LFU 更精确地跟踪访问次数更适合数据访问频率差异很大的场景。生产环境怎么选如果 Redis 只是缓存可以接受任何 key 被淘汰选 allkeys-lru 最省心。如果不同 key 冷热差异明显且希望保护热点数据用 allkeys-lfu。如果只希望淘汰那些设置了过期时间的数据比如临时验证码、会话 key用 volatile-lru这样能避免误删永久数据。绝对不能容忍淘汰任何数据那就保持 noeviction但这意味着内存满后写入会失败你要有告警和容量规划。我曾经踩过一次坑业务方配置了 allkeys-lru结果用户 token 这种设置了 7 天过期的数据因为很久没活跃被 LRU 提前干掉了用户莫名其妙被踢下线。后来改成 volatile-lru永久 key 永不淘汰问题才解决。所以淘汰策略没有银弹完全看数据特征。3.4 内存碎片和监控手段内存碎片是 Redis 内存管理里的隐藏问题。我们用INFO memory看到 mem_fragmentation_ratio这个值表示 RSS 和分配内存的比值。如果比值大于 1.5说明碎片率较高内存被浪费得比较厉害。如果比值接近 1说明内存使用很紧凑碎片很少。如果比值小于 1说明 Redis 使用了部分 swap也就是内存不足已经开始用磁盘交换空间了这是很危险的信号。碎片产生的原因很常见写入大量的 key然后删除大量 key内存分配器不可能把释放的内存完美地拼回去就会出现碎片。Redis 底层用的是 jemalloc 分配器它会尽量优化但碎片依然不可避免。应对方案一是用CONFIG SET activedefrag yes开启自动碎片整理Redis 会在后台定期搬移内存页来减少碎片。但碎片整理本身也有开销需要配合active-defrag-ignore-bytes和active-defrag-threshold-lower这两个配置来设置触发阈值。二是直接重启。在业务低峰期执行 FAILOVER 让从节点顶上来然后重启老节点内存会被完全规整。这是最粗暴但最有效的手段。三是内存监控自动化。建议把 mem_fragmentation_ratio、maxmemory 使用率、evicted_keys 这几个指标纳入监控告警。特别是 evicted_keys 如果持续增长说明淘汰策略正在频繁生效要么容量不足要么数据过期时间设置不合理。还有一个很实用的排查工具redis-cli --memkeys或者--bigkeys可以扫描出内存占用最大的 key 列表。用它可以快速定位哪些 key 是大头然后决定是拆分还是压缩。3.5 大 key 的识别与治理大 key 是 Redis 内存管理里最典型的实战问题。什么是大 key没有一个硬性标准但通常可以这样判断String 类型 value 超过 100KBHash/Set/ZSet 元素数量超过 5000 个或者整个 key 占用内存超过 10MB。大 key 的危害有四条读写阻塞大 key 的读写、序列化、网络传输都会长时间占用主线程。内存不均匀集群模式下数据分布在不同的分片一个大 key 导致某个分片内存爆炸。删除卡顿DEL 一个大 key 会导致主线程阻塞。删除时要异步化。网络带宽浪费每次传给客户端的响应体积巨大拖慢整体网络。治理方式大 String 拆成小 key比如按时间维度拆分。大 Hash 分桶给 key 加后缀拆成多个小 Hash。删除大 key 用 UNLINK 命令它会让后台线程异步释放内存避免阻塞主线程。给 Redis 配置设定扫描任务定期跑redis-cli --bigkeys巡检。对UNLINK 这个命令一定要记住。它是 Redis 4.0 引入的和 DEL 的区别是DEL 同步删除并释放内存UNLINK 只做逻辑删除内存回收交给后台线程。平时写代码对大对象一律用 UNLINK稳很多。4. 事务轻量但别轻视4.1 基本用法和事务特性Redis 的事务没有 MySQL 那么重它是由一组命令打包一次性执行的机制。核心命令四个MULTI开启一个事务EXEC执行事务队列里的所有命令DISCARD取消事务清空命令队列WATCH监视一个或多个 key乐观锁机制看个最简单的例子127.0.0.1:6379 MULTI OK 127.0.0.1:6379 SET key1 value1 QUEUED 127.0.0.1:6379 INCR counter QUEUED 127.0.0.1:6379 EXEC 1) OK 2) (integer) 1从开启 MULTI 到 EXEC 之间所有命令只是进入队列不会真正执行。直到 EXEC 被调用Redis 才会一次性按顺序执行这些命令。这个过程中间不会有其他客户端的命令插进来因为命令执行是单线程的事务期间整个命令队列是一次性交给主线程连续执行的。DISCARD 的作用就是放弃事务中间有命令不想执行了直接清空队列退出事务状态。Redis 事务要回答的核心问题是它究竟保证什么不保证什么我从 ACID 四个维度给答案原子性部分保证。事务队列里的命令要么都不执行比如网络断开、EXEC 前出错要么大部分执行。如果某个命令在 EXEC 执行期间报错了Redis 不会回滚已经执行成功的命令而是继续执行后面的命令。这一点和 MySQL 很不一样。一致性基本保证。即使命令出错Redis 内部的数据结构状态仍然是正确的不会出现数据错乱。隔离性天然隔离。由于命令执行是单线程的事务执行期间不会有其他命令穿插进来所以不存在并发事务互相干扰的问题隔离级别可以看作“串行化”。持久性不保证。是否持久化取决于持久化配置如果 RDB/AOF 写盘失败事务提交的数据可能丢失。4.2 三种容易混淆的错误情况面试中经常问“Redis 事务中命令出错会怎么样”。这里要分三种情况第一种入队时报错。比如命令语法错误、key 参数个数不对MULTI 之后的命令在入队时就被 Redis 发现了会返回错误信息此时 EXEC 会直接返回失败整个事务不执行任何命令。可以认为这种错误是事务级别的错误Redis 会拒绝执行。第二种执行时报错。命令语法没错但运行时出错。比如对一个字符串类型的 key 执行 LPUSH这种错误不会阻止其他命令执行EXEC 会返回一个错误数组其他正确命令的结果照常返回。这是最坑的地方你已经执行了一部分命令但另一部分失败了还没有回滚。第三种WATCH 冲突。这就是乐观锁场景。客户端 A 和客户端 B 同时 WATCH 了一个 key然后各自开启事务。如果 A 在事务 EXEC 之前修改了这个 key那么 B 在 EXEC 的时候会返回 nil表示事务执行失败需要重试。看代码更直观# 乐观锁实现账户扣减 WATCH account:balance bal GET account:balance bal bal - 100 MULTI SET account:balance bal EXEC如果 EXEC 返回 (nil)说明在 WATCH 之后、EXEC 之前有别的客户端修改了 account:balance此时整个事务被放弃你必须重新读取余额再试一次。这就是 Redis 版的 CASCompare And Swap。MySQL 用版本号字段实现乐观锁Redis 用 WATCH 实现思路是一样的。4.3 为什么 Redis 不支持回滚这个问题经常作为加分项被追问。我的理解是作者 Antirez 是刻意不做回滚的原因主要有三个第一Redis 事务中的错误大多属于编程错误比如对字符串执行了列表操作、使用了不存在的命令。这类错误在开发阶段就能发现不应该让生产环境去回滚兜底。第二回滚机制本身很复杂。MySQL 的回滚依赖 undo log需要记录每个操作的逆向操作遇到并发、子事务、外键约束等情况时会变得极其复杂。Redis 设计哲学是极简不想为了一小撮错误场景引入整套回滚机制。第三性能代价。回滚意味着要缓存每个命令执行前的数据快照这在内存数据库里是难以承受的额外开销。Redis 宁愿保持事务高速执行也不愿意给 99.99% 的正常请求加上回滚负担。所以面试时如果被问到“Redis 事务和 MySQL 事务的区别”可以这样答MySQL 提供完整的原子性和回滚适合强一致的金融场景Redis 事务更像是一个命令批量执行器它保证的是命令不被其他客户端插入执行而不是真正的失败回滚。需要真正的原子性时建议用 Lua 脚本。4.4 Lua 脚本一种比事务更强的方式Redis 从 2.6 开始支持 Lua 脚本。用 EVAL 命令执行一段 Lua 脚本整个脚本在 Redis 服务端是原子执行的。和事务对比事务中的命令是逐个执行的如果中间有命令失败后面的命令还会继续Lua 脚本一旦执行脚本里的所有命令作为一个整体不会被其他命令插入。事务不能做逻辑判断和流程控制Lua 脚本可以写 if/else、循环能实现复杂的业务逻辑。事务执行前不能从业务端拿到中间结果Lua 脚本可以在脚本内部读取中间值做条件判断然后决定下一步。一个经典的扣库存例子-- 扣减库存库存不足返回 -1 local stock redis.call(GET, KEYS[1]) if not stock then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return -1 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 0调用方式EVAL 脚本内容 1 stock:1001 5Redis 会为 Lua 脚本计算 SHA1 校验和你可以用 SCRIPT LOAD 预加载脚本然后通过 EVALSHA 调用减少每次传输脚本内容的网络开销。分布式场景下用 Redisson 的 RLock 也是一种思路但和 Lua 脚本解决的不是同一个问题。Python 客户端里用这段脚本做原子扣减比先 GET 再 DECR 要安全得多import redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) script local stock redis.call(GET, KEYS[1]) if not stock then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return -1 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 0 # 把脚本加载到Redis返回sha1 sha r.script_load(script) # 用sha调用避免每次传整个脚本 result r.evalsha(sha, 1, stock:1001, 5) print(result)需要提醒的是Lua 脚本虽然原子但也怕长时间运行。脚本里千万不能写死循环也不要做耗时的操作因为脚本运行期间主线程被完全占住所有其他请求都会阻塞。官方不建议脚本里做网络请求等外部调用这不是 Redis 的设计用途。4.5 分布式事务和 Redis 的关系Redis 事务常被拿到分布式场景里聊这里要厘清一个概念Redis 事务是单机层面的不是分布式事务。如果业务要操作多个 Redis 节点比如集群模式MULTI/EXEC 在单节点上才有效跨节点无法保证原子性。真正要跨节点保证一致性得靠其他方案比如两阶段提交2PC但 Redis 官方没提供。基于消息队列的最终一致性。事务消息 本地消息表。引入 Seata 这类分布式事务框架。所以面试中如果问“用 Redis 事务实现分布式事务行不行”答案很明确不行。Redis 事务只保证单实例上的一批命令不被穿插执行不解决多个服务之间、多个存储之间的一致性问题。那 Redis 在分布式事务里能干嘛它的典型角色是分布式锁。实现方式有两种一是 SETNX 过期时间SET lock:order:1001 token_value NX PX 30000第一个去抢锁的客户端能设置成功其他客户端设置失败。业务执行完用 Lua 脚本对比 token 再释放锁防止误删别人持有的锁。为什么释放锁要用 Lua因为需要先 GET 判断 value 是否等于自己的 token再 DEL这两步如果分开执行中间锁可能已经过期被别的客户端强占就会误删。用 Lua 脚本包起来就是原子的。二是 Redisson 的实现。Redisson 的分布式锁会在后台启动一个看门狗线程默认每 10 秒给锁续一次 30 秒的过期时间防止业务没执行完锁就自动释放。同时它还实现了红锁RedLock的变体但红锁本身在业界争议很大一般单体 Redis 主从哨兵已经能满足绝大多数业务。4.6 事务和 Pipeline 的区别还有一个高频考点事务和 Pipeline 有什么区别它们看起来都是在一次请求里发一堆命令但本质完全不同Pipeline 是客户端行为把多个命令打包发给服务端服务端依次执行减少网络 RTT。它不保证原子性也不保证中间不被其他客户端插入。事务是服务端行为通过 MULTI/EXEC 把命令在服务端排队EXEC 时连续执行保证中间没有其他命令插入。Pipeline 可以配合事务使用用 Pipeline 发送 MULTI、一堆命令、EXEC既能减少网络往返又能保证原子性。很多 Redis 客户端库都支持这种方式。5. 常见问题与排查技巧实录5.1 面试必问Redis 挂了怎么办这个问题其实是在考高可用体系。我一般从三个层面回答一是数据层面用持久化保证不丢数据。RDB AOF 双开至少做到最多丢一秒数据。二是主从层面用主从复制保证读写分离和故障切换。一个主节点挂一个或多个从节点从节点同步主节点的数据。主节点挂了人工把从节点提升为主节点。但这有延迟如果主节点在同步完成前挂了没同步的数据就丢了。三是哨兵层面用 Sentinel 自动完成故障检测和切换。哨兵节点监控主从节点的健康状态当主节点不可达时哨兵集群通过投票选出一个从节点提升为新主节点并通知客户端更新连接信息。哨兵解决的是“Redis 挂了之后自动切换”的问题但要避免脑裂问题需要合理配置 quorum 和 majority。四是集群层面用 Redis Cluster 做数据分片和高可用。数据通过 CRC16 哈希槽分布到 16384 个槽位每个主节点负责一部分槽位主节点挂了由从节点顶上。集群模式适合数据量超过单机内存、需要横向扩展的场景。这里有个面试题常踩的坑主从复制是异步的会产生数据不一致。如果你对一致性要求极高不要依赖主从而应该考虑强一致方案比如引入数据库事务、或者用 RedLock 但也要清楚它的一致性局限。5.2 线上排查一个 Redis 问题排查案例说一个我处理过的典型线上问题帮助你把前面的知识串起来。现象某服务凌晨出现大量超时Redis 的 QPS 没有明显上涨但 P99 延迟从 2ms 飙升到 2s。排查步骤第一步先看基础指标。INFO memory发现 used_memory 正常但 used_memory_rss 偏高碎片率 2.4。这里虽然有碎片但还不至于造成明显延迟。第二步看慢查询日志。SLOWLOG GET 50发现大量阻塞命令来源是 KEYS user:online:*。排查代码发现有一个定时任务每天凌晨全量刷新在线用户调用了一个封装方法内部就是用 KEYS 匹配前缀。这个命令在几百万 key 上全表扫描导致主线程被卡死。第三步看客户端连接数。连接数高达 6 万大量连接处于空闲状态。继续排查发现某业务方的连接池配置了无限最大连接数服务扩容后连接数线性增长。处理方案禁用 KEYS 命令定时任务改成 SCAN 游标式遍历。给 Redis 配置timeout 300让空闲连接自动释放。业务方连接池设置maxTotal 200避免无限制创建连接。对大 key 用 UNLINK 异步删除。这个案例里我用了前面讲的三个知识点单线程阻塞点、内存碎片率、客户端缓冲区内存。说明 Redis 问题往往是连环的一个慢命令会造成连锁反应需要系统性排查。5.3 一套完整的 Redis 配置参考给出一份我在生产环境常用的核心配置方便你对照调整# 内存和淘汰 maxmemory 4gb maxmemory-policy allkeys-lru maxmemory-samples 10 # RDB 持久化 save 900 1 save 300 10 save 60 10000 rdbcompression yes dbfilename dump.rdb dir /var/lib/redis # AOF 持久化 appendonly yes appendfilename appendonly.aof appendfsync everysec no-appendfsync-on-rewrite yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 慢查询 slowlog-log-slower-than 10000 slowlog-max-len 128 # 客户端和连接 timeout 300 tcp-keepalive 60 maxclients 20000 # 安全 rename-command KEYS rename-command FLUSHALL rename-command FLUSHDB # 内存碎片整理 activedefrag yes active-defrag-ignore-bytes 100mb active-defrag-threshold-lower 10 active-defrag-threshold-upper 100 # 主从复制 replica-read-only yes repl-backlog-size 64mb这些配置的具体值要根据业务场景微调但思路是通用的内存必须有上限和淘汰策略AOF 必须开启且 everysec 刷盘慢查询要记录危险命令要禁用链接要设置超时碎片整理要打开。5.4 面试的追问和加分答法最后分享几个我面试候选人时爱追问的问题也是我自己整理八股时留下的笔记追问一Redis 单线程为什么还需要分布式锁答案是单线程只保证同一个 Redis 实例上的命令是原子执行的。但在分布式系统里多个服务实例共享一个 Redis它们的命令还是一个个独立到达的如果没有锁机制两个实例的“读余额、扣减、写回”三步会被穿插执行产生超卖。分布式锁给的是“多个客户端之间的互斥”和 Redis 内部的单线程并不冲突。追问二SETNX 实现分布式锁有哪些坑一是死锁拿到锁的客户端挂了锁永远不释放。加过期时间解决。 二是误删A 的锁还没到期B 抢到锁A 执行完把 B 的锁删了。用 token 校验删除时用 Lua 脚本判断是否是自己的锁。 三是过期时间太短业务没执行完锁就过期了别的客户端进来了。用 Redisson 看门狗自动续期。 四是主从切换导致的锁丢失主节点还没来得及同步锁数据就挂了从节点顶上锁没了。这是 RedLock 想解决的问题但 RedLock 自身也有争议。追问三Redis 内存满了会发生什么如果配置了 maxmemory-policy会触发淘汰策略最常用的是 allkeys-lru。如果是 noeviction写入命令直接报 OOM 错误读请求不受影响。生产环境一定要把内存监控配好不要让 Redis 进入持续淘汰的状态否则缓存命中率暴跌数据库压力飙升。追问四为什么不建议用 Redis 做消息队列要看场景。轻量级、允许消息丢失、不需要复杂消费组的场景Stream 完全够用。但如果你的系统需要严格的消息不丢失、顺序保证、消息回溯、大规模消费者集群还是上 Kafka 或 RocketMQ 这类专业 MQ 更合适。Redis 的定位是缓存和高速数据访问不适合承担核心消息链路。写在最后Redis 的知识点非常多但核心主线其实很清晰底层数据结构决定了它能存什么、省不省内存线程模型决定了它的性能边界内存管理决定了它在高负载下怎么表现事务和 Lua 脚本决定了它能不能承担业务逻辑。把这四条主线串起来Redis 就不再是背八股而是一个完整的系统。我从这两年的实践中最大的体会是不要为了面试而背答案而是要把每个结论都落到真实场景里验证一遍。比如你以为 KEYS 命令只是慢实际线上真的会因为一个误操作让整个集群瘫痪你以为事务不回滚是无伤大雅的特性实际写业务代码时需要非常小心地处理部分成功的情况。只有亲手踩过坑才能把知识变成直觉。