来源 | 君哥聊技术(ID:gh_1f109b82d301)使用 MySQL 时有时候会遇到数据不一致的问题。那一般是什么原因会导致数据不一致呢?今天来聊一聊这个话题。
1.事务失效
1.1 单体事务
事务失效是造成数据不一致的一种常见情况。举一个例子,客户下订单后,订单表插入一条记录,插入成功,账户表扣减金额,但扣减金额失败。如下图:

假如这两张表在同一个库,正常流程是回滚事务,但因为事务失效,导致订单表插入成功,账户表扣减金额失败,最终数据不一致。
可能造成事务失效的因素有很多,比如下面集中情况:
- Java 代码开发中事务管理多数都由 Spring 来管理,但有些情况下 Spring 管理事务会失效,比如事务所在方法的定义上有 private、final、static,或者事务方法是一个内部方法,因为 Spring 无法创建代理;
- 异常被捕获后没有抛出,或者抛出的异常不是 Spring 事务管理的异常,造成事务回滚失败(Spring 默认回滚 RuntimeException)。
1.2 隔离级别
虽然事务没有失效,但是因为事务隔离级别的问题,出现了脏读、不可重复读和幻读等问题。比如读取了其他未提交事务修改的数据,如果那个事务最终回滚,读取到的就是脏数据,导致不一致。
要解决这个问题,就需要根据业务需求选择合适的隔离级别。
1.3 分布式事务
还是用上面客户下单的例子,如果系统是分布式架构,订单服务和账户服务不在一个应用中,那账户服务扣减金额失败了,订单服务无法回滚。

这种情况可以引入分布式事务框架,比如 Seata 或者 RocketMQ 的分布式事务。
2.数据同步
数据同步失败或未完成,也可能会造成数据不一致。
2.1 数据未同步
如下图是一个读写分离的主备架构,应用写主库,然后从备库中读,如果数据未同步完成,就会造成问题。

对一些数据敏感的场景,可以选择从主库读取数据。
2.2 数据同步失败
再看下面双主架构的例子,假如我们有一张表,里面定义了自增主键 id,库 M1 生成了主键 1、2、3,库 M2 也生成了主键 1、2、3,这样两边同步的时候因为主键冲突,同步失败。

要解决这个问题,可以把两个主库的起始 id 值设置为不一样(比如 M1 id 起始值设置成 1,把步长设置成1,M2 起始值设置成 2,把步长设置成 2)。如下图:
3.系统故障
3.1 主库奔溃
在主备架构中,如果主库发生奔溃,InnoDB 利用重做日志进行崩溃恢复,这时要保证已提交事务的数据持久化以及未提交事务的数据回滚。这个过程有可能会造成数据不一致。
3.2 主备切换
如果发生主备切换,备库还有部分数据没有同步到新主库,这时应用来读取数据就会出现数据不一致。
4.数据备份
在数据备份和恢复过程中,可能没有停止业务,有部分业务还在写入。备份完成后做数据恢复时,后写入的业务数据就会丢失,造成数据不一致。
5.应用逻辑问题
应用程序的逻辑有问题,也可能会造成数据不一致。下面列举两个场景:
- 多个线程对同一条数据进行操作,在应用中没有采用加锁机制,导致出现数据不一致;