数据会有短暂的不一致
来源:6-4 结合商城下单场景下分析Seata AT模式流程

慕无忌1797345
2023-01-15
各个微服务分别执行了自己的事务并提交到数据库,最后如果TC的反馈是回滚的话就使用undolog表里面的数据还原回去,那这样子的话服务之间的数据会有短暂的不一致是吗?
1回答
-
大能老师
2023-01-15
一、AT 的原理二、整体机制三、全局锁四、写隔离&读隔离1、写隔离2、读隔离五、AT 二阶段&校验脏写六、既然有读写隔离和全局锁,为什么还有设计脏数据回滚校验七、脏数据回滚失败如何处理?
一、AT 的原理AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
二、整体机制
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
三、全局锁
注册分支事务时,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 需要重试等待 **全局锁** 。
tx1 二阶段全局提交,释放 **全局锁** 。tx2 拿到 **全局锁** 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 **全局锁**,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 **全局锁** 等锁超时,放弃 **全局锁** 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 **全局锁** 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 **脏写** 的问题。
2、 读隔离
在数据库本地事务隔离级别 **读已提交(Read Committed)** 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 **读未提交(Read Uncommitted)** 。
如果应用在特定场景下,必需要求全局的 **读已提交** ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
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
00
相似问题