先把话说在前面:如果你只是会在 Spring Boot 里写一个 RedisTemplate,set 一个字符串再 get 出来,那 Redis 对你来说还是单机玩具。真正让我意识到必须系统学一遍 Redis 高级内容,是第一次把服务部署到多台机器之后——session 不共享了、热点数据扛不住了、Redis 进程一重启数据全没了,每一个问题都够我忙半天的。黑马这套视频的高级篇,正好就是把生产环境里这些硬骨头一块一块啃下来。这篇笔记是我把整套高级篇内容拆开、揉碎,又补了一堆实测之后的整理版,主线包括持久化选型、主从复制、哨兵、分片集群、缓存穿透/击穿/雪崩治理、分布式锁,以及每个模块落地时最容易踩的坑。适合已经会用 Redis 基础命令、正准备把它真正用进项目的开发者。
1. 从单机到生产:高级篇到底在解决什么问题
1.1 基础篇和高级篇之间缺了什么
基础篇教你的是:五种数据类型怎么用,key 怎么设过期,Java 客户端怎么调,这些内容能让你把 Redis 当做一个"好用的 Map"来使。可一旦面对生产环境,问题就变了。第一个问题是数据安全:进程退出,内存数据是不是直接蒸发?第二个问题是高可用:一台 Redis 挂了,系统怎么做到不感知?第三个问题是容量:单机内存几十个 G 到头了,数据量继续涨怎么办?第四个问题是并发下的正确性:多个服务同时扣库存,怎么保证不超卖?
这些问题不是靠多记几个命令就能解决的,它们需要的是架构层面的认知。黑马这个高级篇的精髓,就是没有停留在"这个命令会返回什么"的层面,而是把 Redis 放在一个真实的生产拓扑里去讲。
1.2 四个生产问题对应的高级特性
我用一张表把这四场硬仗和对应的解决方案理出来,后面每一节都会展开:
| 生产问题 | 对应高级特性 | 核心知识点 |
|---|---|---|
| 进程退出丢数据 | 持久化 | RDB 快照、AOF 日志、混合持久化 |
| 单点故障 | 主从复制 + 哨兵 | 全量同步、增量同步、自动故障转移 |
| 单机容量瓶颈 | 分片集群 | 哈希槽、节点伸缩、Gossip 协议 |
| 并发竞争与一致性 | 分布式锁 + 缓存治理 | SETNX、Redisson、缓存三大问题 |
这里我想强调一个学习顺序的问题。持久化应该最先看,因为主从复制的全量同步依赖 RDB,哨兵的故障转移依赖主从关系,链路是一层叠一层的。缓存治理和分布式锁相对独立,你可以在搞懂持久化和高可用之后再单独啃,不用怕跳着看会脱节。
1.3 这套高级篇内容里最容易被忽略的部分
很多人看完这套视频,记住的是 Cluster 怎么搭、Redisson 怎么用,觉得这些"大招"才是重点。但以我自己的实战体验,真正在项目里回报率最高的是持久化配置和缓存穿透/击穿/雪崩治理。原因很简单:集群你未必用得上,但 Redis 数据持久化几乎没有项目不用;缓存三大问题则是只要上缓存就一定会遇到。所以这篇笔记里,我也会把笔墨重点放在这两块。
2. RDB 与 AOF 持久化:数据安全的第一层防线
2.1 RDB 快照不是"实时备份"
RDB 是 Redis 默认开启的持久化方式,它会定期把内存里的数据生成一份二进制快照文件,默认叫dump.rdb。触发方式有几种:手动执行save或bgsave,满足配置的 save 规则,执行flushall或者正常关闭 Redis 时也会触发。
需要特别注意save和bgsave的区别。save是同步操作,直接在主线程里干,会阻塞 Redis,生产环境基本没人用它。bgsave才是正路:主进程fork出一个子进程,子进程负责把数据写入临时文件,写完后原子替换原来的 RDB 文件。
fork之后为什么子进程能拿到一致的数据快照?这里的关键是操作系统的Copy-On-Write(COW)机制。你可以把它理解成"拍照":fork 一瞬间,子进程复制的是父进程的页表,不是所有内存数据,所以非常快。之后父进程继续处理写请求,一旦某个内存页要被修改,操作系统会把这个页复制一份给父进程用,子进程始终看到的是 fork 时刻的内存状态。这套机制保证了 bgsave 不会影响正在服务的主进程。
但 COW 也带来一个隐形坑:如果 fork 之后父进程写操作特别多,需要复制的内存页就多,内存开销会翻倍。实操中如果 Redis 实例占用了 20GB 内存,bgsave 期间可能额外需要几 GB 甚至十几 GB 的物理内存,所以大实例的 fork 非常容易引起主线程卡顿。这也解释了一个经验法则:单机 Redis 内存最好不要超过 20~30GB,太大了不如拆实例或者上集群。
2.2 AOF 日志:每条写命令都留痕
AOF(Append Only File)的思路和 RDB 完全不同:每一条写命令都以 Redis 协议格式追加到日志文件里,重启时把日志里的命令重新执行一遍,就能恢复数据。它的核心配置是appendfsync,对应三种刷盘策略:
| 策略 | 行为 | 安全性 | 性能 |
|---|---|---|---|
| always | 每个写命令都同步刷盘 | 最多丢一条命令 | 最差 |
| everysec | 每秒刷一次盘 | 最多丢 1 秒数据 | 折中,默认推荐 |
| no | 由操作系统决定刷盘时机 | 可能丢几十秒数据 | 最好 |
默认的everysec是最常用选择,因为 Redis 本身是内存数据库,主打高性能,always的性能损耗在写密集场景下很难接受。如果你对数据安全要求极高,比如想极端一点,可以临时改成 always,但代价是吞吐量明显下降。
AOF 还有一个绕不开的操作:重写。随着运行时间变长,AOF 文件会越来越大,里面有大量中间状态命令。比如对一个 key 做了一百次 INCR,文件里可能存了一百条 INCR 命令,但实际只需要 SET 一次最终值就够了。bgrewriteaof会对 AOF 文件进行压缩,原理是按当前内存状态生成最小命令集合,同样也是 fork 子进程来做。
Redis 4.0 之后引入了混合持久化,aof-use-rdb-preamble yes开启后,重写出来的 AOF 文件头部是 RDB 格式的快照,后面再追加增量命令。这样既有 RDB 恢复速度快的优点,又能尽量少丢数据,是目前生产环境的主流方案。
2.3 真实项目中的持久化配置
这里直接给一份我在项目里验证过的基础配置:
appendonly yes appendfilename "appendonly.aof" appendfsync everysec no-appendfsync-on-rewrite yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb aof-use-rdb-preamble yes几个配置怎么理解?
no-appendfsync-on-rewrite yes表示在 AOF 重写期间不刷盘。因为 AOF 重写会带来很高的磁盘 IO,如果此时还要保证每秒刷盘,两者叠加容易造成 IO 阻塞。开了这个选项,重写期间最多会多丢一些数据,换取主线程的稳定性。auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb是自动触发重写的条件:AOF 文件比上次重写时增长了 100%,且文件超过 64MB。
另一个常见坑是启动恢复优先级。如果 AOF 和 RDB 同时存在,Redis 优先用 AOF 恢复,因为 AOF 丢数据更少。但如果你不小心把 AOF 文件搞坏了,会导致 Redis 启动失败。修复办法是使用redis-check-aof --fix appendonly.aof工具,它能帮你删掉受损的尾部命令让实例先启动起来。实测中我用这个工具救过一次数据,值得记一下。
3. 主从复制与哨兵:把单点故障摁死
3.1 主从复制的两种同步模式
主从复制解决的问题很直接:Redis 是单点的,它挂了整个服务就断了,所以要有一份实时的备份。配置方式是在从节点执行replicaof 主节点IP 主节点端口,建立关系后主节点会持续把写命令同步给从节点,从节点默认只服务读请求(replica-read-only yes)。
主从之间第一次建立复制关系时,走的是全量同步。流程我用自己的话描述一遍:从节点发送psync ? -1表示我没有历史数据,主节点收到后执行 bgsave 生成 RDB 快照,同时把新收到的写命令写入复制缓冲区,快照生成完后发给从节点,从节点清空旧数据并加载 RDB,最后主节点把缓冲区里的增量命令再发给从节点。这一套下来,主从数据就对齐了。
全量同步之后,就是增量同步。主节点会维护一个环形缓冲区repl_backlog_buffer,写命令会同时写入这个缓冲区。从节点会记录自己已经同步到的偏移量,之后每次只需要发送psync <runid> <offset>,主节点把缓冲区里偏移量之后的命令发给从节点即可。
环形缓冲区的坑在于它的容量是有限的,默认大小是 1MB。如果从节点断连时间过长,或者主节点写入量太大,backlog 里的数据被新命令覆盖了,从节点就只能退回全量同步。生产环境建议把这个参数调大,比如 64MB 甚至 256MB,具体看你的写入量。
3.2 用 Docker 搭建一套最小主从
我用 Docker 搭过一套三个节点的主从,遇到最多的坑是容器 IP 写死。容器一旦重建,IP 就变了,replicaof配置就失效。正确做法是创建自定义网络,用容器名互相访问:
docker network create redis-net docker run -d --name redis-master --network redis-net -p 6379:6379 \ redis redis-server --appendonly yes docker run -d --name redis-slave1 --network redis-net -p 6380:6379 \ redis redis-server --replicaof redis-master 6379 docker run -d --name redis-slave2 --network redis-net -p 6381:6379 \ redis redis-server --replicaof redis-master 6379启动之后,在任意节点上用info replication查看复制状态。如果看到master_link_status:up,说明复制链路正常。
我再多提一句:主从复制的写命令传播是异步的,主节点不会等从节点确认成功就返回给客户端。这意味着主节点刚写完还没来得及同步就宕机,从节点顶上时会丢一部分数据。这是 Redis 主从的固有特性,不要指望它做到“零丢失”,如果你对这个要求极其严格,需要考虑强一致方案,但通常业务场景 1 秒内的数据丢失是可以接受的。
3.3 哨兵:自动化的故障转移
主从架构还有一个问题:主节点挂了,从节点不会自动升级。你需要哨兵(Sentinel)来做监控和故障转移。
哨兵的核心逻辑是:每个 Sentinel 进程定时向 Redis 节点发送心跳,如果一个 Sentinel 发现主节点不可用,会把它标记为主观下线(sdown);多个 Sentinel 都发现主节点不可用,达到 quorum 数量后,就升级为客观下线(odown);接着 Sentinel 集群会选出一个 leader,由它执行故障转移:从从节点里选一个提升为主节点,并通知其他从节点切换复制目标。
这里有个特别容易理解的“为什么”问题:Sentinel 至少需要部署几个?答案是奇数且大于等于 3。因为客观下线的判定需要"投票",两个 Sentinel 在其中一个宕机时只剩一个,无法构成多数派,整个系统就失效了。我用一句话总结 Sentinel 对数量敏感的原因:故障转移本身也需要防脑裂。
一份常见的 Sentinel 配置如下:
sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000monitor后面2表示至少 2 个 Sentinel 同意主节点不可用,才会开始故障转移。故障转移完成后,主节点地址变了,客户端如果还连着旧地址就废了,所以在项目里我们通常还会配合配置中心或者网关做一层动态感知。
3.4 主从架构最容易踩到的脑裂与丢数据问题
即使是主从 + 哨兵架构,依然存在一个经典问题:脑裂。场景是主节点发生了网络分区,哨兵联系不上它,判定它挂了,然后提升了从节点为新的主节点。但旧主节点并没有真的宕机,它还在继续接收客户端的写请求。等网络恢复后,旧主节点被降级为从节点,它之前接收的那些写请求因为没能同步给新主节点,直接被清空,数据就丢了。
Redis 提供了一套参数从根上减少这种风险:
min-replicas-to-write 1 min-replicas-max-lag 3这两行的意思是:主节点至少要有 1 个从节点在 3 秒内有正常心跳同步,才接受写请求;如果所有从节点都失联了,主节点直接拒绝写入。这样脑裂发生时,旧主节点的写入口会被切断,数据丢失范围大幅缩小。这个参数按理说应该在每套主从环境都配,但很多教程都只提半句,这里单独拎出来强调一下。
4. 扩容到分片集群:当单机内存彻底不够用
4.1 哈希槽才是集群的数据分片方式
主从复制和哨兵解决的是高可用,却没有解决容量问题。无论多少从节点,它们存的都是同一份完整数据,总内存仍然受单机限制。数据量大到几十个 G、上百个 G 之后,就得考虑分片集群。
Redis Cluster 的做法是把整个 key 空间划分为 16384 个哈希槽,每个 key 通过CRC16(key) % 16384计算它属于哪个槽,然后集群把这些槽平均分配给各个主节点。你访问任何一台节点,如果 key 的槽不在当前节点上,节点会返回MOVED错误并告诉你正确节点的地址;支持 Cluster 协议的客户端会自动完成跳转,所以客户端需要专门支持集群模式。
为什么是 16384 而不是更多?这个和集群节点心跳消息的大小有关:每个节点在 Gossip 通信时都会带上自己的槽位信息,16384 个槽位用 Bitmap 表示只要 2KB,即使几百个节点,心跳包也能控制在合理范围内。如果槽位太多,心跳开销会成为问题;太少则数据分布不够均匀。这个数字是性能和均匀度之间的平衡点。
另外,Cluster 模式支持多 key 操作,但要求这些 key 落在同一个槽里。官方提供的机制叫Hash Tag:key 中用{}包住同一部分,Redis 计算槽位时只对花括号内的内容做 CRC16。例如user:{1001}:name和user:{1001}:score,因为{1001}相同,会被分配到同一个槽,事务、Lua 脚本和多 key 命令就能生效了。
4.2 三主三从集群的搭建实操
我用 Docker 搭建一套三主三从集群的过程,可以直接复现:
docker network create redis-cluster-net for port in 6379 6380 6381 6382 6383 6384; do docker run -d --name redis-cluster-$port \ -p $port:6379 \ --network redis-cluster-net \ redis redis-server --port 6379 --cluster-enabled yes \ --cluster-config-file nodes.conf --appendonly yes done启动之后需要把节点互相发现并分配槽位。用官方提供的redis-cli --cluster create命令一步完成:
redis-cli --cluster create \ 172.20.0.2:6379 172.20.0.3:6379 172.20.0.4:6379 \ 172.20.0.5:6379 172.20.0.6:6379 172.20.0.7:6379 \ --cluster-replicas 1这个命令会提示你输入 yes 确认槽位分配方案。--cluster-replicas 1表示每个主节点带一个从节点,最终槽位被平均分配到三个主节点。内存里记住两个校验命令:cluster info查看槽位分配和集群状态,cluster nodes查看节点间的关系。
搭好之后,用可视化工具连接会比较直观。RedisInsight 和 Another Redis Desktop Manager 都支持 Cluster 模式,填一个节点的地址就能自动识别整个集群拓扑,查看每个节点负责的槽位区间,比命令行一个个敲舒服很多。
4.3 集群扩容与槽位迁移
集群不是建完就完事的,数据量增长后要加节点。添加一台新主节点的流程是:先redis-cli --cluster add-node 新节点地址 任意老节点地址,把节点加入集群;然后再执行redis-cli --cluster reshard 新节点地址,指定要从哪些老节点迁移多少个槽位过来。
槽位迁移的过程其实就是在搬数据。Redis 会把槽位对应的 key 逐个从源节点迁移到目标节点,迁移期间,key 可能在源节点也可能在目标节点,客户端会通过ASK重定向找到正确的位置。这个过程中集群对外是正常服务的,但你如果在一个大 key 上执行并发读写,可能会感受到轻微延迟,生产环境扩容一般选在低峰期做。
我的建议是:除非单机内存确实见底了,或者 Redis 的 CPU 单线程瓶颈实在顶不住,否则不要轻易上 Cluster。原因在于它给业务代码带了很多限制:跨槽位事务、跨槽位 Lua、批量操作都变得麻烦,排查问题时也要同时面对多个节点。能用主从 + 哨兵解决就不上集群,这是我在项目里牢牢记住的选型原则。
4.4 集群模式下的使用限制与客户端注意点
集群模式下最容易踩的坑就是"原来能用的命令现在报错":
- 事务
MULTI/EXEC里如果包含多个不同槽位的 key,会直接报CROSSSLOT错误。 - Lua 脚本同理,脚本里操作的所有 key 必须在同一个槽位。
- 没有做 Hash Tag 的多 key 操作,例如
MSET user:1 name user:2 name,也会报错。
这些问题的最佳解法不是去改集群配置,而是在设计 key 的时候就给相关联的数据加上 Hash Tag。比如把同一个用户的属性集中存到一个 hash 里,构造user:1001这样的 key,天然就在同一个槽。
客户端的选型也得注意:普通的 Jedis 不支持集群路由逻辑,要用 JedisCluster;Spring Boot 默认用的 Lettuce 自带集群支持,配置一下节点列表即可。我在生产环境用 Lettuce 比较多,底层连接共享和自动重定向都做得比较成熟,但如果你对底层连接池有强需求,JedisCluster 也不差,关键是把连接超时、重试次数这些参数在压测时调好。
5. 缓存穿透、击穿与雪崩:三个必须处理的坑
5.1 缓存穿透:查了一个一定不存在的数据
缓存穿透的本质是:请求的数据在数据库里压根不存在,所以缓存永远不可能命中,每个请求都会直接打到数据库。攻击者可以故意构造大量不存在的 ID 来打垮数据库。
最有效的两种方案,我按使用成本排序:
第一种是缓存空值。查询 DB 返回为 null 时,也往缓存里写一个 null 占位,并设置一个较短的过期时间,比如 60 秒。实现简单,但缺点是有大量不存在的 key 时会占缓存空间,而且攻击者换着 ID 打,每个 ID 都能产生一条空缓存。所以我通常会给空值 key 设置 2~3 分钟的 TTL,并配合布隆过滤器兜底。
第二种是布隆过滤器。核心原理:申请一个位数组,写入数据时用多个哈希函数把 key 映射到位数组的多个位置并置为 1;查询时同样计算哈希,如果发现任意一个位是 0,则可以确定 key 一定不存在;如果所有位都是 1,则可能存在(因为哈希冲突会产生误判)。在缓存查询之前先经过布隆过滤器,能过滤掉绝大多数不存在的 key。
我自己的落地策略是:空值缓存 + 布隆过滤器一起用。布隆过滤器挡掉绝大多数恶意穿透,空值缓存兜住过滤器误判后打到 DB 的真实空结果。需要注意的是布隆过滤器不适合频繁删除的场景,因为删除一个 key 需要把对应的位清 0,可能导致其他 key 误判,所以如果有大量删除操作,缓存空值方案会更合适。
5.2 缓存击穿:热点 key 过期的一瞬间
缓存击穿和穿透名字很像,但场景完全不同:它特指一个热点 key 在过期的瞬间,大量并发请求同时发现缓存没命中,全部涌向数据库。因为只持续很短时间,很多系统都被大流量瞬间打崩过。
两种主流的解决思路:互斥锁和逻辑过期。
互斥锁的思路是:当缓存 miss 时,先尝试获取一个分布式锁,只有拿到锁的请求才允许去查数据库,其他请求先等待一段时间再重新查缓存。实现不难,但有一个隐含问题:大量线程会阻塞等待,影响接口响应时间。
逻辑过期是黑马这套视频里讲得比较多、我也觉得更优雅的方式:不给 key 设置真正的 TTL,而是在 value 里额外存一个过期时间字段。当请求发现 value 里的时间已经过期时,不会立刻删除 key,而是先返回旧数据,同时异步去数据库更新缓存。这种方案的好处是用户体验更好,不会因为缓存 miss 导致响应变慢;坏处是数据一致性弱一点,因为过期后的第一个请求拿到的是旧数据。
我的实际选择是:对一致性要求高、并发也不低的场景用互斥锁;对一致性容忍度较高的场景用逻辑过期。两个方案都不是银弹,要结合业务判断。
5.3 缓存雪崩:成片 key 同时过期,或 Redis 直接宕机
雪崩的触发条件比击穿更大片:大量 key 的过期时间都设置在同一个时间点,或者 Redis 实例直接挂掉,导致所有缓存请求全部落到数据库。
解决方案分两个层面:
第一个层面是避免过期时间同质化。做法是在设置缓存 TTL 时加入随机值,比如原来是 1 小时,现在按1小时 + 随机0~600秒分布,让过期时间自然错开。代码上就是setTimeout(3600 + random.nextInt(600)),成本极低,效果显著。
第二个层面是降低 Redis 宕机时的冲击。这里就是前面说的主从 + 哨兵 + 集群能真正发挥作用的地方:Redis 高可用让宕机时间变短,但宕机期间数据库还是会接收大量流量。所以还要配合限流降级:当缓存不可用时,只允许部分请求穿透到数据库,其他请求直接返回默认值或错误提示,防止数据库被打死。
顺带说一句:多级缓存也是缓解雪崩的有效手段。本地 JVM 缓存(比如 Caffeine)做第一层,Redis 做第二层,数据库做第三层。Redis 挂了之后,本地缓存还能扛住一部分流量,给故障恢复争取时间。这也是为什么很多资深架构师会强调"不要把所有鸡蛋放在 Redis 一个篮子里"。
5.4 缓存更新策略与一致性
真正上了生产,你会发现比"穿透/击穿/雪崩"更磨人的是缓存和数据库的一致性问题。最常见的做法是 Cache Aside Pattern:读的时候先读缓存,没命中就读库并回填;写的时候先更新数据库,再删除缓存。
为什么是"删除缓存"而不是"更新缓存"?因为更新缓存的代价更高,而且并发下更容易出问题。删掉缓存让下一次读取时重新加载,是一种懒惰但安全的做法。
但先更新 DB 再删缓存依然有个经典并发漏洞:线程 A 更新 DB 为值 2,删除了缓存;线程 B 在 A 删除之前的瞬间读到了旧缓存值 1,回填了缓存。要彻底解决,业界常用的一招是延迟双删:先删除缓存、更新 DB、等几百毫秒再删一次缓存。把"晚一步回填旧值"的窗口尽量压小,虽然不能数学上绝对消除,但在绝大多数业务场景已经够用了。
如果你对一致性要求极高,还可以用订阅数据库 binlog 的方式异步删缓存,比如通过 Canal 监听 MySQL 的 binlog 变更,然后通知 Redis 删除对应 key。这套方案侵入业务代码少,但需要额外部署 Canal 组件,复杂度上升一个量级。
5.5 RedisTemplate 序列化:一个绕不开的细节
和 Spring Boot 整合时最容易踩的深坑就是序列化。RedisTemplate默认使用 JDK 序列化,存进去的 key 会带着一长串\x00前缀,value 是二进制乱码,用 RedisInsight 或者 Another Redis Desktop Manager 查看时完全没法读,排查数据问题困难十倍。
我的做法是自定义一个RedisTemplate,key 使用StringRedisSerializer,value 使用GenericJackson2JsonRedisSerializer。改成 JSON 序列化之后,至少在可视化工具里能直接看到内容,调试体验好很多。但要注意一个副作用:JSON 序列化的 value 会保存对象的类型信息,如果对象结构发生了变化,反序列化可能直接报错,这属于缓存数据序列化方案固有的兼容性问题。在架构评审阶段就应该定下来,不要等到线上跑出乱码再改。
6. 分布式锁:从 setnx 到 Redisson
6.1 为什么 synchronized 在多实例下失效
单机应用里,synchronized或者ReentrantLock能解决多线程竞争问题,因为它们锁的是 JVM 进程内的对象。一旦应用部署成多实例,请求会通过负载均衡随机落到不同的 JVM 进程上,每个进程的锁互相独立,库存超卖、重复下单这类问题就会冒出来。这时候需要一把"全局唯一"的锁,Redis 天然适合这个角色。
6.2 第一版:SET NX EX 加 Lua 释放
Redis 实现分布式锁最基础的一条命令是:
SET lock_key unique_value NX EX 30NX表示只有 key 不存在时才能设置成功,EX 30表示锁的自动过期时间是 30 秒。拿到锁的业务逻辑执行完后,需要释放锁。释放有个很关键的细节:只能释放自己持有的锁,所以 value 通常用唯一标识(比如 UUID),释放前先比较 value,一致才删除,比较和删除必须是一个原子操作,用 Lua 脚本实现:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end这个版本能跑通基本流程,但存在几个隐患:
- 锁过期时间设置多少才合适?业务执行超过 30 秒,锁就自动释放了,其他线程会趁虚而入。
- 锁不是可重入的。同一个线程再次获取锁会失败,除非你自己用 hash 结构记录加锁次数。
- Redis 主节点宕机时会丢锁。锁写入主节点还没同步到从节点,主节点挂了,从节点顶上后锁就没了。
6.3 Redisson 解决了什么
实际项目中我基本不手写上面的逻辑,直接用 Redisson。Redisson 的分布式锁本质是用 Lua 脚本 + Hash 结构实现了可重入锁:hash 的 field 存线程标识,value 存加锁次数,同一个线程可以多次加锁,每次释放减计数,计数归零才真正删除锁。
最值得说的是它的Watchdog 看门狗机制。Redisson 加锁成功后会启动一个定时任务,默认锁的有效期是 30 秒,每 10 秒会检查一次,只要锁还持有中,就自动续期到 30 秒。这样"业务执行时间超过锁过期时间"的问题就消失了,你不需要自己拍脑袋设置一个"估计够用"的过期时间。
用法非常简单:
RLock lock = redissonClient.getLock("order:" + orderId); try { if (lock.tryLock(3, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }6.4 RedLock 和主从故障时的锁安全问题
Redisson 也不是万能的。前面提到主从切换时锁可能丢失:客户端 A 在主节点拿到了锁,主节点还没来得及同步给从节点就宕机了,哨兵把从节点提升为主节点,此时客户端 B 也去加锁,它也能拿到同一把锁。两个客户端同时持锁,分布式锁的核心语义就被破坏了。
Redis 官方提出过 RedLock 方案:同时向多个独立的 Redis 节点申请锁,超过一半节点加锁成功才算获得锁。但这套算法在不少分布式系统专家的论文里被论证过仍然有时间窗口问题,而且实现复杂、性能开销大,生产实践中争议很多。
我的看法是:如果你需要的是"绝对正确"的锁,应该考虑 ZooKeeper 或 etcd 这类带强一致协议的组件;如果你的业务能容忍极小概率的"两个客户端同时执行业务",Redisson 已经够用了。绝大多数互联网场景,比如防止重复下单、库存扣减,业务上还要靠数据库唯一约束兜底,分布式锁只是第一道防线。
7. 高级篇学完之后,我在项目里是怎么落地的
如果让我给一个落地顺序清单,我建议按下面的优先级来:
第一优先级:持久化配置。任何新项目接 Redis,先把 AOF 打开,设置everysec,同时保留 RDB。这不是特性,是底线。
第二优先级:缓存治理。项目里只要用了 Redis 做缓存,就把穿透、击穿、雪崩的应对方案设计进架构,尤其是热点数据要提前想好。
第三优先级:主从 + 哨兵。服务正式上云或者部署到多台机器时,主从复制和哨兵高可用是必须项,它决定了 Redis 挂了你的系统能不能继续转。
第四优先级:分布式锁。只有真正遇到多实例并发写同一个资源的时候再上 Redisson,不要一开始就为所有接口都加锁,过度设计比不加锁更糟糕。
第五优先级:分片集群。单机容量、单线程 CPU 顶不住的时候再考虑,越晚越好。
记笔记我也说两句。黑马这套视频很多知识点是连贯的,比如主从全量同步依赖 RDB、哨兵依赖主从、集群故障转移依赖投票机制。我自己的笔记方法是每看完一个模块,就在本地搭一个最小环境,把视频里的命令完整跑一遍,然后把info输出和启动日志里的关键行截下来贴上。图解可以省,但配置参数一定要亲测,因为视频用的 Redis 版本可能和你本机不一样,有些配置已经改名或者废弃了。
这套高级篇让我最受益的地方不是某个具体命令,而是它把 Redis 从"一个数据结构服务器"升级成了"分布式系统基础设施"的视角。你会开始思考数据怎么不丢、服务怎么不挂、流量怎么不冲垮数据库,这些才是生产级工程师每天都在面对的问题。如果你也刚开始啃这块内容,别求快,一个模块一个模块吃透,比囫囵吞枣刷完整个视频有用得多。