什么是异地多活?

#中间件#面试八股

什么是异地多活?

结论先行:异地多活指多个地域的机房同时对外提供读写服务,任何一地故障,流量切到其余单元继续读写,RTO 达分钟级、RPO 接近 0。它不是"多地部署一套再加个 DNS 切换"——那只是灾备。真正的门槛是单元化闭环:让同一个用户的读写都落在同一单元,从而绕开跨地域事务。

1. 先分清几种容灾形态

形态平时资源利用率RTORPO关键特征
单机房100%——无容灾,机房故障即全站故障
同城双活(双 AZ)≈50%×2分钟级0同城两台独立供电/网络,延迟 <2ms
两地三中心生产 + 同城灾备 + 异地灾备小时级分钟级异地那套通常只做冷/温备
异地多活每单元都承载流量分钟级近 0多单元同时读写,按用户分片

2. 核心:单元化闭环

        用户 U1(归属单元 A)
              │ DNS/GSLB 就近解析
              ▼
   ┌──────────────────────┐          ┌──────────────────────┐
   │      单元 A(华东)    │          │      单元 B(华北)    │
   │  接入 → 应用 → 缓存    │          │  接入 → 应用 → 缓存    │
   │            │          │          │            │          │
   │         DB-A(主)     │◄────────►│         DB-B(主)     │
   └──────────────────────┘  异步双向  └──────────────────────┘
        用户 U2(归属单元 B)── 就近 ──► 单元 B,全程不跨单元

四条设计原则:

  1. 按用户维度分片,一个用户的所有写请求固定落在一个单元("单元内闭环")。
  2. 单元内自成体系:接入、应用、缓存、DB 全都有,不依赖其他单元。
  3. 单元间只同步必要数据,且尽量单向,减少冲突。
  4. 路由必须可计算、可配置,不能靠人工分配。

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):写操作需要多数派确认,租约到期自动降级为只读。

小结

  • 异地多活 = 多单元同时读写,核心是按用户分片的单元化闭环。
  • 数据分三类:私有数据闭环、全局数据单向广播、共享写数据收敛到权威方。
  • 路由要可计算(哈希/虚拟桶),扩容靠迁桶而不是改模数。
  • 代价是复杂度与成本;没有强需求时,同城双活 + 异地温备更划算。
笔记加载中…