事务想必大家都有所了解,回想我当初面试的时候几乎次次都会被问到,重要性可想而知了。那当事务遇到微服务架构,变成分布式事务你又了解多少呢?让我们一起来看看什么是分布式事务以及常见的分布式事务的解决方案吧!
事务
先来回顾一下事务的概念,便于我们过渡到分布式事务。
事务的基本特性
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。
-
原子性(Atomicity)
事务要么全部完成,要么全部取消。 如果事务崩溃,状态回到事务之前(事务回滚)。例子:A转账给B 50 元, 事务1 A账户扣除50元,事务2 B账户增加50元。事务1,2要么同时成功,要么同时失败。如果事务1成功,事务2失败需要将两个事务都失败回滚。
-
一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。还是上一个例子,如果A账户有100元,B账户有100元。那个事务1,事务2发生之前 总余额为200元,当事务执行之后A账户余额50,B账户余额为150元,总余额还是200元。
-
隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。假设A账户有100元转给B账户50元,C账户转给A账户100元。这时候单独分析 A账户需要执行 事务1 扣除50元,事务2 增加100元。在数据库处理这两个事务时候,为了让它们互不影响将它们顺序执行,可以先执行事务1也可以先执行事务2满足互不影响。
-
持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。简单理解当你转账操作结束了(A账户余额50,B账户余额为150元),结果银行断电了(导致数据库系统关闭)。当重新恢复正常的时候,你再去看A账户余额还是50,B账户余额还是为150元。
事务的隔离级别
为什么要有事务隔离级别,因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。
-
读未提交
两个并发的事务不能同时写,但是可以一个事务写,另一个事务读。
还是举例子画图吧,假设账户A余额m = 100,这时候有两个事务操作m分别是事务A 要在m余额上增加50元,事务B要在m余额上扣除100元
这样做的好处,如果事务A和事务B各需要3s时间分别是1s读取余额,1秒执行,1秒提交事务。那么读未提交同时执行AB事务仅需要5秒。
脏读问题:
从图上可以看出,读未提交存在脏读问题。当事务B读取事务A未提交的数据后,事务A发生了回滚。最后结果是m = 50元,不满足事务一致性。
-
读已提交
一个写事务操作的数据将会禁止其他事物访问该数据(无论是读还是写)。
继续举例子画图,例子同上 假设账户A余额m = 100,这时候有两个事务操作m分别是事务A 要在m余额上增加50元,事务B要在m余额上扣除100元
解决了脏读的问题。
不可重复读问题:
事务A读取余额m之后做一些其他操作,然后在读取余额m,事务B扣除余额m的100元。
在上图可以看到我们在事务A读取两次相同的数据余额m,但是结果确出现不同的结果,这就叫做不可重复读。
但有时候业务需求一个事务中访问同一个数据无论访问几次,其他事务是否已经提交新的数据,我只想访问到刚开始的数据。也就是说在一个事务内重复的读余额m,想要查询的值是相同的。 -
可重复读
如果一个事务成功执行并且添加了新数据(事务提交),这些数据对其他正在执行的事务是可见的。但是如果事务成功修改了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在新数据方面突破了隔离,对已存在的数据仍旧隔离。
幻读问题:
当事务A第一次搜索时候只搜索出来 m=100,这时候事务B新增一列n=0,并且作了一系列操作n=100.这时候事务A再次搜索时候除了m=100,还多搜索出发现多一列n=100。这就叫做幻读。 -
串行化
最高级别的隔离。其实就是顺序执行,那么也不会引起任何并发的问题,事务依次有序的执行。这里就不画图了,举个例子就是 事务A和事务B同时操作同一个数据,数据库不会让它们同时执行,会依次执行先执行A再执行B或者先执行B在执行A。这样就不存在脏读,不可重复读,幻读问题。
分布式事务
它就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成,这些事务要么都成功提交,要么都回滚。
对于微服务架构下的系统,跨服务或者跨数据库均会产生多个事务,即多个事务各自独立无法形成一个整体的事务单元。
常见的分布式事务解决方案
-
2PC
2PC(Two-phase commit protocol),中文名称二阶段提交,二阶段分别指的是准备和提交两个阶段。
这里引入两个概念:
RM(Resource Manager):用于直接执行本地事务的提交和回滚。在分布式集群中,一台MySQL服务器就是一个RM。
TM(Transaction Manager):TM是分布式事务的核心管理者。事务管理器与每个RM进行通信,协调并完成分布式事务的处理。发起一个分布式事务的MySQL客户端就是一个TM。
2PC 引入一个事务协调者的角色(TM)来协调管理各参与者(也可称之为各本地资源RM)的提交和回滚。
- 阶段一为准备(prepare)阶段。即所有的RM锁住需要的资源,在本地执行这个事务(执行sql,写redo/undo log等),但不提交,然后向Transaction Manager报告已准备就绪。
- 阶段二为提交阶段(commit)。当Transaction Manager确认所有参与者都ready后,向所有参与者发送commit命令。接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。
举个例子把TM比作领导,而RM则是我们员工。当团队需要一起完成一件事时候,
准备阶段:领导将工作分配给每个员工,并且让他们去调研是否能完成,等到所有员工调研结束并都回复
提交阶段:领导发号施令给每个员工说 开工,这时候全体员工进行工作,工作完成后回复领导,等全部成员完成任务结束。存在的问题:
- 资源占用的问题,2PC过程中会长时间的占用资源(加锁)直到两阶段提交完 成才释放资源。
- 单点故障问题:如果事务协调者在发起全局提交或者全局回顾之前(TC)宕机了,那么资源将会一直被占用,且参与者会一直等待,无法完成其他操作。
- 数据一致性问题:如果TC网络故障导致部分参与者未收到全局提交的指令,将会造成系统的数据不一致
-
TCC
TCC(Try,Confirm,Cancel),中文名称 补偿型事务。相比于2PC而言,TCC属于业务层面的分布式事务,而2PC是数据库层面。- Try 指的是预留,即资源的预留和锁定,注意是预留。
- Confirm 指的是确认操作,这一步其实就是真正的执行了。
- Cancel指的是撤销操作,可以理解为把预留阶段的动作撤销了。
其实从思想上和2PC差不多,也是想试探性执行,如果可以就真正的执行,如果不行就回滚。也是两个阶段 try 如果可以Confirm提交 ,如果不行Cancel回滚。
存在的问题:
操作和撤销阶段都可能会失败,失败如果发生重试还需要保证业务接口的幂等性。
需要开发人员编写大量的补偿代码,增加工作量,且一不小心就很容易出错。 -
本地消息表
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。然后再去调用下一个操作,如果下一个操作调用成功了,消息表的消息状态可以直接改成已成功。
在DB A中维护一个本地消息的表,保存写业务数据到DBA和写业务数据到DBB两个操作数据信息。然后写业务数据到DBA和写业务数据到DBB两个操作,哪个事务执行成功,那么在表中修改对应事务的状态为完成。利用Kafka读取表中未完成的事务进行重试。
可以理解本地消息表为便利签,表中数据是要完成的任务。按照便利签上的任务顺序去做任务,做完我们打一个对号,如果没完成不改动。当我们做完一遍便利签任务在看看便利签上是否还有未打对号的任务(操作失败的事务),如果存在重试再去做任务。
存在的问题:
1、最终一致性,导致脏读问题。
2、需要手动处理回滚方案(定时任务去重试或者回滚),复杂性较高
3、产生分布式事务的场景,需要加入特定场景的MQ消费者 -
MQ事务消息(RocketMQ)
利用RocketMQ消息实现事务。
第一步先给 RocketMQ发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 RocketMQ会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,且性能较高,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
存在的问题:
1、只有RocketMQ支持事务消息,像主流的RabbitMq,Kafka都不支持事务消息,因此事务消息的解决方案具有局限性
2、使用消息管理事务增加了系统的复杂性
本篇文章就先整理分享这些吧,如果觉得本文还可以,点个赞吧嘻嘻。