数据被切开之后,事务怎么办
当一张表的数据分散在多个节点上,一个事务可能同时修改多个节点的数据。这时单机数据库的本地锁与日志机制就不够用了:必须保证这些修改要么全部生效,要么全部不生效,并且对并发的其他事务呈现一致的可见性。
分布式数据库普遍采用两阶段提交来解决原子性问题,再配合一套全局时间戳机制来解决可见性问题。
两阶段提交的基本流程
- 准备阶段:事务协调者把写操作发往各参与节点,参与节点在本地写入数据但暂不使其对外可见,同时锁定相关记录;
- 提交阶段:如果所有参与节点都准备成功,协调者决定提交,并通知各节点把修改正式生效并释放锁;任一节点准备失败则整体回滚。
这套流程的关键在于:提交决定一旦下达就不可撤销,因此协调者必须在做出决定前把决定持久化。若协调者在提交阶段中途故障,恢复后要能依据持久化的决定把未完成的分支推进到终态。
全局时间戳与可见性
两阶段提交解决了「改不改」的问题,还需要解决「什么时候能被看到」的问题。以 TiKV 采用的 Percolator 模型为例:
- 每个事务开始时获取一个全局唯一的开始时间戳,所有读取都基于该时间戳判断版本可见性;
- 事务写入的数据带有这个开始时间戳作为版本标记;
- 提交时再获取一个提交时间戳,只有提交时间戳早于读取时间戳的数据版本才会对读者可见。
由于时间戳由全局授时服务统一分配,不同节点上的事务获得了全序关系,从而可以推断出任意两个事务的先后,这正是快照隔离得以成立的基础。读取操作看不到未提交的数据,也不会因为提交顺序不同而出现中间态。
冲突与重试
并发事务之间不可避免会冲突。常见处理策略是冲突检测加有限重试:
- 写写冲突:两个事务修改同一行时,后到者要么等待锁,要么直接失败并回滚重试;
- 读写冲突:读事务遇到正在提交的版本时,需要根据提交时间戳判断该版本是否可见;
- 重试策略:应用层需要能正确处理事务被回滚的情况,重试应带退避,避免冲突加剧。
重要提醒:分布式事务的成本远高于单机事务。应用层如果不做适配,把原本依赖长事务的逻辑直接搬过来,性能会明显下降。
大事务与长事务是主要风险源
在分布式环境下,事务的代价与它涉及的数据量和持续时间强相关:
- 长事务会长时间持有锁,阻塞其他事务,还可能推迟垃圾回收,导致历史版本堆积;
- 大事务需要维护大量锁信息与写记录,内存占用高,一旦失败回滚代价巨大;
- 批量导入、批量更新这类操作应当拆分为小批次提交,而不是放在一个事务里;
- 需要监控运行时间过长与影响行数过多的事务,把它们作为治理对象。
应用侧的设计建议
- 尽量缩短事务范围,把不涉及数据一致性的逻辑(如远程调用、文件处理)移到事务之外;
- 避免在事务中做用户交互式的等待,这会直接把长事务风险引入系统;
- 为事务失败准备幂等重试逻辑,确保重放不会产生重复效果;
- 对强一致要求不高的场景,考虑放宽隔离级别或改用最终一致方案,换取更高吞吐。
| 原子提交 | 两阶段提交,准备阶段写数据并加锁,提交阶段统一生效或回滚 |
|---|---|
| 授时机制 | 全局时间戳服务统一分配开始与提交时间戳,建立事务全序关系 |
| 可见性 | 基于提交时间戳与读取时间戳比较,确保读不到未提交数据 |
| 冲突处理 | 锁等待或失败重试,应用层需具备幂等重试能力 |
| 主要风险 | 长事务持锁阻塞、大事务内存与回滚代价、历史版本堆积影响回收 |