Redis 地理多活 · Active-Active 与 CRDT 冲突收敛

作者:杨帅      发布日期:2026.08.06

Redis · 地理多活

地理多活

每个区域都能本地读写,并发冲突自动收敛到一致

普通复制只有一个主库能写。Active-Active 让每个区域都是可写的主库,用户就近读写、延迟低到亚毫秒。代价是并发写必然冲突——Redis 用 CRDT 把冲突变成一道确定的合并题,让所有区域最终收敛到同一个结果。

▲ 两地并发 +1 · CRDT 计数器求和收敛conflict-free merge
US 区域 · 可读可写 primary · endpoint-us views = 5 6 7 INCR views (+1) EU 区域 · 可读可写 primary · endpoint-eu views = 5 6 7 INCR views (+1) 异步同步 syncer 朴素 LWW = 6 ✗ 丢一次 CRDT 求和 = 7 ✓ 都算上 计数器的合并是「求和」而非「覆盖」——两地的 +1 都不丢
US 区域:本地可写 EU 区域:本地可写 收敛后的一致值
01 · 它是什么

每个区域都是可写的主库

Active-Active(旧称 CRDB)是基于 CRDT 的地理多活数据库。它至少横跨两个参与集群(区域),每个区域各有自己的主节点和 endpoint。应用连最近的区域,本地读写延迟可低于 1ms;某个区域挂了,其它区域照常服务。

这和上一类「一主多从」的复制根本不同:那里只有一个主库能写,从库只读;这里是多主(multi-primary),你连哪个区域都能写。

打个比方

像一份多人协作文档:每个人(区域)都在编辑自己手上的副本,改动异步同步给其他人。关键不在于"谁先谁后",而在于合并规则足够聪明——谁都不会无声无息地覆盖掉别人的改动。

!

只复制数据本身。 Active-Active 在区域间同步的是数据,不包括数据库配置、Lua 脚本等——这些要在每个区域各自设置。

02 · 为什么用它

低延迟、容灾、合规

全球低延迟

用户就近连本地区域读写,不必跨洋往返,写延迟可低至亚毫秒。

容灾

多主、无单点。一个区域整体故障,其它区域继续对外提供读写。

数据驻留合规

可以把数据保留在特定地理区域,满足数据主权与合规要求。

03 · 核心思想

冲突不可避免,但可以无冲突地解决

既然能连任意区域写,并发且冲突的写入随时可能发生。传统做法是「最后写入者胜」(LWW)——简单,但会丢数据:两地同时给计数器 +1,LWW 只保留一个,结果少算了一次(就像首图里的 6 而非 7)。

CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)换了思路:为每种数据类型设计一套满足交换律 / 结合律 / 幂等的合并规则,使得无论同步顺序如何,所有副本都收敛到同一结果,而且这个结果符合该类型的语义意图。应用不需要写任何冲突处理代码。

一句话抓住本质:把「谁覆盖谁」的抢占,换成「怎么合并」的运算。 加法可交换,所以两地的 +1 无论先同步哪个,结果都是 +2。

04 · 按类型解决

不同数据类型,不同合并规则

CRDT 的冲突解决是「按数据类型 + 命令」定制的——它理解每种类型"本该"怎么合并。

数据类型冲突解决说明
计数器 Counter
INCR / DECR
求和 · 无冲突 各区域的增减全部累加,不丢更新。并发「删除 vs 自增」→ 自增胜(add-wins),DEL 视为"重置"。
集合 Set
SADD / SREM
Add-Wins 观察移除 并发 SADD 取并集、无冲突(可结合);添加与删除相争时,添加胜。
字符串 String
SET / APPEND
最后写入者胜 LWW 寄存器语义。并发设置不同过期时间时,较大的 TTL 胜
哈希 Hash
HSET / HDEL
按字段合并 映射型 CRDT。改不同字段互不冲突;同一字段按计数器 / 字符串语义解决。
列表 List
LPUSH / POP
保留所有项 用元素 ID 标识,合并时保留各区域插入的所有项。
▲ 集合并发 SADD → 并集,无冲突add-wins
US · SADD s "a" { a } { a, b } EU · SADD s "b" { b } { a, b } 两个不同成员的添加是可结合的 → 合并成并集,谁都不丢
05 · 一致性模型

强最终一致性与 Syncer

Active-Active 采用强最终一致性:短时间内各区域的本地值可能不同,但最终都会收敛到同一状态。底层用向量时钟 + CRDT 判定合并;还可选开启因果一致性,在同步时保持操作的因果顺序。

跨区域同步靠 Syncer 进程:它维护一份复制积压(backlog),把变更发给其它区域。平时做增量同步,当某个副本或主库丢失时,做一次全量同步补齐。

▲ 短暂分歧 → Syncer 同步 → 收敛一致vector clocks · converge
US 本地状态 vc=[2,1] vc=[2,2] ✓ 短暂分歧 Syncer backlog · 增量同步 EU 本地状态 vc=[1,2] vc=[2,2] ✓ 短暂分歧
!

这不是强一致(线性一致)。 本地读到的可能是「还没同步完的旧值」,通常几秒内收敛。如果业务需要读到全局最新的唯一真相(如库存扣减防超卖),Active-Active 不是合适工具——那是另一种取舍。

06 · 在 Redis Cloud 里

创建一个多活数据库

1

建库时选 Active-Active 类型

在控制台点 Create DatabaseTypeActive-Active

2

选择 2 个及以上区域

例如 us-east-1eu-west-1,分别设置内存大小与区域内复制。每个区域会拿到自己的 endpoint

Type Active-Active Regions us-east-1, eu-west-1 # ≥ 2 个参与区域 Per-region 内存大小 + 区域内副本 # 每个区域各有独立 endpoint,应用就近连接
3

应用就近连接、本地读写

应用连接最近区域的 endpoint 即可,读写都在本地完成,跨区同步由 Redis 自动处理。

i

几个要记住的边界:计数器是 59 位(比 64 位有符号少 5 位,上限约 2.88×10¹⁷);Active-Active 数据库默认驱逐策略是 noeviction,且驱逐在分片达 80% 内存时就更早触发;跨区只同步数据,配置与脚本各区自理。

07 · 常见的坑

用多活前,先想清楚这些

×
把它当强一致用A-A 是强最终一致,本地读可能短暂偏旧。要线性一致(防超卖等)就别用它。
×
用 LWW 心智套计数器计数器是求和不是覆盖。想累加就用 INCR,别用 SET 去"设置"计数值。
×
指望配置/脚本自动同步只有数据跨区同步,数据库配置和 Lua 脚本不会——每个区域都要各自配置。
×
忽略计数器位宽CRDT 计数器是 59 位,不是 64 位。接近极限的数值设计要提前留意。
×
内存留太满A-A 驱逐更早(80%)、默认 noeviction,容量要留足余量,否则写入报错。
×
假设所有命令语义不变部分命令 / 数据类型在 A-A 下语义不同或受限,开发前查"Develop with A-A"文档。
收尾 · 一句话

把抢占,换成合并

Active-Active 让每个区域都能写,用 CRDT 把并发冲突变成一道确定的合并题,最终所有区域收敛到同一状态。它用最终一致,换来了全球低延迟区域级容灾

多主可写 · 冲突按类型合并(计数器求和 / 集合并集 / 字符串 LWW)· 向量时钟 + Syncer 收敛 · 强最终一致