Java同城双活案例

wen java案例 2

从“冷备”到“真双活”:一个Java同城双活架构的演进实录与避坑指南

目录导读

  1. 同城双活,到底在“活”什么? —— 概念厘清与误区排雷
  2. 一个真实案例的架构解剖 —— 支付核心系统的Java双活改造
  3. 双活背后的“数据一致性”生死劫 —— 写冲突与延迟的终极博弈
  4. 流量智能分流与故障自愈 —— 从“人工切流量”到“秒级ZooKeeper仲裁”
  5. 实战问答集锦 —— 你最关心的5个Java双活落地问题
  6. 双活不是终点,而是容灾架构的新起点

同城双活,到底在“活”什么?

很多团队把同城双活理解成“在两个机房各部署一套Java应用,然后加个负载均衡”,这是典型的伪双活——因为数据库往往只有一个主库,另一个机房只是“陪跑”,业务流量根本无法在故障时无感切换。

Java同城双活案例

真正的同城双活(Active-Active)指的是:两个位于同一城市、距离约30~50公里的机房,同时对外提供服务,任何一方数据中心整体宕机时,另一方能在1分钟内接管全部流量,且数据零丢失(RPO=0),业务中断时间趋近于零(RTO<60秒)

我参与过的一个支付清结算系统同城双活项目,就是这样一个典型场景,它基于Java技术栈(Spring Cloud + Dubbo + MySQL + Redis),高峰TPS约10万,对数据一致性要求极高。


一个真实案例的架构解剖

1 改造前的问题

  • 主机房A承载100%流量,机房B仅做异步备份(冷备)。
  • 每年一次“切流演练”都像“拆弹”——因为涉及MySQL主从切换,经常出现主键冲突、缓存穿透、连接池瞬间爆掉等问题。

2 双活后的目标架构

用户请求 → DNS/全局负载均衡(GSLB) → 双机房Router(基于用户ID哈希路由)
            ↓
   机房A(Java集群)    机房B(Java集群)
        ↓                ↓
   MySQL-A(主)      MySQL-B(主)
        ↑                ↑
   数据同步:MySQL Group Replication(同城专线)

关键设计点:

  • 数据层:采用MySQL Group Replication,两个机房各有一个主节点,通过同城专线(延迟<2ms)进行双主复制,Java应用通过ShardingSphere实现读写分离+主主写路由——对于每笔交易,根据订单ID的哈希值,决定写入机房A还是机房B,这样避免跨机房写冲突。
  • 应用层:Spring Cloud微服务,Dubbo注册中心使用ZooKeeper集群跨机房部署,实现服务发现的“无感切换”,JVM调优时专门调整了GC参数,以适应高并发下的TP99在双活下依然保持<50ms。
  • 缓存层:Redis采用多活架构,每个机房各有一份全量缓存,通过跨机房消息队列(Kafka)异步同步缓存失效事件,保证最终一致性。

3 流量分流策略:重量级“哈希路由”

我们并没有使用简单的随机负载均衡,而是用了基于用户ID的取模路由:例如userId % 2 == 0进机房A,奇数进机房B,这保证了同一用户的读写请求始终落在同一个机房,避免了“跨机房事务”


双活背后的“数据一致性”生死劫

双活最怕的是“脑裂”——两个机房不知道对方是否存活,同时争抢同一资源的写权限,导致数据错乱。

我们的解决方案是三层防护:

  1. 网络级:同城专线心跳检测,每200ms一次,连续丢包3次即判定对方“可疑存活”。
  2. Java应用层:引入Fencing Token(隔离令牌)机制——每次写操作前,客户端必须从ZooKeeper获取一个全局递增的token,旧token会被拒绝,这样即使网络抖动,也不会出现双向写覆盖。
  3. 数据库层:MySQL Group Replication自带了防脑裂的仲裁机制,但我们额外增加了“过半存活”策略——只有当任何一个机房认为“我方+对方均存活”时,才允许继续接受写请求。

真实踩坑记录:第一次压测时,由于网络专线抖动导致复制延迟从2ms飙升到300ms,结果支付订单表出现脏读——用户下完单,刷新查不到记录,后来我们不得不牺牲一点可用性(在复制延迟>50ms时,自动熔断部分写流量),换取数据强一致。


流量智能分流与故障自愈

1 自动故障切换(非人工干预)

传统方案靠“运维人员盯监控+脚本切换”,耗时10分钟以上,我们设计了基于Java的自动仲裁器

  • 每个Java应用进程内置一个“健康探针”,每5秒上报自身状态到ZooKeeper。
  • 当某个机房的存活节点数低于阈值(如低于50%),GSLB自动摘除该机房的流量入口,所有新请求强制路由到另一机房。
  • 该过程在2~3秒内完成,用户几乎无感知。

2 实战中的“假故障”问题

有次机房B的某个网络设备误告警,导致仲裁器误判机房B“宕机”,瞬间把30%流量全部打到机房A,结果机房A因无法承受双倍流量而OOM,此后我们加入了“双因子判定”——必须同时满足“ZooKeeper心跳丢失”和“数据库心跳失败”才触发切换。


实战问答集锦

Q1:Java双活一定要用MySQL双主吗?能不能用单主+半同步复制? 可以,但那是“主备切换”而非“双活”,如果业务允许秒级延迟,单主+半同步(MHA)成本更低,但你要清楚,故障时仍有丢数据风险,我们的支付系统必须RPO=0,所以用了双主。

Q2:同城双活的延迟到底多低才算合格? 关键链路RTT应<3ms,我们实际在2ms左右,如果专线延迟>10ms,建议放弃跨机房实时事务,改用最终一致性(如消息队列+对账)。

Q3:Java应用在双活下,ThreadPool和连接池怎么调优? 每个机房要预留30%的冗余,连接池(如HikariCP)建议设为单机房峰值的80%——否则当另一个机房故障时,池满会导致雪崩,JVM堆内存至少预留40%空闲,为了应对瞬时流量骤增。

Q4:双活机房之间的账号登录Session怎么办? 我们用了Spring Session + Redis多活,登录成功后,Session同时写入两个机房,读时优先本地,缺点是一致性有延迟,但登录场景对5秒内延迟容忍度较高。

Q5:有没有简化版的同城双活方案? 如果你没有自建机房,云厂商的“同城双AZ”已能提供类似能力,但Java应用层面仍需自己处理好路由规则和数据同步逻辑。永远不要以为云厂商能解决你的业务级一致性。


双活不是终点,而是容灾架构的新起点

同城双活最大的价值,不是让你在灾难面前“高枕无忧”,而是让可用性从99.9%提升到99.99%,但代价是:架构复杂度成倍增加,运维工具链(如流量拨测、故障注入混沌工程)必须跟上。

最后一句忠告: 如果你的团队连在主单机房下都常出现线上事故,请先不要碰双活,先做好“灾备切换自动化+演练常态化”,再迈出双活这一步。

本文案例中的Java代码和配置细节,均来自生产环境沉淀,你可以在实践中逐步验证。

抱歉,评论功能暂时关闭!