首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >不停机更换数据库解决方案

不停机更换数据库解决方案

作者头像
JavaEdge
修改2026-06-29 14:49:59
修改2026-06-29 14:49:59
1.7K0
举报
文章被收录于专栏:系统设计系统设计

0 前言

总会遇到更换DB问题:

  • MySQL分库分表后,要从原单实例DB迁移到新DB集群
  • 系统从传统部署方式向云上迁移时,也要从自建DB迁移到云DB
  • 在线分析类系统,MySQL性能不够用时,需切换成专门的分析类DB,如HBase

整个迁移过程,不能长时间停服,也不能丢数据。如何不停机安全迁移数据更换DB呢?

1 不停机更换数据库

设计迁移方案,要做到,每步都可逆。要保证,每执行一个步骤后,一旦出现问题,能快速回滚到上一步。看订单库案例。

先把

1.1 旧库数据复制到新库

旧库还在线,所以不断有订单数据写旧库,不仅要往新库复制数据,还要保证新旧库数据【实时同步】,就要用

1.2 同步程序

实现新旧两个数据库实时同步。

咋实现两个异构DB之间数据实时同步?

Binlog实时同步数据。若源库不是MySQL就麻烦,但也可参考复制状态机理论实现。这步不需回滚,只增加一个新库和一个同步程序,对旧库和程序没任何改变。即使新上线的同步程序影响旧库,只要停掉同步程序。

代码语言:mermaid
复制
graph LR
    订单服务[订单服务] <-->|读写| 旧库[(旧库)]
    旧库 -->|数据同步| 实时同步程序[实时同步程序]
    实时同步程序 -->|数据写入| 新库[(新库)]

改造订单服务,业务逻辑部分不变,DAO层改造:

  • 支持双写新旧两库,并预留热切换开关,能通过开关控制三种写状态:只写旧库、只写新库和同步双写
  • 支持读新、旧两库,预留热切换开关,控制读旧库or新库

1.3 上线【新订单服务】

订单服务仍只读写旧库,不读写新库。让这【新订单服务】稳定运行至少1~2w,期间除验证新订单服务稳定性,还要验证新、旧两个订单库中的数据是否一致。该过程,若新版订单服务异常,可立即下线新订单服务,回滚到旧订单服务。

稳定一段时间后

1.4 开启订单服务的双写开关,同时停掉同步程序

这个双写的业务逻辑,一定先写旧库,再写新库,并以写旧库结果为准。

  • 旧库写成功,新库写失败,返回写成功,但记录日志,后续用这日志验证新库是否还有问题
  • 旧库写失败,直接返回失败,就不写新库

即原则上不能让新库影响现有业务可用性和数据准确性。上面这过程若异常,可关闭双写,回滚到只读写旧库。

切换到双写后新、旧库数据可能存在不一致:

  • 停止同步程序、开启双写,这两个过程难以做到无缝衔接(即使业务低峰期,可能也不是业务停摆期)
  • 双写策略也不保证新旧库强一致,这时就要上线一个对比和补偿程序,对比旧库的最近数据变更,然后检查新库中的数据是否一致,若不一致,就补偿
代码语言:mermaid
复制
graph LR
    订单服务[订单服务] <-->|读写| 旧库[(旧库)]
    订单服务 -->|写入| 新库[(新库)]
    旧库 -->|数据比对| 比对/补偿程序[比对/补偿程序]
    比对/补偿程序 -->|补偿写入| 新库

开启双写后,还要至少稳定运行几w

1.5 持续检查

确保不能有旧库写成功,新库写失败的case。对比程序也没发现新旧两库数据有不一致,这时就可认为,新旧两库数据一直保持同步。

接着

1.6 灰度发布把读请求切到新库

期间若异常,可再切回旧库。全部读请求都切换到新库后,读写请求就已经都切换到新库,实际切换完成。

再稳定一段时间后

1.7 双写改为单写新库,同时停掉对比程序(不可逆)

停掉对比程序,把订单服务的写状态改为【只写新库】,旧库可下线。整个迁移过程中,只有这步不可逆。但这步主要操作就是摘掉不再使用的旧库,对在用的新库无改变,异常可能性已很小。

就完成在线更换DB的全部流程。双写版本的订单服务也就完成了它的历史使命,可在下一次升级订单服务版本时,下线双写功能。

2 对比和补偿程序

2.1 难度

对比两个在随时变化的DB中的数据。没有类似复制状态机这样理论上严谨实际操作还很简单的方法。但还可根据业务数据实际情况,针对实现对比和补偿,经一段时间,把新旧两个DB差异,逐渐收敛一致。

订单这类时效性强的数据,方便对比和补偿。因为订单一旦完成几乎不会再变,对比和补偿程序,就能依据订单完成时间,每次只对比这时间窗口内完成的订单。补偿逻辑简单:发现不一致,直接用旧库订单数据覆盖新库订单数据。

切换双写期间,少量不一致订单数据,等订单完成后,会被补偿程序修正。后续只要不是双写时,新库频繁写失败,就能保证两库数据完全一致。

2.2 麻烦的是更通用case

如商品信息随时可能变化。若数据上有更新时间,对比程序可利用这更新时间,每次在旧库取一个更新时间窗口内的数据,去新库找相同主键的数据对比,发现数据不一致,还要对比更新时间。若新库数据更新时间晚于旧库数据,可能对比期间数据发生变化,这种情况暂不补偿,放到下个时间窗口继续对比。时间窗口的结束时间,不要选取当前时间,而要比当前时间早点儿,如1min前,避免对比正写入的数据。

若数据连时间戳也没,只能去旧库读取Binlog,获取数据变化,去新库对比和补偿。

这些方法严格推敲都不是百分百严谨,都不能保证在任何情况下,经过对比和补偿后,新库数据和旧库完全一样。但大多数情况下,这些实践方法还是可以有效地收敛新旧两个库的数据差异,酌情采用。

3 总结

设计在线切换数据库的技术方案,首先要保证安全性,确保每一个步骤一旦失败,都可以快速回滚。此外,还要确保迁移过程中不丢数据,这主要是依靠实时同步程序和对比补偿程序来实现。

切换过程按顺序:

  1. 上线同步程序,从旧库中复制数据到新库中,并实时保持同步
  2. 上线双写订单服务,只读写旧库
  3. 开启双写,同时停止同步程序
  4. 开启对比和补偿程序,确保新旧数据库数据完全一样
  5. 逐步切量读请求到新库上
  6. 下线对比补偿程序,关闭双写,读写都切换到新库上
  7. 下线旧库和订单服务的双写功能

FAQ

Q:整个切换方案仅一步不可逆:双写切换为单写新库。若不计成本,咋修正迁移方案,让这步也快速回滚?

A:不可逆主要因为,一旦切新库单写,旧库数据就和新库不再一致,无法再切回旧库。所以,关键是,切到新库单写后,需保证旧库数据和新库保持同步。双写得新增一种过渡状态:从双写以旧库为准,过渡到双写以新库为准。再把比对和补偿程序反过来,用新库数据补偿旧库数据。就能做到,一旦出问题,再切回旧库。

也有其它实践:

  • 异常就在新库处理,前提是双写期间bug修完 。这时切到新库自然就行
  • 双写若以新库为准,则新库写失败就不会去写旧库了。所以,不存在这种情况
  • 有个问题。双写时存在旧库成功,新库失败case。若这样逆向操作,同样也存在,双写时没问题,但过渡状态中,新库写失败(双写时,旧库可以成功),而旧库又是在新库中获取数据,那这部分数据就没了

Q:这种数据同步架构下,若下游某同步程序或DB异常,需要把 Binlog 回退到某个时间点然后重新同步,咋办?

A:若下游只有一个同步程序,直接按时间重置Canal实例的位点。但若MQ下游有多个消费者,就不能重置Canal里的位点,否则影响其它消费者。在MQ的消费订阅上按时间重置位点,这只影响出问题的那个订阅。所以,这种架构下,MQ中的消息,最好将保存时间设置长一些,如3天。

去O时设计的一个方案和这个很相似。需要将56亿oracle的财务数据迁移到mysql中并保证一直提供稳定的线上服务。方案主要涉及: 1.设计同步worker以6亿/天的速度同步至MySQL,预先按照1000的间隔生成560w个任务,用于保证数据同步不丢失 2.MySQL业务数据库为防止和oracle中id碰撞,设计id生成器。前三位为库编号 3.因为高达100w/分钟的事务量,设计了前置防重表+任务表(按UUID分库),用于防重、规避热点和异步增加吞吐量 4.业务数据库通过在前置库生成的id分库,同时会异步同步至ES 5.原接口服务改造,写老库数据和发送MQ在一个事务(mq挂了会影响服务)。提供新接口服务写新库并发MQ 6.接收端消费MQ并根据标示同步至老库或新库 7.提前定义好一个阈值单号,比如当前数据已经累积到57亿,那我们可以根据增速估算一个阈值58亿。在上线后只有58亿以后的数据会通过mq同步至老库的。以前的数据都由worker同步(在适当时候:如目前单号已超过58亿,生成另外20w个任务,并开始执行另外两亿数据的同步,期间新库会缺少这两亿数据) 8.MQ同步稳定一段时间后,用新接口灰度替代老接口,在整体稳定后全量切换至新接口,并用MQ同步老库

Q:无限层级(每条记录都对应一个父id)怎样设计能够快速查询,之前设计是存一个path字段,用like查询,总感觉这样设计不优雅。

A:考虑使用关系型数据库中的递归查询(Recursive Query),如MySQL的WITH RECURSIVE语句、PostgreSQL中的WITH RECURSIVE语句。可快速地查询出某记录的所有子孙记录,同时避免了LIKE查询需扫描大量记录。


Q:热切开关具体什么方式实现?

A:一般编码实现。可对外暴露一个可供调用的接口或通过动态配置下发等方式触发。


Q:线上大表怎么在不影响业务的情况下增加字段? 我能想到的在从库增加字段,等从库同步追上来时,切换主从。

A:大表加字段过程中会锁表,期间所有写操作都会阻塞。若能接受业务停摆,直接加还是最方便。

开启双写和停止数据同步服务这一步需要停服或者锁库,不然会有数据不一致风险。


Q:双写阶段,会不会存在两份数据id不一致的情况(如MySQL自增ID),若下游有依赖这个表的ID,一旦切换就无法回退。很多公司的数仓表就是这样!

A:建议以一边的自增ID为准。


Q:既然有比对和补偿程序,能否不使用数据实时同步。先上线观察双写和补偿程序,没问题后,先进行数据从旧到新的快照复制,再开启双写,因有缝衔接而丢失的数据通过补偿程序来做?

A:考虑到很难实现一个完美对比补偿程序,推荐别这么做。


Q:现有系统的库存和订单耦合在一起,库存操作和订单状态更新在一个事务,现在要把库存迁出去作为一个独立服务。

组里方案:双写时发送RocketMQ事务消息,异步消费操作新库存。但后来发现库存操作对顺序有要求,如两个订单分别预占不同库位的库存,所以在消息中指定库位信息,消费时按库位跟新库存交互。

又遇到问题:双写和最终单写新库存的逻辑不一致,单写新库存预占库存的策略由库存控制,而非业务系统,所以担心全切过去后出现问题。

我现在想法:异步+修改调用规则(指定库位信息),这会导致单写新系统没有得到充分验证,不如开始时直接拆分事务做同步双写,对库存操作成功单据没更新状态的情况做监控报警,好处是调用逻辑完全一致,同步操作新库存出故障的风险通过开关和灰度来控制。有更好方案?

A:想法可行,可先同步双写验证新库存系统正确性,同时对库存操作成功单据没更新状态的情况进行监控报警,确保数据一致性。在验证通过后再逐步切换为异步+修改调用规则的方式,以降低同步操作新库存出故障的风险。

想避免同步操作新库存出故障风险,考虑Seata、TCC,避免双写和最终单写新库存的逻辑不一致问题,并保证数据一致性。但分布式事务也带来性能开销和复杂度,权衡!


Q:修改线上DB的表结构?原方案:建新表,把旧数据倒过去,然后新旧表分别改名。但数据一致问题较麻烦,改名过程中也易出错。

A:一般很少去删除表的字段,只增加字段。这样,新表能兼容旧表,也不用迁移数据。


Q:阿里云MySQL同步到RDS,需做啥?校验程序也必须?

A:考虑实施成本VS数据重要程度来决策。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 不停机更换数据库
    • 1.1 旧库数据复制到新库
    • 1.2 同步程序
      • 咋实现两个异构DB之间数据实时同步?
    • 1.3 上线【新订单服务】
    • 1.4 开启订单服务的双写开关,同时停掉同步程序
    • 1.5 持续检查
    • 1.6 灰度发布把读请求切到新库
    • 1.7 双写改为单写新库,同时停掉对比程序(不可逆)
  • 2 对比和补偿程序
    • 2.1 难度
    • 2.2 麻烦的是更通用case
  • 3 总结
  • FAQ
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档