Seata的AT事务模式

发布于 2022-04-30  2.4k 次阅读


一,Seata的基础理论

  • TC(Transaction Coordinator) -事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚
  • TM(Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务,提交或回滚全局事务
  • BM(Transaction Manager) - 资源管理器:管理分支事务处理的资源,与TC交互以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

其中,TC需要单独部署的Server服务端,TM和RM为嵌入到应用的Client客户端

在Seata中,一个分布式事务的生命周期如下:

 

  1. TM请求TC开启一个全局事务,TC会生成一个XID作为该全局事务的编号,XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起
  2. RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联
  3. TM请求TC告诉XID对应的全局事务是进行提交还是回滚
  4. TC驱动RM们将XID对应的自己的本地事务进行提交还是回滚

在搭建TC时需要存储事务会话信息,使用DB进行存储,需要导入三张表

  • branch_table:存储分支事务(也就是事务参与者)
  • global_table:存储全局事务信息
  • lock_table:存储全局锁信息

二,Seata的AT事务模式

 AT模式是一种无侵入式的分布式事务解决方案, Seata默认使用的是AT模式

  在AT模式下,用户只需要关注自己的"业务SQL",用户的"业务SQL"作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚

一,AT模式的两阶段

一阶段准备:

  在一阶段,Seata会拦截业务SQL,首先解析SQL语义,找到"业务SQL"要更新的业务数据,在业务数据被更新前将其保存成"before image",然后执行"业务SQL"更新业务数据,在数据更新之后,再将其保存成"after image",最后生成行锁,以上操作全部在一个数据事务内完成,这样保证了一阶段操作的原子性

二阶段提交:

二阶段如果是提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需要将一阶段的快照数据和行锁删除掉,完成数据清理即可

二阶段回滚:

  二阶段如果要回滚,Seata就需要回滚一阶段已经执行的"业务SQL",还原业务数据,回滚方式便是用"Before image"还原业务数据,但在还原前要首先要效验脏读,对比"数据库当前业务数据"和"after amage",如果两份数据完全一致就说明没有脏读,可以还原业务数据,如果不一致说明脏读,出现脏读就需要人工处理

二,AT事务的读写隔

一,写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 
  • 拿不到 全局锁 ,不能提交本地事务
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

二,读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

三,案例演示

详细的代码有点多不进行描述了,总体就是订单服务和库存服务,订单服务调用库存服务

① order

//controller
@RestController
public class OrderController {

    @Autowired
    OrderServiceImpl orderService;

    @GetMapping("/OrderController")
    public String OrderController(orderpojo orderpojo){

        boolean b = orderService.setOrder(orderpojo);
        if(b){

            return "订单创建成功!!";
        }
        return "订单创建失败!!";
    }
}
//service
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    orderDao orderDao;

    @Autowired
    orderfeign orderfeign;//openfign远程调用

    @Override
    @GlobalTransactional
    public boolean setOrder(orderpojo orderpojo) {


        boolean b = orderDao.insert_order(orderpojo);
        orderfeign.dw(orderpojo.getProductId());
        int a= 10/0;
        return b;
    }
}

② stock

@RestController
public class StockController {

    @Autowired
    StockServiceImpl stockService;

    @GetMapping("/StockConoller/{c}")
    public void StockConoller(@PathVariable("c") int c){

        boolean b = stockService.setStock(c);
    }
}

端点debug演示:

① global_table

② branch_table

③ lock_table

④undo_log

 


路漫漫其修远兮,吾将上下而求索