保障分库分表后的数据一致性,核心在于应对跨库/跨表操作带来的挑战,主要围绕分布式事务和数据迁移/同步两大场景展开。
一、分布式事务一致性保障
当业务操作涉及多个分片(库或表)的更新时,需要分布式事务来保证 ACID 特性。主要方案对比如下:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| XA 协议 (2PC) | 两阶段提交,由事务管理器协调所有资源管理器(数据库),经历准备和提交/回滚两个阶段。 | 强一致性,原生数据库支持(如 MySQL XA)。 | 同步阻塞,性能低,依赖数据库XA实现,协调者单点故障。 | 对一致性要求极高,且吞吐量不高的内部业务。 |
| TCC (Try-Confirm-Cancel) | 业务层面实现的二阶段提交,每个阶段由业务代码提供对应操作(Try预留资源,Confirm确认,Cancel取消)。 | 性能较好,可跨异构数据源,避免了长事务锁。 | 业务侵入性强,开发复杂,需实现所有服务的三个接口。 | 高并发、对一致性有要求、且业务可清晰定义Try/Confirm/Cancel的短流程。 |
| 基于消息的最终一致性 (本地消息表、事务消息) | 利用消息队列的可靠性,将分布式事务拆分为本地事务和异步消息。 | 吞吐量高,业务侵入性相对较低,系统解耦。 | 只能保证最终一致性,存在延迟,需处理消息幂等和防丢。 | 跨系统、对实时一致性要求不高的场景,如订单创建后发券、扣减库存后通知。 |
| AT 模式 (Seata) | 一种无侵入的分布式事务解决方案,通过拦截 SQL 自动生成回滚日志(undo_log),在提交时异步删除,失败时自动回滚。 | 对业务代码几乎零侵入,使用简单。 | 依赖全局锁,在高并发热点数据场景可能有性能瓶颈,需部署额外组件。 | 希望快速引入分布式事务,且业务逻辑非极端高并发的 Java 应用。 |
代码示例 (基于消息的最终一致性本地消息表):
-- 1. 在业务数据库中创建本地消息表 CREATE TABLE local_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_id VARCHAR(64) NOT NULL COMMENT '业务ID,如订单号', biz_type VARCHAR(32) NOT NULL COMMENT '业务类型', content TEXT COMMENT '消息内容', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-待发送, 1-已发送, 2-已确认', retry_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_status (status), INDEX idx_biz (biz_type, biz_id) );// 2. 业务服务伪代码 (以创建订单并扣减库存为例) @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private LocalMessageMapper messageMapper; @Autowired private MQProducer mqProducer; @Transactional public void createOrder(OrderDTO orderDTO) { // 1. 本地事务:创建订单(主库操作) Order order = convertToOrder(orderDTO); orderMapper.insert(order); // 2. 在同一事务中,插入本地消息记录 LocalMessage message = new LocalMessage(); message.setBizId(order.getOrderNo()); message.setBizType("ORDER_CREATED"); message.setContent(JSON.toJSONString(order)); message.setStatus(0); messageMapper.insert(message); // // 3. 事务提交后,由定时任务扫描本地消息表,发送MQ // 库存服务消费MQ,执行扣减库存操作。若失败,消息会重试。 } }二、数据迁移与同步期间的一致性保障
在分库分表扩容或数据重整时,需保证迁移过程中业务不间断且数据不丢失、不错乱。核心策略是平滑迁移与双写。
典型双写迁移流程:
- 全量迁移:将历史数据从旧库(单库单表)同步到新分片库表。
- 增量同步:使用 Canal、Debezium 等工具监听旧库的 binlog,实时将增量变更同步到新库,追平数据。
- 开启双写:应用改造,对数据的增删改操作同时写入旧库和新分片库。此阶段需处理写冲突和保证幂等。
- 数据校验与补偿:对比新旧库数据差异,通过校验脚本发现不一致,并触发补偿修复。
- 流量切换:逐步将读流量和写流量切至新库,最终下线旧库写操作。
关键一致性措施:
- 幂等性设计:双写和补偿操作必须幂等,防止重复操作导致数据错误。
// 示例:基于唯一业务键实现插入幂等 public void insertOrderSharding(Order order) { // 先尝试查询,或使用 INSERT ... ON DUPLICATE KEY UPDATE if (orderMapper.selectByOrderNo(order.getOrderNo(), order.getShardKey()) == null) { orderMapper.insert(order); } } - 数据校验:定期全量或抽样对比新旧库数据,可使用 checksum 或逐字段对比。
- 可逆与回滚:迁移方案必须设计回滚链路,在出现问题时能快速切回旧库。
三、日常运维与设计层面的保障
- 全局唯一有序ID:使用雪花算法(Snowflake)、号段模式等生成分布式ID,避免因自增ID冲突导致数据错乱。
- 避免跨分片事务:从业务设计上,尽量让一个事务内的操作落在同一个分片内。例如,将关联紧密的数据(如用户和其订单)通过相同的分片键(
user_id)路由到同一分片。 - 最终一致性补偿:对于无法避免的跨分片弱一致性操作,建立对账系统,定期扫描并修复不一致的数据。
参考来源
- MySQL 分库分表实战:Sharding-JDBC 动态扩容与数据一致性保障
- 一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
- 八股已死、场景当立(分库分表篇)
- 社交系统用户关系分库分表实战:数据迁移与同步
- 【资深架构师亲授】:PHP分库分表数据迁移的7大核心策略