事务大家应该都不陌生,ACID也是老生常谈了,但是在讲分布式事务之前,我们还是复习下事务的四大特性:
原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,即不会存在中间状态,与原子性是密切相关的。
隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
其中隔离性会包含几个隔离级别,介绍之前我们先看几个概念,好明白事务隔离级别要实际解决的问题。
脏读:所谓脏读,就是指事务A读到了事务B还没有提交的数据,比如银行取钱,事务A开启事务,此时开启事务B-->取走100元,这时再看事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
不可重复读:所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务-->查出银行卡余额为1000元,此时开启事务B-->事务B取走100元-->提交,数据库里面余额变为900元,此时再看事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。
幻读:所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务-->修改所有学生当天签到状况为false,此时开启事务B-->事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。
事务隔离级别,就是为了解决上面几种问题而诞生的。为什么要有事务隔离级别,因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。SQL 标准定义了四种隔离级别,这四种隔离级别分别是:
读未提交(READ UNCOMMITTED):能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用。
读提交 (READ COMMITTED):能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读。
可重复读 (REPEATABLE READ):在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题不能完全避免(虽然大部分情况下不会存在)。
串行化 (SERIALIZABLE):最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。
从上往下,隔离强度逐渐增强,性能逐渐变差。可重复读是 MySQL 的默认级别。下表展示 4 种隔离级别对这三个问题的解决程度。
隔离级别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
复习完事务的基础知识,我们来看分布式事务,顾名思义分布式事务就是要在分布式系统中实现事务,保证在分布式系统中不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口(Mysql5开始,innoDB引擎也实现了),而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
XA协议包含两阶段提交(2PC)和三阶段提交(3PC)两种实现,先来看2PC。
2PC
2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,分别为prepare阶段和commitJ2PC 引入一个事务协调者的角色来协调管理各参与者的提交和回滚。
第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。
第二阶段,如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求,接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息,当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。
那么失败情况呢?在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。在第二阶段,事务协调节点向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作。
由上述可知,XA两阶段提交解决了分布式事务的问题并保证了强一致性,但是它也有明显的不足:
1.性能问题
XA协议遵循强一致性。在事务执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。这样的过程有着非常明显的性能问题。
2.协调者单点故障问题
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3.丢失消息导致的不一致问题
在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
基于这些问题,大家又提出了3PC(三阶段提交)的概念,三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。can_commit阶段与2PC的prepare阶段相比,变更成不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。在do_commit阶段,如果参与者无法及时接收到来自协调者的do_commit或者abort请求时,会在等待超时之后,继续进行事务的提交。(因为进入第三阶段,说明参与者在第二阶段已经收到了PreCommit请求,可能由于网络超时等原因,参与者没有收到commit或者abort响应,但是成功提交的几率很大)。
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞, 因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题。因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
总的来说,3PC通过预提交阶段可以减少协调者单点故障的问题,但是不能保证数据一致性。而且多引入一个阶段也多一个交互,因此性能会更差一些。
除了XA,我们再了解下TCC事务
TCC
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,TCC 指的是Try - Confirm - Cancel。
- Try 指的是预留,即资源的预留和锁定,注意是预留。
- Confirm 指的是确认操作,这一步其实就是真正的执行了。
- Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。TCC模型有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。
相对于 2PC、3PC ,TCC不需要全局锁,性能更好。但是开发量也更大,毕竟都在业务上实现,对于每一个操作都需要定义三个动作分别对应Try - Confirm - Cancel。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。
最后我们再介绍几种事务模式,分别是Saga,MQ和消息表模式
Saga事务
Saga 是长事务解决方案,思路是将大事务拆分若干个小事务,需要为每个子事务都实现正向操作和补偿操作。当子事务执行成功时,则继续执行正向操作,直到成功。当正向操作执行失败时,回滚本地事务的同时,会调用上一阶段的补偿操作,直到回到初始状态。Saga与TCC类似,同样没有全局锁。但实现起来更简单。
MQ事务
RocketMQ 就很好的支持了MQ事务,首先,给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务;再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。如果是 Commit 那么订阅方就能收到这条消息,再做对应的操作,做完了之后再消费这条消息即可。如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看出通过 RocketMQ 还是比较容易实现的,但也要考虑mq本身的可用性问题。
最后介绍下本地消息表事务
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务的执行和把消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
总结
可以看出 2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。
TCC和Saga类似,是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大。
本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。
引用:
https://zhuanlan.zhihu.com/p/183753774
https://www.cnblogs.com/xrq730/p/5087378.html https://blog.csdn.net/kusedexingfu/article/details/103484198
https://blog.csdn.net/bjweimengshu/article/details/79607522