集群与高可用
开篇:单点 Redis 挂了怎么办?
你开了一家生意火爆的奶茶店,只有一台收银机。某天收银机坏了——全店停业,排队的顾客全走了。这就是单点故障的代价。
Redis 单节点也面临同样的三大问题:
- 单点故障:节点挂了,整个缓存层瘫痪,所有请求涌向数据库。
- 容量有限:一台机器的内存总有上限,数据量大了装不下。
- 压力集中:所有读写请求都打在一台机器上,性能有天花板。
怎么解决?分三步走:先做持久化保证数据不丢,再做主从复制实现数据冗余和读写分离,最后上哨兵或 Cluster 实现自动故障转移和水平扩展。
这三步对应的是分布式系统中经典的 AKF 扩展模型:X 轴(全量镜像/主从复制)解决单点故障,Y 轴(业务拆分)应对复杂度,Z 轴(数据分片)突破容量上限。这篇文章会从地基到屋顶,把 Redis 高可用的每一层都讲透。
一、持久化:数据不丢的保障
Redis 是内存数据库,断电就没了。持久化就是把内存中的数据写到磁盘上,让 Redis 重启后能恢复数据。Redis 提供了两种持久化机制:RDB 和 AOF。
1.1 RDB:快照
RDB 就像给 Redis 拍一张照片——在某个时间点把内存中的所有数据一次性保存到磁盘上,生成一个 .rdb 文件。下次 Redis 启动时,直接加载这个文件就能恢复到快照时刻的状态。
触发方式有两种:
自动触发:通过配置文件设置保存条件。Redis 会定期检查这些条件,满足任何一条就触发快照。
save 900 1 # 900秒内至少1个key变化,触发快照
save 300 10 # 300秒内至少10个key变化,触发快照
save 60 10000 # 60秒内至少10000个key变化,触发快照这些条件可以通过修改 redis.conf 自定义,也可以通过命令动态设置:
CONFIG SET save "300 10 60 10000"手动触发:
SAVE:在主线程中执行,会阻塞所有请求。生产环境慎用——执行 SAVE 期间 Redis 完全不可用。BGSAVE:fork 一个子进程在后台生成快照,不阻塞主线程。推荐使用,但 fork 操作本身会消耗一定的内存和 CPU(尤其是数据量大的时候,fork 的瞬间内存会翻倍)。
优点:快照文件小(压缩的二进制格式)、恢复速度快,非常适合做备份和灾难恢复。你可以每小时把 RDB 文件备份到远程存储,出问题了随时可以恢复。
缺点:两次快照之间的数据可能丢失。比如配置了 save 300 10,上一次快照是 4 分钟前,现在 Redis 挂了,这 4 分钟的数据就没了。拍照频率越高越安全,但 fork 子进程的性能开销也越大,所以需要在安全性和性能之间做权衡。
1.2 AOF:操作日志
如果说 RDB 是拍照片,AOF 就是写日记——把 Redis 执行的每一条写命令追加到一个日志文件(Append Only File)的末尾。重启时按顺序重放这些命令就能恢复数据。
AOF 有三种写回策略,决定了命令写入磁盘的时机:
| 策略 | 行为 | 可靠性 | 性能 |
|---|---|---|---|
| Always | 每条命令执行完立即同步写磁盘 | 最高,最多丢 1 条命令 | 最低 |
| Everysec | 先写内存缓冲区,每秒批量刷盘 | 较高,最多丢 1 秒数据 | 较好 |
| No | 先写内存缓冲区,由 OS 决定何时刷盘 | 最低,可能丢大量数据 | 最高 |
Everysec 是默认也是最推荐的策略——在可靠性和性能之间取了一个很好的平衡。
Always 虽然最可靠,但每条命令都要同步写磁盘,跟直接用磁盘数据库差不多了,失去了 Redis 作为内存数据库的性能优势。No 策略把命运交给操作系统,啥时候写磁盘你完全不知道,万一没来得及写就宕机了,数据就没了。
优点:数据可靠性高,丢失数据的窗口很小(Everysec 最多丢 1 秒)。
缺点:AOF 文件比 RDB 大得多(记录了所有写命令),恢复速度也更慢(要重放全部命令)。不过 Redis 有 AOF 重写机制,会定期把 AOF 文件中的命令合并压缩(比如同一个 Key 被 SET 了 100 次,重写后只保留最后一次),减小文件体积。
1.3 混合持久化(RDB + AOF)
RDB 恢复快但可能丢数据,AOF 数据全但恢复慢。能不能两个都要?Redis 4.0 说:可以。
开启混合持久化后(aof-use-rdb-preamble yes),AOF 重写时会先把数据以 RDB 格式写入 AOF 文件开头,后续新的写命令再以 AOF 格式追加到文件末尾。
[AOF 文件结构]
┌─────────────────────┐
│ RDB 格式的全量数据 │ ← 恢复快
├─────────────────────┤
│ AOF 格式的增量命令 │ ← 数据全
└─────────────────────┘这样既能快速恢复(加载 RDB 部分),又能保证数据完整(重放 AOF 部分)。这是目前 Redis 持久化的最佳实践。
缺点是:AOF 文件中混了 RDB 格式的内容,可读性变差了;而且混合持久化的 AOF 文件不能用在旧版本的 Redis 中。
RDB 和 AOF 的完整对比:
| 特性 | RDB | AOF |
|---|---|---|
| 数据可靠性 | 可能丢失最后一次快照后的数据 | 取决于写回策略,最少丢 1 条命令 |
| 文件大小 | 小(压缩的二进制) | 大(完整的命令记录) |
| 恢复速度 | 快(直接加载) | 慢(需要重放命令) |
| 写入性能 | 对正常运行影响小(BGSAVE 是子进程) | 每次写操作都需要追加日志 |
| 适合场景 | 备份、灾难恢复 | 数据存档、高可靠场景 |
Redis 能完全保证数据不丢失吗?
不能。即使用 AOF 的 Always 策略,也不能 100% 保证数据不丢失。原因有三:(1)操作系统的 I/O 缓冲区可能延迟实际写入——数据到了内核缓冲区但还没落盘,机器就挂了;(2)磁盘写入本身有物理延迟,尤其是机械硬盘的寻道和旋转时间;(3)硬件故障可能在写入过程中发生。归根结底,Redis 不是为持久化设计的。要持久化,用关系型数据库。
二、主从复制:数据冗余与读写分离
一台机器不够,就多来几台。主从复制就是把主节点(Master)的数据复制到一个或多个从节点(Slave),实现数据冗余和读写分离。
核心规则:主节点负责写,从节点负责读。数据的复制是单向的:主 → 从。从节点默认是只读模式。
全量复制
当从节点第一次连接主节点时(或者数据差异太大无法增量同步时),会触发全量复制。整个流程如下:
全量复制的开销很大——主节点要做 BGSAVE(消耗 CPU 和内存),要传输 RDB 文件(消耗带宽),从节点要清空重建(期间不可用)。所以应该尽量避免频繁的全量复制。
增量复制
如果主从之间因为网络闪断而短暂断开连接,重连后不需要重新做全量复制。从 Redis 2.8 开始,支持增量复制。
主节点和从节点各自维护一个复制偏移量(offset)——可以理解为"日记写到了第几页"。主节点把新的写命令保存在一个环形的复制积压缓冲区(repl_backlog)中。断连后重连时,从节点告诉主节点自己的偏移量,主节点只需要发送缺失的那部分命令即可。
如果断连时间太长,缺失的命令已经被缓冲区覆盖了(缓冲区是环形的,空间有限),那就只能回退到全量复制了。所以可以通过调大 repl-backlog-size 来减少全量复制的触发频率。
主从复制的价值与局限
核心价值:
- 数据冗余:数据有多个副本,一台挂了数据不丢。
- 读写分离:写操作走主节点,读操作分散到多个从节点,提升系统整体的并发读能力。增加从节点就能增加读吞吐。
- 故障恢复:主节点挂了可以切换到从节点继续服务。
最大的缺点:不具备自动故障转移能力。主节点挂了需要运维人员手动介入——把某个从节点提升为新的主节点,然后通知其他从节点和客户端更新连接信息。在凌晨三点接到告警电话手动切换——这体验可不好。而且在手动切换完成之前,如果数据没有及时同步到从节点,还会导致数据丢失。
三、哨兵模式:自动故障转移
为了解决主从模式的"手动切换"问题,Redis 引入了哨兵(Sentinel)。
哨兵就像小区的保安——它不存数据,专门负责监控。时刻盯着主节点和从节点的状态,一旦主节点挂了,自动完成故障转移,不需要人工介入。
通常需要部署奇数个哨兵节点(至少 3 个),以确保投票判定的准确性和容错能力。
故障检测与转移流程
第一步:PING 探活
每个哨兵定期向主节点和所有从节点发送 PING 命令,就像保安每隔几分钟巡逻一圈。
第二步:主观下线(SDOWN)
如果某个哨兵在配置的 down-after-milliseconds 时间内没收到主节点的 PONG 响应,它会把主节点标记为"主观下线"。这只是一个哨兵的个人判断——也许只是这个哨兵自己和主节点之间的网络不通,并不能说明主节点真的挂了。
第三步:客观下线(ODOWN)
这个哨兵会询问其他哨兵:"你们觉得主节点挂了吗?"当超过配置的 quorum(法定人数)的哨兵都认为主节点下线了,主节点才会被标记为"客观下线"——大家都说你挂了,那你大概率真的挂了。
第四步:选举新主
哨兵从所有健康的从节点中选出一个新的主节点。选择标准按优先级排序:
- 从节点的
slave-priority配置值(越小优先级越高)。 - 复制偏移量(数据最新的优先,保证数据尽量不丢)。
- 运行 ID(最小的优先,这是最后的仲裁规则)。
第五步:通知更新
新主节点被选出后,哨兵会:
- 让新主节点执行
SLAVEOF NO ONE,升级为主节点。 - 让其他从节点指向新的主节点(
SLAVEOF new_master)。 - 通过发布订阅机制通知客户端,更新连接信息。
整个过程自动完成,不需要人工介入。哨兵模式的核心价值就是这个自动化。
脑裂问题
脑裂是分布式系统中的经典问题——网络分区导致集群中出现两个"主节点",各自接受写请求,数据就分叉了。名字很形象:大脑裂开了,一个身体有了两个大脑,各自为政。
在上面的场景中:哨兵发现和原 Master 连不上了,选了 Slave 1 做新 Master。但原 Master 其实没挂,只是网络分区了,它还在接受部分客户端的写入。等网络恢复后,原 Master 会变成新 Master 的从节点,做全量同步——它在分区期间接收的所有写数据都会被丢弃。
脑裂的三大危害:
- 数据不一致:两个主节点可能对同一数据写入了不同的值。
- 数据丢失:网络恢复后,旧主节点要做全量同步,清空自己的数据,分区期间的写入全部丢失。
- 重复写入:客户端可能在两个主节点上执行了相同的写操作。
如何缓解脑裂?Redis 提供了两个配置参数:
min-slaves-to-write 1 # 主节点至少有1个从节点在线才接受写入
min-slaves-max-lag 10 # 从节点的ACK延迟不超过10秒这两个参数必须同时满足才允许主节点写入。如果主节点与所有从节点的通信都断了超过 10 秒,主节点会主动拒绝写入,从而避免脑裂期间产生脏数据。
脑裂能彻底解决吗?
不能,只能通过合理配置尽量规避。举个例子:假设 down-after-milliseconds 是 8 秒,min-slaves-max-lag 是 10 秒,主从切换过程需要 5 秒。主节点宕机第 8 秒时哨兵开始切换,但主节点在第 9 秒恢复了——这时候 lag 还没超过 10 秒,主节点依然可写。那第 9~13 秒之间写入的数据就会在新主节点选出后丢失。这是分布式系统中一致性和可用性的天然权衡(CAP 定理)。
四、Cluster:分片集群
主从 + 哨兵解决了高可用问题,但还有一个问题没解决:容量上限。单个主节点的内存就那么大,数据量再大就装不下了。而且写操作全部集中在一个主节点上,写吞吐也有天花板。
Redis Cluster 的方案是:把数据分片(sharding)存储到多个节点上,每个节点只负责一部分数据。同时每个分片有主从副本,既能水平扩展,又能自动容错。
Cluster 是 Redis 官方推荐的分布式集群解决方案,它提供了:
- 数据自动分片:数据按规则分散到多个节点。
- 自动故障转移:内置故障检测,不需要额外的哨兵。
- 水平扩展:添加节点就能扩容,删除节点就能缩容。
4.1 16384 个哈希槽
Redis Cluster 把整个数据集划分为 16384 个哈希槽(hash slot),编号 0~16383。每个主节点负责一部分槽。
当你执行 SET name "Redis" 时,Redis 会用 CRC16 算法计算 key 的哈希值,然后对 16384 取余,得到对应的槽编号,再路由到负责该槽的节点。
slot = CRC16("name") % 16384这个过程对客户端来说是透明的——客户端连接集群中的任意节点,如果要操作的 Key 不在该节点上,节点会返回一个 MOVED 重定向,告诉客户端去找正确的节点。智能客户端(如 Jedis Cluster、Lettuce)会缓存槽和节点的映射关系,大部分情况下能直接连到正确的节点,避免多余的网络跳转。
为什么是 16384 而不是 65536? Redis 作者在 GitHub 上解释过,核心原因有三个:
- 心跳包大小:节点间的心跳消息要携带槽的位图(bitmap)信息。16384 个槽只需要 2KB(16384 / 8 = 2048 bytes),而 65536 需要 8KB,对于频繁发送的心跳包来说太浪费带宽了。
- 集群规模:Redis Cluster 的设计上限是大约 1000 个主节点。16384 个槽能保证每个节点平均分到 16 个槽以上,已经足够均衡了。用 65536 的话每个节点的槽太多,负载管理和数据迁移成本都会增加。
- 计算效率:16384 = 2^14,位运算友好,计算哈希槽的效率更高。
4.2 节点通信:Gossip 协议
Cluster 中的节点通过 Gossip 协议进行通信——每个节点定期随机选几个其他节点交换信息(包括节点状态、槽分配情况等)。就像村子里的八卦传播,虽然不是瞬间传遍全村,但最终每个人都会知道所有消息。
Gossip 协议的优点是去中心化、容错性强,不需要一个专门的"中心管理者"。缺点是信息传播有一定延迟——在集群规模较大时,一个节点状态变化可能需要几秒钟才能被所有节点感知到。
4.3 故障检测与恢复
Cluster 的故障检测机制和哨兵类似,也是"主观下线 → 客观下线"的两阶段判定:
- 节点 A 发现节点 B 没有响应 PING,标记 B 为主观下线(PFAIL)。
- 节点 A 通过 Gossip 协议告诉其他节点:"我觉得 B 挂了"。
- 当超过半数的主节点都标记 B 为 PFAIL 时,B 被标记为客观下线(FAIL)。
- 如果 B 是主节点,它的某个从节点会自动提升为新的主节点,接管 B 负责的槽。
Cluster 不需要额外部署哨兵——它自己内置了故障检测和转移机制,部署和运维更简单。
4.4 Cluster 中的事务和 Lua 限制
和 MySQL 分库分表一样,Redis Cluster 的分片架构对跨节点操作有限制:
- 事务不能跨多个节点执行,事务中涉及的所有 Key 必须在同一个节点(同一个槽)上。
- Lua 脚本中访问的所有 Key 也必须在同一个节点上,否则报错
command keys must in same slot。
解决方案:Hash Tag
在 Key 名中用花括号 {} 标记分片依据,Redis 只用花括号内的部分计算哈希值。
user:{12345}:profile → CRC16("12345") % 16384
user:{12345}:settings → CRC16("12345") % 16384
user:{12345}:orders → CRC16("12345") % 16384这三个 Key 的 Hash Tag 都是 12345,一定会落在同一个节点上,就能在一个事务或 Lua 脚本中操作了。这和 MySQL 分库分表中的"基因法"是类似的思路——通过设计 Key 的命名规则来控制数据的物理分布。
其他方案还包括:在应用层拆分操作(把跨节点的操作拆成多个单节点操作),或者使用分布式事务管理器协调。
Redis 7.0.11 新增了 allow-cross-slot-keys 选项
这个选项允许在单个命令中使用不同槽的 Key(比如 MSET 多个跨槽的 Key)。但需要注意:即使启用了这个选项,事务和 Lua 中的 Key 仍然必须在同一个槽才能保证原子性。它解决的是普通命令的跨槽便利性问题,不是事务的原子性问题。
五、过期删除与内存淘汰
5.1 惰性删除 + 定期删除
Redis 的 Key 设了过期时间后,到期了会立即删除吗?不一定。
Redis 采用两种策略配合删除过期 Key:
惰性删除(被动):Key 过期后不立即删除,等到下次有请求访问这个 Key 时,才检查是否过期。如果过期了就删除并返回 nil,没过期就正常返回。
好处是不浪费 CPU——不用花时间去主动扫描。坏处是如果一个过期 Key 再也没人访问,它就永远留在内存里——一种隐性的内存泄漏。
定期删除(主动):Redis 默认每秒执行 10 次(由 hz 参数控制)过期扫描,每次流程如下:
- 从设置了过期时间的 Key 中随机抽取 20 个。
- 删除其中已过期的 Key。
- 如果过期 Key 的比例超过 25%,回到第 1 步继续抽样。
- 直到过期比例降到 25% 以下,或者本次扫描耗时超过 25 毫秒。
Redis 默认同时开启这两种策略,互相补充。
为什么一批 Key 同时过期会导致其他 Key 的读写变慢?
因为定期删除是在 Redis 的主线程中执行的(Redis 是单线程模型)。如果大量 Key 同时过期,每次抽样中过期比例都超过 25%,主线程就会不断循环删除,直到超时。这段时间内,后面排队的业务请求只能等着——表现为访问延迟突然增大。
解决方案:用随机 TTL 打散过期时间,避免大量 Key 在同一时刻过期。
关于内存释放的一个细节:即使 Redis 删除了过期 Key,操作系统的 RSS(常驻内存)也不一定会立即下降。因为 Redis 使用的内存分配器(默认 jemalloc)会把释放的内存放到"空闲池"中复用,而不是每次都归还给操作系统。这是为了减少系统调用的开销。所以你可能看到 info memory 中 used_memory 下降了,但系统监控的内存占用没怎么变——这是正常的,不用慌。
5.2 八大内存淘汰策略
当 Redis 内存用满(达到 maxmemory 配置)后,新的写入命令怎么办?这时候就需要内存淘汰策略了——决定哪些 Key 要被"牺牲"掉来腾出空间。
Redis 提供了 8 种策略:
| 策略 | 作用范围 | 淘汰规则 |
|---|---|---|
| noeviction | - | 不淘汰任何 Key,直接返回错误 |
| allkeys-lru | 所有 Key | 淘汰最近最少使用的 Key |
| volatile-lru | 设了过期时间的 Key | 淘汰最近最少使用的 Key |
| allkeys-random | 所有 Key | 随机淘汰 |
| volatile-random | 设了过期时间的 Key | 随机淘汰 |
| volatile-ttl | 设了过期时间的 Key | 淘汰剩余 TTL 最短的 Key |
| allkeys-lfu | 所有 Key | 淘汰访问频率最低的 Key |
| volatile-lfu | 设了过期时间的 Key | 淘汰访问频率最低的 Key |
怎么选?
- Redis 作为纯缓存(最常见):推荐
allkeys-lru。最久没被访问的数据,未来被访问的概率也最低,淘汰它们对命中率影响最小。 - Redis 作为半缓存半持久化:用
volatile-lru。只淘汰设了过期时间的 Key,没设过期时间的"永久"数据不会被误删。但这种用法本身不推荐——Redis 不建议承担持久化职责。 - 不确定用什么:千万别用默认的
noeviction!内存满了直接返回错误,会导致写操作全部失败,业务直接崩。阿里云 Redis 默认用的是volatile-lru,腾讯云默认是noeviction(官方建议创建实例后立即修改)。
LRU vs LFU 的区别
- LRU(Least Recently Used)关注"最后一次访问时间"——越久没用的越先淘汰。
- LFU(Least Frequently Used)关注"访问次数"——访问次数最少的越先淘汰。
举个例子:数据 A 过去一小时被访问了 100 次但最近 5 分钟没被访问,数据 B 最近 1 分钟被访问了 1 次。LRU 会淘汰 A(最近没被用),LFU 会保留 A(总次数多)。在有些场景下 LFU 命中率更高,Redis 4.0+ 开始支持。
六、常见面试题精选
Q1:RDB 和 AOF 怎么选?
如果追求恢复速度和备份效率,用 RDB。如果追求数据可靠性,用 AOF。最佳实践是开启混合持久化(Redis 4.0+),兼得两者优点:RDB 的恢复速度 + AOF 的数据完整性。
Q2:主从复制会不会丢数据?
会。主从复制是异步的,主节点写入后不等从节点确认就返回给客户端。如果主节点在同步完成前挂了,未同步的数据就丢了。这是 Redis 选择了 AP(可用性 + 分区容忍性)而非 CP(一致性 + 分区容忍性)的体现。
Q3:哨兵和 Cluster 的区别是什么?
哨兵解决的是高可用问题——监控主节点状态、自动故障转移,但数据仍然存在一个主节点上,没有解决容量上限和写扩展问题。Cluster 解决的是高可用 + 水平扩展——数据分片存储在多个节点上,每个分片有自己的主从副本,读和写都能扩展。如果数据量不大且写压力不高,哨兵就够了;如果需要存储海量数据或需要更高的写吞吐,必须上 Cluster。
Q4:Redis 的脑裂问题怎么解决?
通过 min-slaves-to-write 和 min-slaves-max-lag 两个参数配合。当主节点无法与足够数量的从节点保持通信时,主动拒绝写入,避免脑裂期间产生脏数据。但这不能彻底解决脑裂,只能尽量降低数据丢失的风险——在主从切换的过程中仍然有短暂的数据丢失窗口。
Q5:为什么 Redis Cluster 的哈希槽是 16384 个?
三个原因:(1)心跳包中槽位图只需 2KB,用 65536 要 8KB,太浪费带宽;(2)Redis Cluster 设计上限约 1000 个主节点,16384 个槽足够每个节点分到合理数量的槽;(3)16384 = 2^14,位运算效率高。
Q6:过期 Key 一定会被立即删除吗?
不会。Redis 用惰性删除 + 定期删除的组合策略。惰性删除是访问时才检查和删除,定期删除是每秒随机抽样检查。所以过期 Key 可能在过期后仍然存在一段时间。大量 Key 同时过期还会拖慢其他请求的响应速度(因为删除在主线程中执行)。
小结
| 组件 | 解决的问题 | 核心能力 |
|---|---|---|
| RDB / AOF | 数据持久化 | 重启后数据恢复 |
| 主从复制 | 数据冗余 + 读写分离 | 读扩展、数据备份 |
| 哨兵 | 主节点故障 | 自动故障转移 |
| Cluster | 容量上限 + 高可用 | 数据分片 + 自动容错 |
| 过期策略 | 过期 Key 清理 | 惰性删除 + 定期删除 |
| 内存淘汰 | 内存打满 | 8 种 LRU/LFU/TTL 策略 |
Redis 的高可用架构是一层层叠加上去的:持久化是地基,主从复制是框架,哨兵是安保系统,Cluster 是商业综合体。根据你的业务规模选择合适的层级就好——不是每个项目都需要 Cluster,很多时候主从 + 哨兵就完全够用了。小团队不要过度设计,大团队不要裸奔上线。
附录:Redis 事务机制
在讨论集群和分布式场景时,事务是一个绕不开的话题。这里补充介绍 Redis 事务的核心机制和它与 MySQL 事务的关键区别。
基本用法
Redis 事务通过以下命令实现:
MULTI:开启事务,后续命令进入队列。EXEC:执行队列中的所有命令。DISCARD:取消事务,清空队列。WATCH key:监视一个 Key,如果在事务执行前被其他客户端修改,事务会被取消。
Jedis jedis = new Jedis("localhost", 6379);
// 开启事务
Transaction tx = jedis.multi();
tx.decrBy("item:10001:stock", 1); // 扣减库存
tx.rpush("orders", "user:10001,item:10001"); // 记录订单
// 执行事务
List<Object> result = tx.exec();
if (result == null) {
System.out.println("事务执行失败(Key 被 WATCH 修改)");
} else {
System.out.println("事务执行成功");
}Redis 事务 vs MySQL 事务
两者最大的区别在于:Redis 事务不支持回滚。
MySQL 的事务遵循 ACID 原则——原子性、一致性、隔离性、持久性。如果事务中有一条 SQL 失败了,整个事务会回滚到执行前的状态。
Redis 的事务则简单得多:它只保证一组命令按顺序执行、不会被其他客户端的命令打断(原子性的一种——不可中断,但不包括回滚)。如果事务中的某条命令执行失败(比如对字符串执行了列表操作),其他命令仍然会继续执行,不会回滚。
Redis 为什么不支持回滚? 官方给出的理由是:
- 性能优先:引入回滚机制会大幅增加复杂性和性能开销,与 Redis 的设计哲学冲突。
- 错误类型有限:Redis 命令失败的原因通常是语法错误或类型不匹配,这些应该在开发阶段就发现并修复,不应该靠运行时回滚来兜底。
- 使用场景不同:Redis 是缓存,不是关系型数据库。如果你需要复杂的事务支持,应该用 MySQL。
事务 vs Lua 脚本
Redis 中除了事务,Lua 脚本也能保证原子性执行。两者的核心区别:
| 维度 | 事务(MULTI/EXEC) | Lua 脚本 |
|---|---|---|
| 命令间依赖 | 不支持——后续命令不能依赖前面命令的结果 | 支持——可以用前一个命令的结果做判断 |
| 网络交互 | 每条命令一次网络交互 | 整个脚本一次性提交 |
| 流程控制 | 不支持 if/else/循环 | 支持完整的 Lua 语法 |
| 失败处理 | 某条失败不影响其他命令 | 某条失败会终止后续执行 |
| 回滚 | 不支持 | 不支持 |
一般来说,如果需要在 Redis 中实现复杂的原子性逻辑(比如"先读后写"的 CAS 操作),Lua 脚本是更好的选择。
在 Cluster 中使用事务和 Lua
前面提到,Cluster 中的事务和 Lua 都要求所有 Key 在同一个节点上。这是因为 Cluster 的每个节点是独立的 Redis 实例,跨节点的原子性操作需要分布式事务的支持,而 Redis 为了保持简单和高性能,选择不提供这种支持。
解决方案主要有三种:
- Hash Tag:通过
{tag}把相关 Key 路由到同一个节点。 - 应用层拆分:把跨节点的操作拆成多个独立的单节点操作。
- 业务取舍:评估是否真的需要跨 Key 的原子性,很多时候单 Key 的原子操作(如 INCR、SETNX)就够用了。