跳至主要內容

Seata 分布式事务解决方案

Hirsuntech大约 20 分钟

Seata 分布式事务解决方案

Seata 是 一款分布式事务的解决方案,致力于在微服务架构下提供高性能且简单易用的分布式事务解决方案。

这里的微服务架构并不是唯一的限定,只要我们开发的底层框架是Spring Boot或者是SSM,都可以使用Seata来进行开发。

为什么会产生分布式事务

1734969345243.png

案例分析

  1. 访问电商平台:

    • 作为一个游客,他可能会访问某一个著名的电商平台。
    • 电商平台中因为规模较大,会按照公司的组织职能(如库管、会员管理、订单管理等)切分成多个子服务或子项目。
  2. 创建订单的过程:

    • 创建订单时,需要在订单库中新增一条订单数据。
    • 新增订单后,会员积分需要增加。
    • 在单个数据库中,这些操作可以在一个事务中完成。
  3. 分布式环境下的挑战: 涉及到三个独立的库时,如何保证所有的数据要么全局提交,要么全局回滚,这就是分布式事务需要解决的问题。

事务协调者的角色

  • 作为应用,需要有一个角色来下达提交或者回滚的指令。
  • 事务协调者在架构中作为统筹者,向其他服务下达请求提交或回滚的通知。

分布式事务的通用形式

1734969684047.png
1734969684047.png
  1. 请求的处理:

    • 外部请求进来后,事务协调者通知每个服务开启事务。
    • 各个模块(如订单服务、会员服务、库存服务)执行各自的操作,但不提交数据。
  2. 事务的提交:

    • 所有模块处理完毕并通知事务协调者。
    • 事务协调者发送提交指令,各模块提交数据,完成最终的数据库操作。

二阶段提交

  • 阶段一: 事务协调者发起数据操作请求,各服务独立处理但不提交数据。
  • 阶段二: 事务协调者收到完成报告后,下达提交命令,各服务进行数据提交。

阿里巴巴Seata的解决方案

分布式事务体系有三个重要角色

重要角色

TM(事务管理器):

  • 决定何时进行全局提交或回滚。
  • 定义事务的边界。
1734970245840.png
1734970245840.png

TC(事务协调者):

  • 负责通知命令的中间件Seata-Server。
  • 传达指令给各个服务。

RM(资源管理器): 执行具体操作(如数据库操作)。

TM 的实现

代码示例:

@GlobalTransactional
public void createOrder() {
    orderService.createOrder();
    pointsService.addPoints();
    stockService.reduceStock();
}
  • @GlobalTransactional注解开启全局事务。
  • 各个服务执行各自的操作。

RM 的实现

  • 订单服务

    • 创建订单,插入订单数据。
    • 使用 @Transactional 注解开启本地事务,完成数据操作后立刻提交。
  • 积分服务和 库存服务: 类似订单服务,分别更新积分和减少库存。

1734969345243.png

实质就是 三条 SQL 语句。

1734970535512.png
1734970535512.png
  • transactional 默认的规则就会将本地事务进行提交。
  • 也就是说,在第一阶段,在这个方法执行完了以后,数据就会立即写入到这个对应的订单库里边,之后它还会向server去上报好我这个订单服务的操作已经做好了。
1734970705120.png
1734970705120.png
  • 如果三个子事务都成功提交,则 TM 会告诉 TC 全局提交。
  • 任何一个提交失败,则 TM 告诉 TC 全局回滚。
1734971122968.png

特点:每个子事务提交成功,立刻写表

Seata AT 模式如何回滚

每个子事务提交成功,立刻写表,回滚区数据已经清空。既然如此,如何回滚?

Seata 设计的解决方案:seata 会在每个库中都增加一 个 UNDO_LOG 表。

Undo Log表:

  • 每个数据库中增加undo_log表,用于记录回滚日志。
  • 通过SQL解析生成反向操作的SQL语句。
1734972480202.png1734972557593.png1734972577316.png

seata 使用了 sql 解析的技术,生成逆向sql。

高并发环境下的处理

1734972676119.png
1734972676119.png

分布式锁: TC 为事务增加全局分布式锁,保证同一条数据的并发操作。

如何防止脏写

先来看一下使用 Seata AT 模式是怎么产生脏写的:

1734973229025.png
1734973229025.png

如何防止脏写?

方案一

业务二执行时加 @GlobalTransactional注解

1734973292037.png
1734973292037.png

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

方案二

业务二执行时加 @GlobalLock 注解

1734973351390.png
1734973351390.png

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

方法三

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

1734973574342.png
1734973574342.png

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

如果单单是 transactional,那么就有可能会出现脏写,根本原因是没有 Globallock 注解时,不会检查全局锁,这可能会导致另外一个全局事务回滚时,发现某个分支事务被脏写了。

所以加 select for update 也有个好处,就是可以重试。

如何防止脏读

Seata AT 模式的脏读是指在全局事务未提交前,被其它业务读到已提交的分支事务的数据,本质上是Seata默认的全局事务是读未提交。

那么怎么避免脏读现象呢?业务二查询 A 时加 @GlobalLock 注解 + select for update语句:

1734973793075.png
1734973793075.png

SELECT FOR UPDATE

SELECT FOR UPDATE 是一种用于数据库事务中的语句,主要用于在读取数据的同时锁定这些数据,以防止其他事务在读取期间对这些数据进行修改。

它通常用于需要确保数据一致性的场景,特别是在并发环境下。

特征和效果

  • 行级锁定SELECT FOR UPDATE 会对查询返回的行加上排他锁(exclusive lock),这意味着其他事务不能对这些行进行更新或删除操作,直到当前事务结束(提交或回滚)。
  • 防止脏读、不可重复读和幻读:通过锁定行,SELECT FOR UPDATE 可以防止其他事务在当前事务读取后但尚未提交前对这些行进行修改,从而避免了脏读、不可重复读和幻读问题。
  • 事务一致性:在事务中使用 SELECT FOR UPDATE 可以确保在事务提交之前,其他事务无法修改锁定的行,从而保证了数据的一致性。
  • 死锁风险:使用 SELECT FOR UPDATE 可能会增加死锁的风险,因为多个事务可能会相互等待对方释放锁。数据库系统通常会有机制来检测和处理死锁。
  • 性能影响:锁定行会影响并发性能,因为其他事务可能需要等待锁释放。这在高并发环境中尤其需要小心使用。
  • 阻塞机制:如果全局锁未释放,select for update 会阻塞,等待全局锁释放后再继续,提供了重试的机会。

总结

通过上述步骤和机制,Seata提供了一个高效且易用的分布式事务解决方案,适用于各种微服务架构。