1、聊聊事务原理
一句话来说,要么成功,要么失败,不能有中间态
1.1、回忆一下ACID
- Atomic:原子性,就是一堆SQL,要么一起成功,要么都别执行,不允许某个SQL成功了,某个SQL失败了,这就是扯淡么
- Consistency:一致性,这个是针对数据一致性来说的,就是一组SQL执行之前,数据必须是准确的,执行之后,数据也必须是准确的。别搞了半天,执行完了SQL,结果SQL对应的数据修改没给你执行,这不是坑爹么
- Isolation:隔离性,这个就是说多个事务在跑的时候不能互相干扰,别事务A操作个数据,弄到一半儿还没弄好呢,结果事务B来改了这个数据,导致事务A的操作出错了,那不就搞笑了么
- Durability:持久性,事务成功了,就必须永久对数据的修改是有效的,别过了一会儿数据自己没了,不见了,那就好玩儿了
1.2、老生常谈的事务隔离级别
- 读未提交(Read Uncommitted):这个很坑爹,就是说某个事务还没提交的时候,修改的数据,就让别的事务给读到了,这就恶心了,很容易导致出错的。这个也叫做脏读
- 读已提交(Read Committed)又叫做不可重复读:这个比上面那个稍微好一点,但是一样比较尴尬,就是说事务A在跑的时候, 先查询了一个数据是值1,然后过了段时间,事务B把那个数据给修改了一下还提交了,此时事务A再次查询这个数据就成了值2了,这是读了人家事务提交的数据啊,所以是读已提交。这个也叫做不可重复读,就是所谓的一个事务内对一个数据两次读,可能会读到不一样的值
- 可重复读(Read Repeatable):这个就是比上面那个再好点儿,就是说事务A在执行过程中,对某个数据的值,无论读多少次都是值1;哪怕这个过程中事务B修改了数据的值还提交了,但是事务A读到的还是自己事务开始时这个数据的值
- 串行化:幻读,不可重复读和可重复读都是针对两个事务同时对某条数据在修改,但是幻读针对的是插入,比如某个事务把所有行的某个字段都修改为了2,结果另外一个事务插入了一条数据,那个字段的值是1,然后就尴尬了。第一个事务会突然发现多出来一条数据,那个数据的字段是1。如果要解决幻读,就需要使用串行化级别的隔离级别,所有事务都串行起来,不允许多个事务并行操作
2、MySQL的事务隔离级别剖析一下
2.1、InnoDB存储引擎架构设计
2.2、MySQL是怎么实现RR事务隔离的
2.2.1、Redo Log日志了解一下
2.2.2、MVC Undo Log日志链条
- 事务A,插入了一条数据
- 接着有一个事务B跑来修改了一下这条数据
- 接着假设事务C又来修改了一下这个值为C
2.2.3、 MySQL RR readview + undo log日志链
- 首先假设有一条数据是事务id=50的一个事务插入,之后有事务A和事务B同时在运行
- 事务A发起一个查询,他就是第一次查询就会生成一个ReadView,此时ReadView里的creator_trx_id是60,min_trx_id是60,max_trx_id=71,m_ids=[60,70]
- 这个时候事务A基于这个ReadView去查询这条数据,会发现这条数据的trx_id=50,是小于ReadView里的min_trx_id的,
说明他发起查询之前,早就有事务插入这条数据了还提交了所以此时可以查到这条原始数据 - 接着事务B此时更新了这条数据的值为B,此时会修改trx_id=70,同时生成一个undo log,而且此时事务B还提交了,也就是说事务B已经结束了
- 这个时候有一个问题,ReadView中的m_ids此时还会是60和70吗?
那是必然的,因为ReadView一旦生成就不会改变了,这个时候虽然事务B已经结束了,但是事务A的ReadView里,还是会有60和70两个事务id,意思就是说,你在事务A开启的时候,事务B当时是在运行的
那好,接着此时事务A去查询这条数据的值,他会惊讶的发现此时数据的trx_id是70了,70一方面是在ReadView的min_trx_id和max_trx_id的范围区间的,同时还在m_ids列表中
说明起码是事务A开启查询的时候,id=70的这个事务B还是在运行的,然后由这个事务B更新了这条数据,此时事务A是不能查询到事务B的这个值的,因此这个时候只能顺着指针往历史版本链条上去找
接着事务顺着指针找到下面一条数据,trx_id为50,是小于ReadView的min_trx_id的,说明在他开启查找之前,就已经提交这个事务了,所以事务A是可以查询到这个值的,此时事务A查到的是原始值
你事务A多次读同一个数据,每次读到的都是一样的值,除非是他自己修改了值,否则读到的都是一样的值,不管别的事务如何修改数据,事务A的ReadView始终是不变的,他基于这个ReadView始终看到的值是一样的 - 接着此时事务A再次查询,此时发现符合条件有2条数据,一条是原始值的那个数据,一条是事务C插入的那条数据,但是事务C插入的那条数据的trx_id是80,这个80是大于自己的ReadView的max_trx_id的,说明是自己发起查询之后,这个事务才启动的,所以此时这条数据是不能查询的,所以MySQL RR避免了幻读
3、XA规范了解一下
X/Open的组织定义了分布式事务的模型,这里面有几个角色,就是AP(Application,应用程序),TM(Transaction Manager,事务管理器),RM(Resource Manager,资源管理器),CRM(Communication Resource Manager,通信资源管理器)
TM的话就是一个在系统里嵌入的一个专门管理横跨多个数据库的事务的一个组件,RM的话说白了就是数据库(比如MySQL),CRM可以是消息中间件(但是也可以不用这个东西)
那么XA是啥呢?说白了,就是定义好的那个TM与RM之间的接口规范,就是管理分布式事务的那个组件跟各个数据库之间通信的一个接口
这个XA仅仅是个规范,具体的实现是数据库产商来提供的,比如说MySQL就会提供XA规范的接口函数和类库实现
JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现,例如Atomikos, bitronix都提供了jar包方式的JTA实现框架。这样我们就能够在Tomcat或者Jetty之类的服务器上运行使用JTA实现事务的应用系统,一般是单机跨多库情况下,但是在微服务的今天,一般都是一个服务一个库,一般都不会这么干了
3.1、2PC是啥?
X/Open组织定义的一套分布式事务的模型,还是比较虚的,还没办法落地,而且XA接口规范也是一个比较务虚的一个东西,还是没法落地的
2PC说白了就是基于XA规范搞的一套分布式事务的理论,也可以叫做一套规范,或者是协议
缺点:
1、同步阻塞:在阶段一里执行prepare操作会占用资源,一直到整个分布式事务完成,才会释放资源,这个过程中,如果有其他人要访问这个资源,就会被阻塞住
2、单点故障:TM是个单点,一旦挂掉就完蛋了
3、事务状态丢失:即使把TM做成一个双机热备的,一个TM挂了自动选举其他的TM出来,但是如果TM挂掉的同时,接收到commit消息的某个库也挂了,此时即使重新选举了其他的TM,压根儿不知道这个分布式事务当前的状态,因为不知道哪个库接收过commit消息,那个接收过commit消息的库也挂了
4、脑裂问题:在阶段二中,如果发生了脑裂问题,那么就会导致某些数据库没有接收到commit消息,那就完蛋了,有些库收到了commit消息,结果有些库没有收到,这咋整呢,那肯定完蛋了
3.1、3PC是啥?
如果人家TM在DoCommit阶段发送了abort消息给各个库,结果因为脑裂问题,某个库没接收到abort消息,自己还执行了commit操作,不是也不对么
4、TCC是个什么原理(柔性事务)
try、confirm和cancel
4.1、允许空回滚
事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因为丢包而导致的网络超时,此时事务管理器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作,而 Cancel 操作调用未出现超时
TCC 服务在未收到 Try 请求的情况下收到 Cancel 请求,这种场景被称为空回滚;空回滚在生产环境经常出现,用户在实现TCC服务时,应允许允许空回滚的执行,即收到空回滚时返回成功
4.2、防悬挂控制
事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因网络拥堵而导致的超时,此时事务管理器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作,Cancel 调用未超时;在此之后,拥堵在网络上的一阶段 Try 数据包被 TCC 服务收到,出现了二阶段 Cancel 请求比一阶段 Try 请求先执行的情况,此 TCC 服务在执行完到的 Try 之后,将永远不会再收到二阶段的 Confirm 或者 Cancel ,造成 TCC 服务悬挂。
用户在实现 TCC 服务时,要允许空回滚,但是要拒绝执行空回滚之后 Try 请求,要避免出现悬挂
4.3、TCC vs 2PC
2PC:是资源层面的分布式事务,一直会持有资源的锁。 如果跨十几个库,一下锁这么多数据库,会导致,极度浪费资源。降低了吞吐量
TCC:在业务层面的分布式事务,最终一致性,不会一直持有锁。将锁的粒度变小,每操作完一个库,就释放了锁。但是需要原本一个接口要拆三个接口,比较麻烦
用2PC 比 TCC 要性能高。因为tcc多了多次接口调用。而此时的2PC 不怕占用资源,反正就一个调用。高并发场景下TCC 优势要大
4.4、异步确保型TCC技术方案
如果要接入到一个TCC分布式事务中来,从业务服务必须改造自己的接口,本来就是一个接口,现在要新增两个接口,try接口,cancel接口。改造起来比较麻烦
这个大概来说就是把之前的通用型TCC方案给改造了一下,就是在主业务服务和从业务服务之间加了一个可靠消息服务,但是这个可靠消息服务可不是在请求什么MQ之类的东西,而是将消息放在数据库里的
4.5、补偿性TCC解决方案
补偿型 TCC 解决方案与通用型 TCC 解决方案的结构相似,其从业务服务也需要参与到主业务服务的活动决策当中。但不一样的是,前者的从业务服务只需要提供 Do 和 Compensate 两个接口,而后者需要提供三个接口
Do 接口直接执行真正的完整业务逻辑,完成业务处理,业务执行结果外部可见;Compensate 操作用于业务补偿,抵消或部分抵消正向业务操作的业务结果,Compensate操作需满足幂等性。
与通用型解决方案相比,补偿型解决方案的从业务服务不需要改造原有业务逻辑,只需要额外增加一个补偿回滚逻辑即可,业务改造量较小。但要注意的是,业务在一阶段就执行完整个业务逻辑,无法做到有效的事务隔离,当需要回滚时,可能存在补偿失败的情况,还需要额外的异常处理机制,比如人工介入
6、可靠消息最终一致性方案(RocketMQ)
7、最大努力通知方案
8、sagas 长事务解决方案
8.1、编排(Choreography)
由于没有中心的控制类参与参与者操作之间的协调工作,因此通过消息发送的方式进行协调
在分布式事务场景下,如果对数据有强一致性要求,会在业务层上才去“两阶段提交”(2PC)的方案。
如果保证最终一致性的话可以采取TCC(Try Confirm Cancel)模式。虽然TCC保证最终一致性的模式被业内广泛使用,但是对于某些分布式事务场景,流程多、流程长、还可能要调用其它公司的服务。特别是对于不可控的服务(其他公司的服务),这些服务无法遵循 TCC 开发模式,导致TCC模式的开发成本增高。体现在具体场景中,以金融核心的业务为代表(渠道层、产品层、集成层),其特点是:流程多、流程长、调用不可控服务。同时也是应为流程长,事务边界太长,加锁时间长,使用TCC模式会影响并发性能。
鉴于此类业务场景的分布式事务处理,提出了Saga分布式处理模式。Saga是一种“长事务的解决方案”,更适合于“业务流程长、业务流程多”的场景。特别是针对参与事务的服务是遗留系统服务,此类服务无法提供TCC模式下的三个接口,就可以采用Saga模式
- “订单服务”中执行“创建订单”操作,此时会发送一个“创建订单消息”到队列中
- “支付服务”监听到队列中的这个订单消息,调用“支付订单”的操作,同时也发送“只服务消息”到队列中。
- “库存服务”在监听到“支付消息”之后会进行“扣减库存”的处理,并且发送“扣减库存消息”等待下一个消费者接受
- “发货服务”作为整个事务的最后一个子事务,在接到“扣减库存消息”以后会执行发货的子事务,完成事务以后会给“订单服务”发送“发货消息”,订单服务在接受到消息以后完成整个事务闭环,并且提交
上面说的是事务执行成功的情况,如果事务执行失败那应该如何处理?
- 假设在执行“发货”时子事务失败了,会发送“发货失败消息”
- 库存服务在接受到“发货失败消息”之后会执行“回滚库存”的操作,该操作将原来扣减的库存加回去,同时发送“扣减失败消息”
- “支付服务”在接受到“扣减失败消息”之后会执行“回滚支付”,进行退款的操作,同时发送“支付失败消息”。订单服务在接受到该消息以后将下单事务标记为失败
从上面的描述可以看出编排的好处:
简单:每个子事务进行操作时只用发布事件消息,其他子事务监听处理
松耦合:参与者(服务)之间通过订阅事件进行沟通,组合会更加灵活
当然也有一些缺点:
理解困难 : 没有对业务流程进行完整的描述,要了解整个事务的执行过程需要通过阅读代码完成。增加开发人员理解和维护代码的难度
存在服务的循环依赖:由于通过消息和事件进行沟通,参与者之间会存在循环依赖的情况。也就是A服务调用B服务,B服务又调用A服务的情况。这也增加了架构设计的复杂度,在设计初期需要认真考虑。
紧耦合风险:每个参与者执行的方法都依赖于上一步参与者发出的消息,但是上一步的参与者的所有消息都需要被订阅,才能了解参与者的真实状态,无形中增加了两个服务的耦合度
8.2、控制(Orchestration)
其核心是定义一个控制类,它会告诉参与者(服务)应该执行哪些操作(子事务)。 Saga控制类通过命令以及异步回复的方式与参与者进行交互
- 订单服务执行下单事务时,向Saga协调器发送请求命令,Saga协调器接受到命令以后按照子事务执行的顺序调用服务中的方法
- 最开始执行“支付订单”的操作,调用“支付服务”中的“支付订单”操作,并且通过虚线的部分返回执行结果“支付完成”
- 接下来,执行“库存服务”中的“扣减库存”方法,同样通过虚线部分返回扣减完成的消息给“请求反馈“模块
- 紧接着就是执行“发货“命令,调用”发货服务“中的”发货“方法,并且返回”发货完成“的响应
- 最后,三个子事务都执行完毕以后,返回订单服务,完成整个分布式事务
介绍完成成功完成事务之后,再来看看出现异常的情况
- 在执行“发货”命令时发现“发货失败”,于是“发货服务”反馈给Saga协调器。
- 此时协调器调用“库存服务”中的“回滚库存”操作,将扣减的库存恢复。
- 然后调用“支付服务”中的“回滚支付”完成支付退款的工作。
- 最后,通知订单服务事务处理失败
控制器设计的优点:
避免循环依赖:在编排模式中存在参与者之间的循环调用,而中心控制类的方式可以避免这种情况的发生
降低复杂性:所有事务交给控制器完成,它负责命令的执行和回复的处理,参与者只需要完成自身的任务,不用考虑处理消息的方式,降低参与者接入的复杂性
容易测试:测试工作集中在集中控制类上,其他服务单独测试功能即可
容易扩展:如果事务需要添加新步骤,只需修改控制类,保持事务复杂性保持线性,回滚更容易管理
当然这种方法也存在缺点:
依赖控制器:控制器中集中太多逻辑的风险。
增加管理难度:这种模式除了管理各个业务服务以外,还需要额外管理控制类服务,无形中增加了管理的难度和复杂度。而且存在单点风险,一旦控制器出现问题,整个业务就处于瘫痪中
如感兴趣,点赞加关注,谢谢
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。