☰
Redis全栈学习指南:从基础到集群源码的进阶路线
2026/9/29 17:34:01 网站建设 项目流程

做后端这么多年,Redis 几乎是每个项目的标配,但实事求地说,能把 Redis 讲透的资料非常少。大多数时候我们只是塞了个 RedisTemplate,用一个缓存注解,集群模式停留在“听说过”的程度,面试问到底层原理就含糊其辞。前阵子翻到一套“Redis 全栈小册”,标题虽然朴素,内容却从基础、应用一路覆盖到原理、集群、拓展和源码。我花了一个周末通读,边看边把这些年线上踩过的坑全对上了号。这篇文章就把我认为最值得反复看、也最贴近实际工作的一条 Redis 学习路径整理出来,不整虚的,想系统掌握 Redis 的开发者照着走就行。

1. 基础篇:先把 Redis 的“手”和“脚”接上

1.1 五大基础数据类型,一张表记住用法

Redis 的入门门槛低,几乎所有人第一次接触都是那几个命令:SET、GET、DEL。但真正到生产环境,经常能看到有人把所有东西全塞进 String,List 只用来当普通队列,ZSet 一辈子没用过。这个现象很普遍,原因是大部分人没有把数据类型和应用场景建立映射。我把五大数据类型按“场景记忆法”重新梳理了一遍,用起来会顺手很多。

String 是最通用的键值模型,适合计数器、缓存、分布式锁、Session 共享。命令抓住SET key value NX EX seconds、INCR、GETSET这几个核心就够了。其中INCR是原子操作,不用再拿锁去保护计数逻辑,秒杀库存扣减的前置计数经常用它。Hash 适合存对象,一个 key 对应一个用户、一个商品,字段就是对象的属性。相比把整个对象序列化成 JSON 塞进 String,Hash 的好处是能单独更新某个字段,比如只改用户头像,不需要把整个对象取回来再写回去。购物车、用户信息、商品详情这类场景用 Hash 特别合适。

List 的双端队列特性,让它在消息队列、最新消息列表这种场景很顺手。RPUSH往右推,LPOP从左边取,天然就是个 FIFO 队列;BLPOP还能阻塞等待,避免客户端空转。要注意 List 做队列功能时,Redis 本身没有确认机制,业务消费失败后消息就丢了,这个逻辑得靠业务代码兜底。Set 的核心是去重和集合运算,标签、关注关系、抽奖名单都能用SADD、SINTER、SUNION一次算出来。关注列表这种双边关系,用 Set 分别存粉丝和关注名单,算共同好友就是SINTER,一个命令搞定,比在数据库里用 IN 查询高效得多。

ZSet 是加分项,排序逻辑交给 Redis 而不是业务代码。每个成员带一个 score,排行榜就是ZREVRANGE,按时间范围刷帖子就是 ZSet 存时间戳,按分数范围筛选就是ZRANGEBYSCORE。ZSet 底层跳表的设计,让插入和查询之间取得非常好的平衡,这也是后面读源码时会反复遇到的核心结构。

除了这五种,还有几个容易被忽略的进阶类型。Bitmaps 可以用位运算统计用户签到、在线状态,内存占用极低;HyperLogLog 做 UV 统计误差在 0.81% 左右,但内存只需要 12KB;Geo 类型存经纬度,做附近的人、附近门店非常方便;Stream 是 Redis 5.0 引入的消息队列,比 List 那个临时方案完整得多,后面拓展篇会单独展开。基础阶段的重点不是背完所有命令,而是看到业务场景能想到该用哪个类型,这一步做好了,后面应用层才不会变形。

1.2 环境安装:编译、Docker、客户端都不落下

环境安装是我见过翻车率最高的环节,尤其是刚接触 Redis 的人。官方源码编译是最标准的做法,下载稳定版源码包解压后依次执行make和make install,装完在/usr/local/bin下就有redis-server和redis-cli。如果有编译依赖问题,多半是缺少 gcc 或者 make,装好再编译就行。

平时做开发我强烈建议直接用 Docker,省去编译和系统依赖的麻烦。一条命令就能起一个可用的 Redis:

docker run -d --name redis-dev -p 6379:6379 -v /data/redis:/data redis:7-alpine redis-server --appendonly yes

这里我把持久化目录挂到了宿主机/data/redis,并且开了 AOF。开发环境这样用足够了,生产环境再考虑独立部署和集群方案。启动后先别急着写代码,用redis-cli ping验证一下,返回 PONG 就说明通了。

还有一个几乎必踩的坑:redis.conf 里的bind 127.0.0.1和protected-mode yes。本地测试没问题,但要在服务器上提供服务或者连远程客户端,你就会遇到连接失败。正确做法是明确bind内网网卡地址,关闭 protected-mode 或者配好密码,千万不要图省事直接注释掉 bind 让 Redis 暴露在公网。这类安全问题在真实生产里出过太多事,被扫描到然后挖矿的情况并不少见。

桌面客户端我推荐 Another Redis Desktop Manager,它界面直观,能直接看 key 扫描结果、内存占用,还能执行命令。不过我还是要多说一句,日常排查问题时,请先相信redis-cli,因为它能看到连客户端界面都不一定暴露的原始信息,比如INFO、MEMORY、SLOWLOG这类底层诊断命令,命令行比图形界面方便得多。

2. 应用篇:缓存、锁和一致性,这才是生产级用法

2.1 缓存穿透、缓存击穿、缓存雪崩怎么打

基础阶段学会读写 Redis 之后,第二阶段最容易出现的问题是把 Redis 当成“缓存数据库”用,也就是先查 Redis,没有就查 MySQL,查到再回填 Redis。这个模型看起来简单,但一旦遇到高并发,三个经典问题会轮流来找你:缓存穿透、缓存击穿、缓存雪崩。

缓存穿透指的是请求的数据在数据库里根本不存在,所以 Redis 里也永远没有,请求每次都打穿到数据库。如果恶意用一个不存在的 ID 刷接口,数据库会被查挂。应对办法有三个:入口参数先校验,明显不合理的 ID 直接拒绝;第二个是布隆过滤器,把所有可能存在的数据的特征提前放进去,查询前先判断,能挡掉大部分不存在的 key;第三种是即使查不到数据,也把空结果缓存几分钟,设置很短的过期时间,避免同一空 key 反复穿透。

缓存击穿则是某个热点 key 恰好过期了,同一瞬间大量请求同时落到数据库。这种情况的典型特征是“只有一个 key,但访问量极大”。最常用的解法是互斥锁:查询数据库之前先尝试拿分布式锁,拿到的线程去查库并回填缓存,拿不到的线程先短暂等待再重试。另一种思路是逻辑过期,也就是缓存不设置物理过期时间,而是塞一个逻辑过期时间字段,读的时候发现逻辑过期就异步刷新,同时老数据继续提供服务,体验会平滑很多。

缓存雪崩是大量 key 在同一时间过期,导致大量请求同时穿透。最常见的原因就是设置了相同的过期时间,比如“统一晚上 0 点失效”“统一 30 分钟”。解法也简单:过期时间加一个随机偏移量,把集中失效打散;再加一层多级缓存兜底,请求先走本地缓存再走 Redis,数据库永远放到最后;同时服务入口做好熔断降级,哪怕缓存全挂了,也要避免把数据库打满。

这里我特别想强调的是顺序。很多人遇到这三个问题会一上来就写布隆过滤器,但布隆过滤器只能解决穿透,不能解决击穿和雪崩。正确顺序应该是先把过期时间随机化,这是最便宜、收益最高的动作;再给热点 key 做互斥锁或逻辑过期;最后才是上布隆过滤器或空值缓存兜底。每一层解决一类问题,互相不能替代。

2.2 分布式锁的正确姿势,绕开那三个经典坑

分布式锁是我面试时最常问、也是线上出问题最多的点之一。先看一个最朴素的写法:SETNX lock_key,拿到锁之后处理业务,最后DEL lock_key。这个写法有三个致命问题。

第一个问题是锁没有过期时间。如果拿到锁的进程在业务执行到一半时挂了,锁永远释放不了,后面的请求全部阻塞。改进方法是给锁加过期时间,但大部分人的第一版实现是SETNX之后再EXPIRE,两步操作不是原子的,进程在 SETNX 和 EXPIRE 之间挂了,锁一样会死。正确写法是使用单条原子命令:

SET lock_key client_id NX PX 30000

一次调用完成“加锁 + 设置过期时间”。第二个问题是释放锁的时候不能随便DEL,因为有可能当前线程的锁已经过期,另一个线程拿到了锁,这时如果原来的线程执行完一DEL,就把别人的锁删掉了。所以释放锁之前必须先比较value是不是自己写入的那个客户端标识,相等才能删除。比较和删除同样要保证原子,标准做法就是用 Lua 脚本,因为 Redis 内置执行 Lua 脚本时单线程串行,天然不会中间被打断:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

第三个问题是业务执行时间超过锁的过期时间,锁自动释放了,后面的线程又进来,同一个资源被并发操作。这个问题在 Redisson 里叫看门狗,它会在锁快过期时自动续期,直到业务正常释放。如果你用的是 go-redis 这类库,没有内置续期,就要自行评估业务最大执行时间,把过期时间设得足够长,或者在业务里主动续期。

顺便说一句 RedLock 的争议。RedLock 试图用多个 Redis 节点来保证分布式锁的可靠性,但在网络分区这种极端情况下依然存在理论漏洞,分布式系统的大佬们为此争论过很多轮。我的实际建议是:绝大多数业务场景,单机 Redis 或者哨兵模式下的分布式锁已经足够;如果对一致性要求真的极高,请考虑 etcd、ZooKeeper 这类带强一致语义的组件,而不是强行 RedLock。

2.3 缓存与数据库一致性,别被“删缓存”带偏

缓存和数据库的一致性是应用层最难缠的话题。最被推崇的是 Cache Aside 模式:读的时候先读缓存,没有就查库并回填;写的时候先更新数据库,再删除缓存。注意这里是删除缓存而不是更新缓存,因为更新缓存可能产生并发写顺序问题,而删除之后下一次读自然会回填最新数据。

但“先更新数据库,再删除缓存”也有自己的问题:更新库成功、删缓存失败,缓存里还是旧值。解决思路有两种,第一种是延迟双删,先删除缓存,再更新数据库,隔一小段时间再删一次,这个思路能覆盖大部分问题但不够优雅;第二种是订阅数据库 binlog,把删除缓存这个动作变成一个异步任务,保证最终删除一定会执行。后者在 Canal 这类中间件的帮助下是生产环境比较稳的方案。

还有一个很多人容易混淆的点:强一致和最终一致的差别。数据库和缓存是两个存储系统,在没有引入分布式事务的前提下,追求强一致基本是伪命题。正确的目标是把不一致的时间窗口压缩到足够小,再通过兜底策略让它最终收敛。所以实际工程里我会做三件事:先保证“更新数据库成功之后必须触发删缓存”,把失败的动作丢进本地消息表或定时任务重试;再对缓存设置一个合理的 TTL,即使删除失败,过期之后也能自愈,这是最重要的兜底;最后把对一致性要求极高的场景单独设计,不要指望缓存的那点加速一次性解决所有问题。

这一节最后提一下旁路缓存的两个性能细节。批量查询时尽量用MGET而不是循环GET,一次网络往返能省下大量 RTT;缓存对象如果很大,压缩算法比如 LZ4 或者 Gzip 可以先处理一遍,Redis 的带宽往往是高频场景最先触到的瓶颈。这些细节不需要花费太多成本,但对性能的提升非常明显。

3. 原理篇:Redis 快的原因,全部藏在源码里

3.1 说它是单线程,其实是多路复用

Redis 性能好的原因被简化成“单线程没有上下文切换”,这个说法太片面。严格来说,Redis 处理命令的核心模型是事件驱动的单线程循环,但它同时使用了操作系统提供的 I/O 多路复用机制,最常见的底层就是 epoll。Redis 主线程只需要在一个事件循环里同时监听大量客户端连接的可读可写事件,哪个 socket 有数据就处理哪个,没有数据就阻塞等待,CPU 不会因为轮询所有连接而空转。

单线程最大的收益其实是避免共享数据竞争,模型里永远只有一个命令在被执行,所以 Redis 内部大量数据结构不需要加锁。Redis 6.0 开始引入了多线程,但这些线程只负责网络数据的读写,真正执行命令的主流程依然单线程。换句话说,多线程解决了网络 I/O 的 CPU 开销,但没有破坏命令执行的原子性。

理解这个模型对实际调优很有帮助。不要写执行时间长的命令,比如KEYS *、大范围SMEMBERS、超大 key 的DEL,因为它们是阻塞主线程的。应该用SCAN分批遍历代替KEYS *,用UNLINK代替DEL做异步删除。生产环境慢查询日志里出现最多的,几乎都是这种“单个命令过大”的问题,搞清楚事件循环后就完全能理解了。

3.2 底层数据结构:每种类型都有两副面孔

Redis 每种对外数据类型,内部都有不止一种编码。设计这个两层结构的目的很简单:小数据用紧凑结构省内存,大数据用高效结构保性能。读源码时最有趣的也是这套编码切换逻辑。

String 内部用的是 SDS,也就是简单动态字符串,而不是裸的 C 字符串。SDS 记录了长度,获取字符串长度是 O(1);同时有预分配机制,追加字符串时不用频繁重新分配内存,还能二进制安全地存任意内容。Hash、ZSet 在元素少、值小的时候用 listpack 这类紧凑编码,元素超过阈值后自动切换成 hashtable 或 skiplist。比如 ZSet 底层就是listpack + dict + skiplist的组合,dict 负责按 member 查找分数,skiplist 负责按 score 排序,两个结构一起才实现 O(logN) 的范围查询。

List 的结构演变特别说明问题。早期版本用双向链表,内存碎片多;后来改成 quicklist,也就是把多个压缩块用链表串起来;到了 Redis 7.x 又引入了 listpack,整体设计一直在朝“少指针、少碎片、省内存”的方向走。Set 也是同样道理,全部元素都是整数且数量少时用整数集合 intset,一旦不满足条件就升级成 hashtable。

这部分理解以后,有两个实际收益。第一,写数据时注意元素规模,小对象不要盲目塞大量字段,它们会触发编码升级,内存占用可能从几十字节跳到几百字节;第二,排查内存问题时,用OBJECT ENCODING key能看到到底用的是哪种编码,这比猜要快得多。

3.3 持久化:RDB 和 AOF 怎么选,别再靠感觉

持久化是很多人配置上最随意的一环。RDB 是内存快照,通过 fork 子进程生成二进制文件,恢复速度快,但可能会丢失最后一次快照之后的数据;AOF 记录每一条写命令,通过 appendonly 开启,配合 fsync everysec,最多丢失一秒数据,但文件体积大,恢复速度相对慢。

具体配置上,生产环境我一般这样取舍:

RDB 适合做备份和快速重启恢复。定时任务每天做一次BGSAVE,冷备份放到异地或者对象存储,这个是灾难恢复的底线。AOF 适合保证数据安全。appendfsync设成 everysec,在性能和可靠性之间最平衡;always 模式每写一条命令都 fsync,对性能影响明显,除非数据极度重要否则不建议。

Redis 4.0 之后有了 AOF 重写,机制是将当前内存状态转成命令写入新 AOF,跟 RDB 的原理类似,都是为了压缩文件体积。Redis 7.x 进一步引入了 AOF 与 RDB 的混合持久化,AOF 文件头部用 RDB 格式存全量数据,后面再追加增量命令,兼顾恢复速度和文件大小。如果用的是较新版本,直接开启混合持久化是合理的选择。

还有一个实际教训:很多人部署 Redis 默认不开 AOF,一旦机器重启,几小时的数据直接没了。如果业务允许最多丢一分钟数据,请至少把 appendonly 打开;如果完全不能容忍丢失,那要评估的是 OS 层和网络层的稳定性,而不是只靠 Redis 自己。

4. 集群篇:单机跑不动了,怎么横向扩展

4.1 主从复制:最朴素的读写分离

单机 Redis 做到了高吞吐的起点,但无法应对两个问题:单点故障和数据容量的水平扩展。主从复制是第一步,结构上一台主节点接收写请求,多台从节点接收读请求,写数据异步同步给从节点。配置从节点最简单的方式是REPLICAOF master_ip master_port,或者直接写进 redis.conf,然后从节点默认只读。

主从同步的核心是复制积压缓冲区。第一次连接用全量同步,主节点生成 RDB 快照发给从节点;从节点加载完 RDB 后,主节点再把同步期间的写命令通过积压缓冲区补发。后续同步变成增量同步,只需要把断线期间的命令补发,所以从节点的断线恢复很快,只要它的断开时间没有超过缓冲区的覆盖范围。

主从模式有几个实践要点。有人说主节点不要开持久化,这个说法是错误的,主节点至少开 AOF,因为从节点的数据源头就是主节点,主节点重启如果没持久化,可能直接用从节点数据反向追主,造成数据丢失。从节点数量不是越多越好,每个从节点都会增加主节点的网络输出压力,合理做法是挂 2 到 3 个从节点,或者用从节点的从节点来分摊。最要紧的是,主从复制是异步的,写主节点成功后,读从节点可能还没有读到,强一致场景不能直接依赖从节点读。

4.2 哨兵:让故障转移自动发生

主从模式把故障转移留给了人工,哨兵就是来解决这个问题的。Sentinel 是一个独立进程,监视主节点和从节点状态,在主节点下线后自动选举一个从节点升主,并修改其他从节点的主从关系,最后把新主节点的地址通知给客户端。

哨兵判断下线分两个阶段。单个哨兵自己发现某个节点down了,这叫主观下线;当多个哨兵都报告同一个节点不可达,达到配置的 quorum 数量,就更新为客观下线,开始触发故障转移。故障转移时选举从节点的依据,是优先级、复制偏移量、运行 ID 这几项的综合排序,尽量选数据最完整、延迟最低的从节点。

生产配置哨兵有两个容易忽略的细节。第一,哨兵至少要部署 3 个节点,因为仲裁需要多数派,如果只有 2 个,一个哨兵挂掉就永远达不到 quorum。第二,客户端要有哨兵地址而不是固定主节点地址,否则主节点切换后,老客户端还往旧主节点写,等于全部写入失败。像 Jedis、Redisson、go-redis 都有对应的哨兵模式配置,启用之后客户端会自动感知主节点变化。

给一个最小哨兵配置参考:

sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000 sentinel parallel-syncs mymaster 1

2代表至少两个哨兵同意才算客观下线;down-after-milliseconds是判断节点失联的时长;parallel-syncs控制在故障转移时同时允许几个从节点同步新主节点,设 1 是为了避免网络冲击。这几个参数直接决定故障转移的快慢,可以根据业务容忍度调整。

4.3 Cluster 模式:16384 个槽是怎么分的

如果数据量已经超过单机内存,读写性能也到了瓶颈,就用 Redis Cluster。Cluster 把数据按 key 做 CRC16 哈希,对 16384 个槽位取模,每台主节点负责一段槽位。客户端根据槽位和节点槽位路由表,直接找到对应节点。如果一个 key 不在当前节点上,节点会返回MOVED错误,同时带上正确节点的地址,客户端收到后更新本地路由缓存并重新定向。

Cluster 的数据分片有两个特点。一个是使用{hash tag}可以让多个 key 落在同一槽位,比如{user:100}.cart和{user:100}.orders,这样它们就可以被放进同一个 Lua 脚本或者事务里操作,跨槽事务会直接报错,这是 Cluster 和单机使用习惯最大的区别。另一个是槽位重分配,扩容或缩容时用redis-cli --cluster reshard命令在线迁移槽位,迁移过程中 key 会被标为临时迁移状态,客户端收到ASK重定向后需要重新请求目标节点,这个细节必须由客户端库处理,使用集群版客户端连接时一般都已经封装好了。

Cluster 模式下每个分片依然可以配置从节点。某个主节点挂掉后,它的从节点会被选为主节点接替槽位,这就是集群的故障转移;如果主节点和它所有从节点都挂了,这个分片的槽位就不可用,整个集群在默认情况下也拒绝请求,所以生产环境每个分片至少保证一个从节点,并且考虑把主从分散到不同物理机。

主从、哨兵、Cluster 三者怎么选,很多团队容易搞混。我的判断是:数据量和 QPS 都不高,先上主从并搭配哨兵,这是性价比最高的高可用方案;单机内存已经吃紧,数据量超过单台机器承载范围,才考虑 Cluster。不要为了“集群听起来高级”就用 Cluster,它会带来跨 key 操作限制、客户端复杂度上升、运维难度增大等额外成本。

5. 拓展篇:Redis 不止缓存,还能干这些事

5.1 Stream、Lua、Pipeline,把 Redis 用出花

Redis 是一个通用存储层,很多能力平时被缓存场景掩盖了。Stream 是 5.0 引入的消费模型,支持消费者组、消息确认、消息回溯,基本能覆盖轻量级消息队列的诉求。相比部署一套独立的 MQ,如果团队规模不大、性能要求中等,用 Stream 可以省掉一个中间件;但它没有堆积淘汰、多级存储这类功能,服务端就是内存,积压太多会占用大量内存。

Lua 脚本是 Redis 实现原子多步操作的利器。比如扣库存需要三步:判断库存是否足够、扣减库存、记录流水,如果分三次发送,中间一步挂了状态就不一致。放进一个 Lua 脚本里执行,Redis 保证脚本执行期间其他命令不会插入。我见过不少团队买了昂贵的中间件来解决库存扣减问题,其实一条 Lua 脚本加一个 key 就搞定了,关键就是理解 Redis 单线程执行脚本这个性质。

Pipeline 解决的是多命令网络开销问题。平时每条命令都是一次网络往返,100 条命令就是 100 次 RTT。Pipeline 把多条命令打包成一次发送,一次性拿回所有响应。实际测试中,大量小命令的前提下吞吐量可以提升一个数量级。要注意 Pipeline 不等于事务,它只是把请求合并发送,执行过程中其他客户端的命令也可能穿插进来;真正需要事务语义还得用 MULTI/EXEC 或者 Lua。

日常开发里还能用 Bitmaps 做签到统计和在线状态,用 HyperLogLog 做 UV 统计,用 Geo 做附近门店查询。这些功能本质上都是 Redis 对特定数据结构的高效组织,把它当成一个多功能工具箱而不是单纯缓存,很多业务逻辑就会被简化,代价是外部系统数量减少。

5.2 监控与性能定位:bigkey、慢查询一个都不能少

Redis 上线后,运维成本最集中的三件事是:内存异常增长、命令延迟突刺、偶发连接超时。排查这些问题的入口是三个命令:INFO、SLOWLOG、CLIENT LIST。

INFO memory能看内存使用量跟碎片率。MEMORY DOCTOR会给内存健康建议,MEMORY USAGE key能估算单个 key 的占用。bigkey 排查常规做法是redis-cli --bigkeys,它会分段扫描并统计各类数据中最大的 key,这个命令对线上风险较低,但还是建议在低峰执行。慢查询用SLOWLOG GET查看,能直接定位哪些命令执行时间超阈值;阈值上限slowlog-log-slower-than默认是 10000 微秒,如果业务对延迟敏感,建议调到 1000 微秒。

连接异常跟CLIENT LIST里的标记有关。如果出现大量客户端处于 blocked 状态,要查是否有长时间阻塞命令;连接数过高时,先看maxclients是否够用,再看是否有客户端没关连接的泄漏问题。设置maxmemory和淘汰策略maxmemory-policy是最重要的兜底手段,一般缓存场景用allkeys-lru,带持久化的业务用noeviction更安全,让 OOM 暴露问题而不是悄悄淘汰数据。

延迟突变最容易被忽略的因素是持久化。RDB 的BGSAVE会 fork 子进程,fork 瞬间如果内存很大,会有一段时间的停顿;AOF 的 fsync everysec 一般情况下没问题,磁盘延迟上升也会直接影响写命令。线上排查时可以用INFO stats里的 expired_keys、keyspace 命中率来辅助判断。

6. 源码篇:读 Redis 源码,推荐这条路线

6.1 从入口到命令分发,三步建立起全局观

新手读源码最容易迷路,因为 Redis 代码量不小。我建议三条主线:命令是怎么从网络进来、数据是怎么存进内存、持久化是怎么把数据落盘的。

第一条线从src/server.c的 main 函数开始,初始化完配置、网络监听、事件循环之后,请求到达就会激发读事件,输入缓冲里的命令被解析后,通过命令查找表 dispatch 到具体的处理函数。比如SET对应t_string.c里的setCommand。这种方式下看源码,只要抓准命令分发入口,基本不会迷路。

第二条线看对象系统,object.c和t_*.c文件。创建一个 key 时,要先创建 redisObject,通过 type 和 encoding 决定底层数据结构,再调用底层实现的 API。看完这条线,你就会理解为什么同一个命令在不同编码下实现完全不同,也会理解OBJECT ENCODING背后到底查的是什么。

第三条线看rdb.c和aof.c。RDB 的序列化、反序列化,AOF 的追加、重写、加载,逻辑都在这里面。配合着BGSAVE的实现可以看懂 fork 子进程与写时复制的配合。这也是生产排查里理解“为什么大内存实例 BGSAVE 会导致延迟”最直接的突破口。

6.2 源码里值得抄的数据结构与工程技巧

读源码千万别只是看热闹,Redis 的代码里有很多可以直接抄进自己项目的设计。

第一个是命令注册表。所有命令集中在commands.c里,每条命令都声明了名字、参数、回调函数、可用的 flag。这个设计把命令元信息和实现解耦,加新命令只需要改注册表,非常干净。我在自己负责的网关项目里也借鉴了这个模式,注册新接口时不用到处加 if-else。

第二个是内存估算和编码切换的时机。Redis 每个类型都有list-max-listpack-size、hash-max-listpack-entries这类配置,平衡内存和性能。它们背后其实是工程里的经典思路:小数据用紧凑格式,大数据用通用格式,临界点用配置暴露出来。理解和调优这些参数,比对别人现成配置照抄要有效得多。

第三个值得关注的是惰性删除和内存淘汰策略。Redis 的过期 key 不是靠定时器一个个扫出来的,而是依赖过期键惰性访问、定期抽样和淘汰机制的组合。源码里expire.c的activeExpireCycle控制抽样频率,evict.c负责内存淘汰。这套“懒 + 定期 + 淘汰兜底”的思路,对解决各种资源回收问题都有启发。

如果有精力,networking.c里事件循环、读写缓冲区的处理也值得精读。它的代码风格严谨,变量命名、错误处理都很规范,是学习 C 工程实践很好的教材。不过这一段依赖前面的事件模型基础,建议放在熟悉源码结构之后再看。

7. 实战问题排查实录:踩过的坑帮你提前排雷

7.1 高频问题速查表

我把过去几年线上遇到的高频 Redis 问题整理成一个速查表,方便对照。

现象可能原因排查命令/手段解决方向
本地能连,远程连不上bind、protected-mode检查 redis.conf,用 netstat 看端口监听bind 内网地址,配密码再开放端口
内存突然暴涨bigkey、内存碎片redis-cli --bigkeys、MEMORY DOCTOR拆分大 key,考虑 lazyfree 异步删除
命令延迟突刺AOF fsync、RDB fork、慢命令SLOWLOG GET、INFO stats调低 slowlog 阈值,控制大 key,避免频繁 BGSAVE
主从数据不一致异步复制延迟INFO replication看 lag关键数据不要读从节点,或者改读写逻辑
写入报 MISCONF无法 RDB 持久化看日志中的磁盘不足增大磁盘,或临时关闭持久化,尽快定位根因
连接数打满客户端泄漏CLIENT LIST、INFO clients检查连接池是否合理复用,排查未关闭的连接
分布式锁失效锁过期时间太短看业务执行耗时用看门狗续期,或按最大耗时设过期时间

表里的问题我基本都亲手遇到过。最典型的是远程连不上的问题,查了半天网络,最后发现是 protected-mode 在作祟,这种基础配置一定要在最开始就确认好,避免浪费半天时间。

7.2 我的十条避坑清单

最后分享几条最容易复制的避坑经验,都是普通文档里不会写的。

第一,不要在 redis.conf 里用默认密码方式。生产环境用官方 ACL 功能,给不同业务配不同账号权限,能避免一个密码泄露导致整个 Redis 全被扫。第二,不要随便用FLUSHALL或者FLUSHDB,如果真要清空,先确认是否做了备份,再考虑方向。线上我见过有人一条命令清掉整个测试环境数据,从此养成了习惯:危险命令一律加别名确认。

第三,所有涉及过期时间的配置,都要考虑业务高峰的余量。像验证码、优惠券这种业务,过期时间设成刚好等于业务逻辑时间,一旦网络抖动或者任务积压,就会产生诡异问题。第四,写缓存之前评估 key 数量。如果 key 数量动辄千万级,先规划好命名规范和定期淘汰策略,否则 Redis 迟早被没用的 key 塞满。

第五,连接池不要开得过大。很多人以为连接池越大越好,实际上 Redis 是单线程处理命令,连接过多会让事件循环被各种 I/O 打断,吞吐量和延迟反而更差。常规服务 200 到 500 个连接已经足够,压测是验证连接池大小的唯一标准。

第六,热点 key 要主动治理。比如爆款商品、热门帖子,可以把同一条数据复制多份,用随机后缀分散到多个 key 和节点上,避免单 key 打爆单节点。第七,批量操作时记得用 MGET 和 pipeline,一份时间能省下九十份的网络开销。

第八,Redis 里默认没有主键约束,业务侧的幂等设计不能省。重复消费、重复扣款这类问题,一定要靠业务建唯一标识和 Lua 脚本兜底。第九,升级版本要谨慎,关键小版本之间行为可能有变化,尤其是持久化格式、复制协议、内存编码这几种,建议先在测试环境把版本差异测透。

第十,也是最重要的一条:把 Redis 当成一个会“丢失数据”的组件来设计。Redis 即使开了 AOF,依然存在进程崩溃、磁盘问题、误操作等风险。重要的业务数据不能只放在 Redis 里,要留一份在数据库,或者设计好重建缓存的链路。抱有这种预期之后,你在做架构决策时就会更稳,不会因为 Redis 挂了导致整个系统一起崩。

我带团队时经常要求新人把这条学习路径完整走一遍,基础、原理、集群、源码,每多补一层,线上出问题时你的底气就多一分。许多人一开始只看命令怎么用,遇到故障就慌,根源就是缺了原理和集群这两层。选择哪些资料不重要,重要的是你有没有把这套完整的知识框架搭起来。Redis 的全栈之路没有捷径,但按这个顺序走,确实能少踩很多我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询