数据会有短暂的不一致

来源:6-4 结合商城下单场景下分析Seata AT模式流程

慕无忌1797345

2023-01-15

各个微服务分别执行了自己的事务并提交到数据库,最后如果TC的反馈是回滚的话就使用undolog表里面的数据还原回去,那这样子的话服务之间的数据会有短暂的不一致是吗?

写回答

1回答

大能老师

2023-01-15

        一、AT 的原理二、整体机制三、全局锁四、写隔离&读隔离1、写隔离2、读隔离五、AT 二阶段&校验脏写六、既然有读写隔离和全局锁,为什么还有设计脏数据回滚校验七、脏数据回滚失败如何处理?
一、AT 的原理

AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

https://img.mukewang.com/szimg/63c4008309ead1e207000320.jpg

二、整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。

    • 回滚通过一阶段的回滚日志进行反向补偿。

三、全局锁

注册分支事务时,client会把所有的LockKey 拼到一起作为全局锁发送给Seata-server。

Seata 全局锁的设计是为了什么? 

以扣减库存场景为例,TX1 完成库存扣减的一阶段,库存从100扣减为99,正在等待二阶段的通知。TX2也要扣减同一商品的库存,如果没有全局锁的限制,TX2库存从99扣减为98,这时如果TX1接收到回滚通知,进行回滚把库存从98回滚到100。因为没有全局锁,造成了**脏写**

另外全局锁的引入也是 at 性能并不是那么的优秀的原因。例如引入全局锁,解决数据不会发生脏写的问题,Seata at牺牲了业务的并发能力。在非常要求性能的场景,可能还是需要考虑TCC,SAGA,可靠消息等方案

四、写隔离&读隔离

1、写隔离

  • 一阶段本地事务提交前,需要确保先拿到 **全局锁** 。

  • 拿不到 **全局锁** ,不能提交本地事务。

  • 拿 **全局锁** 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

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

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

https://img.mukewang.com/szimg/63c400a409885cf114581066.jpg

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

https://img.mukewang.com/szimg/63c400b509fe93b107180521.jpg

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

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

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

2、 读隔离

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

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

https://img.mukewang.com/szimg/603e18fb09fbf71207240521.jpg

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

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


五、 AT 二阶段&校验脏写

二阶段是完全异步化的并且完全由Seata控制,Seata根据所有事务参与者的提交情况决定二阶段的处理

  • 如果所有事务提交成功,则二阶段的任务就是删除一阶段生成 的undoLog,并释放**全局锁**

  • 如果部分事务参与者提交失败,则需要根据undoLog对已经注册的事务分支进行回滚,并释放**全局锁**

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


六、 既然有读写隔离和全局锁,为什么还有设计脏数据回滚校验


正常情况下读写隔离和全局锁的设计,可以保证 seata AT 模式下两个事务直接的数据一致性,也可以避免脏写和二阶段回滚时候校验出脏写的问题。

事务1执行完毕业务sql之后,会获取全局锁,提交完事务之后,将DB锁释放掉。事务2进行业务sql执行,尝试获取全局锁,但此时全局锁由事务1持有。假设此时,事务1发生了异常需要回滚,事务1会尝试获取DB锁,但这时候的DB锁被事务2持有,于是就出现了,事务1等待事务2释放DB锁,事务2等待事务1释放全局锁,形成了死锁关系。在AT模式下,尝试获取全局锁会默认重试30次,每10ms进行一次重试,当死锁发生了,事务2长时间获取不到全局锁,任务就会超时,事务2会进行回滚并释放DB锁。也就是说事务2的操作失败了,此时事务1可以重新拿到DB锁进行快照恢复,money重新回到了100,恢复快照之后,事务1释放全局锁即可。**此时事务2并没有执行成功,保证了数据的一致性**。

⭐️那么读写隔离+全局锁的情况下什么时候还会发生脏写呢?


一个事务被Seata管理,另一个非Seata管理。

上面例子,事务1在释放了DB锁之后,事务2开始执行。事务2执行完SQL之后,由于其并非由Seata管理,所以不需要获取全局锁,直接提交事务释放DB锁。我们依旧假设此时事务1发生异常需要回滚,按照数据快照来看,事务2执行完毕了,此时如果直接回滚,那么又发生了脏写问题。 为了防止这个问题,Seata在保存快照时实际上会记录2份快照,一份是修改之前的快照,一份是修改之后的快照,在恢复快照数据时,会将更新后的快照值和当前数据库的实际值进行比对(类似CAS过程),如果数值不匹配则说明在此期间有另外的事务(也就是事务 2)修改了数据,此时直接释放全局锁,事务1记录异常,发送告警信息让人工介入。如果一致则恢复数据,释放全局锁即可。

七、脏数据回滚失败如何处理?


A:来自官方文档的答案-[Seata常见问题]https://seata.io/zh-cn/docs/overview/faq.html#5

1. 脏数据需手动处理,根据日志提示修正数据或者将对应undo删除(可自定义实现FailureHandler做邮件通知或其他)

2. 关闭回滚时undo镜像校验,不推荐该方案。

注:建议事前做好隔离保证无脏数据

[参考文献]

https://cloud.tencent.com/developer/article/1928386

https://seata.io/zh-cn/docs/dev/mode/at-mode.html

https://zhuanlan.zhihu.com/p/314991447

https://seata.io/zh-cn/docs/overview/faq.html#5


0
0

Java分布式架构设计与开发实战

项目贯穿式讲解,真正将理论与实战相结合

325 学习 · 74 问题

查看课程