代码改变世界

分布式事务学习与理解

2026-09-30 02:29  ajj40123  阅读(11)  评论(0)    收藏  举报

背景:在分布式系统中,由于各服务使用的数据库不一致,很难用单一数据库事务方式来解决事务问题

基本概念

  参与角色

    TC:事务协调者,协调管理全局事务

    TM:事务发起者,负责发起全局事务

    RM:事务参与者,负责最终执行本地事务

  提交模式

    两阶段提交(2PC):

      1.准备阶段:TC通知RM准备提交,并获取响应

      2.执行阶段

        所有RM已经准备好,TC发送提交事务通知,所有RM提交事务

        若某一RM未准备好,TC发送回滚通知,所有RM事务回滚

  事务分类:

    全局事务:由 TM 发起,包含多个分支事务的逻辑整体,对应一个唯一的 XID(分布式事务,包含所有的分支事务)。

    分支事务:某个微服务节点(RM)上的本地事务,是全局事务的最小执行单元(又称为本地事务,某个服务的本地数据库事务)。

  一致性分类:

    强一致性:读写强一致,写什么就读什么

    弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。

    最终一致性:弱一致性的升级版。系统会保证在一定时间内达到数据一致的状态。

解决方案

1. XA协议(数据库协议)

  • 每个事务参与者,执行本地事务操作,执行完成后,通知事务协调者,但是不提交

  • 汇总事务执行状态,根据结果处理

    • 所有参与者执行成功,通知所有参与者提交

    • 如果有某个参与者没有执行成功,通知所有参与者回滚

  • 优劣

    • 优点: 解决分布式事务的问题,强一致性

    • 缺点:存在事务挂起,资源空耗,性能低,可能导致锁表

 

2. TCC(Try-Confirm-Cancel)
被协调者有三个接口

  Try 预留或者锁定资源
  Confirm 确认执行
  Cancel 释放资源并逆向补偿
分为两阶段提交

  准备阶段:TC调用所有服务的Try接口
  提交阶段:
    TC根据结果,如果所有成功,则调用所有服务的Confirm接口
    如果存在失败,则调用所有服务的Cancel接口

优劣

  • 优点:没有事务挂起,没有性能问题;灵活度很高;

  • 缺点:

    • 过程复杂,业务接口拆分三个,增加工作量和复杂度;

    • 需要进行业务改造,工作量大,实现复杂

3. Seata AT模式
基于数据源 代理拦截 SQL,自动生成 undo_log,按全局事务状态提交或回滚

  AT 自动事务处理,Seata 创新的一种非侵入式的分布式事务解决方案
  实现方式
    在各个事务的数据库中建表 undo_log表
    第一阶段: 直接执行业务,seata代理数据源(DataSource),拦截sql执行,提取sql执行前后的数据镜像,将前后数据镜像转化成一条sql,加入到当前本地事务,执行提交,插入到undo_log表中
    第二阶段:
      如果所有参与者都处理成功,TC 协调所有参与者直接删除undo_log记录
      如果存在参与者处理失败,TC协调所有的参与者回滚,参与者从undo_log表中提取前后镜像,计算得到逆向补偿sql,执行sql实现补偿

优劣:

  • 优点:实现分布式事务;没有事务挂起,性能高;操作简单,工作量很小,免费开源;

  • 缺点:数据库会新增一张非系统表;灵活度不高;无法处理复杂事务(比如多表多关联条件处理的数据);

4. Saga

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

优劣:

  优点:适合长事务,无长时间锁

  缺点:补偿逻辑复杂

 

问题1:seata的AT模式全局事务执行过程

1.TM 在TC注册全局事务,TC 开启全局事务并生成 XID返回给TM。
2.TM将XID 透传到下游服务,各服务的 RM 识别到 XID 后注册分支事务。
3.一阶段执行本地事务:RM 拦截 SQL,查询前镜像,执行业务 SQL,查询后镜像,生成 undo_log,将业务数据和 undo_log 在同一个本地事务中提交。
4.提交前获取全局锁:RM 在本地事务提交前向 TC 申请全局锁,获取成功后提交本地事务,释放本地数据库锁,获取失败则回滚本地事务并重试或超时放弃。

5.二阶段段处理

  二阶段提交:若全局事务提交,TC 通知各 RM 异步删除 undo_log,过程很快。
  二阶段回滚:若全局事务回滚,TC 通知 RM 根据 undo_log 的前后镜像生成反向 sql,执行sql实现补偿。

问题2:seata的AT模式如何实现写隔离?全局锁与本地锁的区别?

举例:

脏写产生:

业务一开启全局事务,其中包含分支事务A(修改 A)和分支事务 B(修改 B)

业务二修改 A,其中业务一执行分支事务 A 先获取本地锁

业务二则等待业务一执行完分支事务 A 之后

获得本地锁修改 A 并入库

业务一在执行分支事务时发生异常了

由于分支事务 A 的数据被业务二修改

导致业务一的全局事务无法回滚。

 

解决办法:

1.业务二执行时加 @GlobalTransactional注解

业务二在执行全局事务过程中,分支事务 A 提交前注册分支事务获取全局锁时,发现业务业务一全局锁还没执行完,因此业务二提交不了,抛异常回滚,所以不会发生脏写。

2.业务二执行时加 @GlobalLock注解:

与 @GlobalTransactional注解效果类似,只不过不需要开启全局事务,只在本地事务提交前,检查全局锁是否存在。

3.业务二执行时加 @GlobalLock 注解 + select for update语句。

如果加了select for update语句,则会在 update 前检查全局锁是否存在,只有当全局锁释放之后,业务二才能开始执行 updateA 操作

 

本地锁和全局锁区别

 
特性全局锁 (Global Lock)本地锁 (Local Lock)
作用范围 分布式事务中跨多个分支事务 单个本地事务内的数据操作
实现方式 基于数据库唯一约束的逻辑锁,由 TC 管理 数据库行锁或事务内隐式锁(如 SELECT FOR UPDATE)
目标 防止多个全局事务同时修改同一条数据,避免写-写冲突 保证事务执行期间前/后镜像与业务 SQL 的一致性,防止脏写
使用场景 多服务并发修改同一数据,如库存扣减、秒杀场景 单表操作或事务内部 SQL 执行期间,确保 undo_log 正确生成
风险 高并发下可能导致锁等待或死锁,需要合理控制事务粒度 若锁释放不及时,可能阻塞本地事务或影响性能

 

问题3:缓存与数据库不同步,如何解决?

自答:

1.手工清缓存

2.实时清理缓存:准备一张缓存表,当数据变动时,在缓存表添加记录;实时监控缓存表,若有数据变化,获取缓存数据,执行缓存清理操作

参考答案:

1.同步删除:更新请求→更新数据库→删除缓存

2.延迟双删:更新请求→删除缓存→更新数据库→删除缓存

3.监听binlog删除+重试:更新请求→更新数据库→缓存管理服务订阅binlog→删除缓存消息推送至消息队列→删除缓存→失败重试