什么是异地多活?
结论先行:异地多活指多个地域的机房同时对外提供读写服务,任何一地故障,流量切到其余单元继续读写,RTO 达分钟级、RPO 接近 0。它不是"多地部署一套再加个 DNS 切换"——那只是灾备。真正的门槛是单元化闭环:让同一个用户的读写都落在同一单元,从而绕开跨地域事务。
1. 先分清几种容灾形态
| 形态 | 平时资源利用率 | RTO | RPO | 关键特征 |
|---|---|---|---|---|
| 单机房 | 100% | — | — | 无容灾,机房故障即全站故障 |
| 同城双活(双 AZ) | ≈50%×2 | 分钟级 | 0 | 同城两台独立供电/网络,延迟 <2ms |
| 两地三中心 | 生产 + 同城灾备 + 异地灾备 | 小时级 | 分钟级 | 异地那套通常只做冷/温备 |
| 异地多活 | 每单元都承载流量 | 分钟级 | 近 0 | 多单元同时读写,按用户分片 |
2. 核心:单元化闭环
用户 U1(归属单元 A)
│ DNS/GSLB 就近解析
▼
┌──────────────────────┐ ┌──────────────────────┐
│ 单元 A(华东) │ │ 单元 B(华北) │
│ 接入 → 应用 → 缓存 │ │ 接入 → 应用 → 缓存 │
│ │ │ │ │ │
│ DB-A(主) │◄────────►│ DB-B(主) │
└──────────────────────┘ 异步双向 └──────────────────────┘
用户 U2(归属单元 B)── 就近 ──► 单元 B,全程不跨单元
四条设计原则:
- 按用户维度分片,一个用户的所有写请求固定落在一个单元("单元内闭环")。
- 单元内自成体系:接入、应用、缓存、DB 全都有,不依赖其他单元。
- 单元间只同步必要数据,且尽量单向,减少冲突。
- 路由必须可计算、可配置,不能靠人工分配。
3. 数据要分三类处理
| 数据类型 | 例子 | 处理策略 |
|---|---|---|
| 用户私有数据 | 订单、余额、购物车 | 单元内闭环,本单元主写,其他单元只读副本 |
| 全局只读数据 | 商品、类目、配置 | 一处写入,单向广播到各单元(缓存优先) |
| 跨单元共享写数据 | 库存、转账、优惠券库存 | 收敛到单一"权威单元",或走全局服务 + 对账 |
第三类是最难的:库存扣减跨单元,必须选定一个 owner,其他单元远程调用或异步扣减 + 补偿。
4. 数据同步与冲突
| 方式 | 延迟 | 一致性 | 适用 |
|---|---|---|---|
| 数据库原生复制(binlog) | 毫秒~秒 | 最终一致 | MySQL 主从、单元间只读同步 |
| CDC 中间件(Canal/DTS) | 秒级 | 最终一致,可过滤转换 | 只同步部分表/字段,异构存储 |
| 应用双写 | 取决于实现 | 弱,易丢一边 | 尽量避免 |
| 同步双写(跨地域) | 几十毫秒 | 强一致 | 延迟不可接受,仅用于极少量核心数据 |
写冲突的三种解法:
- LWW(Last Write Wins):按时间戳覆盖,实现最简,会静默丢写。
- 业务规避:设计上禁止两个单元同时写同一行(按用户闭环就天然满足)。
- CRDT / 计数器合并:适合点赞数、库存这类可交换合并的数据。
5. 路由配置与算法
# unit-routing.yaml
units:
- id: unit-a
region: cn-east
weight: 1
shards: [0, 511] # 覆盖 user_id hash 的前半段
db: jdbc:mysql://db-a:3306/app
- id: unit-b
region: cn-north
weight: 1
shards: [512, 1023]
db: jdbc:mysql://db-b:3306/app
fallback: unit-a # 路由表缺失时的兜底单元
public final class UnitRouter {
private static final int SHARDS = 1024;
// 用 user_id 稳定取模,保证同一用户永远落同一单元
public static String route(long userId) {
int shard = Math.floorMod(Long.hashCode(userId), SHARDS);
return shard <= 511 ? "unit-a" : "unit-b";
}
}
扩容时不要改模数(会导致全量数据搬迁),改用一致性哈希或"虚拟桶 + 迁桶":桶数固定 1024,扩单元只是把若干桶整体迁移,迁移期间双写 + 校验。
6. 常见追问
Q:为什么不能只做"就近接入 + 主从复制"? 那样写仍然只能落在唯一主库,异地机房只分担读。主库所在城市一挂,写就停了——RTO 达不到分钟级,本质还是灾备而非多活。
Q:跨单元转账怎么做? 两种主流做法:一是把"账户"这类强一致数据收敛到单一权威单元,跨单元调用;二是用"本地记账 + 异步清算 + 日终对账",牺牲实时性换可用性。多数金融系统选前者,因为一致性优先。
Q:单元内闭环有没有例外? 有。登录态、全局唯一 ID、库存这类天然全局的数据必须跨单元,通常用独立的全局服务(发号器、统一会话中心)承载,并接受其成为新的单点——所以要给它做多副本 + 快速切主。
Q:异地多活最大的代价是什么? 复杂度:路由表、数据同步、冲突处理、跨单元调用、灰度迁移、双写校验、对账系统。如果没有明确的监管或业务要求,同城双活 + 异地冷备的性价比更高。
Q:多活会不会脑裂? 会。两个单元都认为自己是权威时,会各自接受写。防护靠仲裁(quorum)+ 租约(lease):写操作需要多数派确认,租约到期自动降级为只读。
小结
- 异地多活 = 多单元同时读写,核心是按用户分片的单元化闭环。
- 数据分三类:私有数据闭环、全局数据单向广播、共享写数据收敛到权威方。
- 路由要可计算(哈希/虚拟桶),扩容靠迁桶而不是改模数。
- 代价是复杂度与成本;没有强需求时,同城双活 + 异地温备更划算。