数据被切开之后,事务怎么办

当一张表的数据分散在多个节点上,一个事务可能同时修改多个节点的数据。这时单机数据库的本地锁与日志机制就不够用了:必须保证这些修改要么全部生效,要么全部不生效,并且对并发的其他事务呈现一致的可见性。

分布式数据库普遍采用两阶段提交来解决原子性问题,再配合一套全局时间戳机制来解决可见性问题。

两阶段提交的基本流程

  1. 准备阶段:事务协调者把写操作发往各参与节点,参与节点在本地写入数据但暂不使其对外可见,同时锁定相关记录;
  2. 提交阶段:如果所有参与节点都准备成功,协调者决定提交,并通知各节点把修改正式生效并释放锁;任一节点准备失败则整体回滚。

这套流程的关键在于:提交决定一旦下达就不可撤销,因此协调者必须在做出决定前把决定持久化。若协调者在提交阶段中途故障,恢复后要能依据持久化的决定把未完成的分支推进到终态。

全局时间戳与可见性

两阶段提交解决了「改不改」的问题,还需要解决「什么时候能被看到」的问题。以 TiKV 采用的 Percolator 模型为例:

  • 每个事务开始时获取一个全局唯一的开始时间戳,所有读取都基于该时间戳判断版本可见性;
  • 事务写入的数据带有这个开始时间戳作为版本标记;
  • 提交时再获取一个提交时间戳,只有提交时间戳早于读取时间戳的数据版本才会对读者可见。

由于时间戳由全局授时服务统一分配,不同节点上的事务获得了全序关系,从而可以推断出任意两个事务的先后,这正是快照隔离得以成立的基础。读取操作看不到未提交的数据,也不会因为提交顺序不同而出现中间态。

冲突与重试

并发事务之间不可避免会冲突。常见处理策略是冲突检测加有限重试:

  • 写写冲突:两个事务修改同一行时,后到者要么等待锁,要么直接失败并回滚重试;
  • 读写冲突:读事务遇到正在提交的版本时,需要根据提交时间戳判断该版本是否可见;
  • 重试策略:应用层需要能正确处理事务被回滚的情况,重试应带退避,避免冲突加剧。
重要提醒:分布式事务的成本远高于单机事务。应用层如果不做适配,把原本依赖长事务的逻辑直接搬过来,性能会明显下降。

大事务与长事务是主要风险源

在分布式环境下,事务的代价与它涉及的数据量和持续时间强相关:

  • 长事务会长时间持有锁,阻塞其他事务,还可能推迟垃圾回收,导致历史版本堆积;
  • 大事务需要维护大量锁信息与写记录,内存占用高,一旦失败回滚代价巨大;
  • 批量导入、批量更新这类操作应当拆分为小批次提交,而不是放在一个事务里;
  • 需要监控运行时间过长与影响行数过多的事务,把它们作为治理对象。

应用侧的设计建议

  1. 尽量缩短事务范围,把不涉及数据一致性的逻辑(如远程调用、文件处理)移到事务之外;
  2. 避免在事务中做用户交互式的等待,这会直接把长事务风险引入系统;
  3. 为事务失败准备幂等重试逻辑,确保重放不会产生重复效果;
  4. 对强一致要求不高的场景,考虑放宽隔离级别或改用最终一致方案,换取更高吞吐。
事务机制要点
原子提交两阶段提交,准备阶段写数据并加锁,提交阶段统一生效或回滚
授时机制全局时间戳服务统一分配开始与提交时间戳,建立事务全序关系
可见性基于提交时间戳与读取时间戳比较,确保读不到未提交数据
冲突处理锁等待或失败重试,应用层需具备幂等重试能力
主要风险长事务持锁阻塞、大事务内存与回滚代价、历史版本堆积影响回收