分布式事务
什么是分布式事务?
便于理解分布式事务主要是两个概念,
一个是事务,比如我们买东西,交钱拿货物。这是一个完整的事务。我们去一家小店直接买,这就是一个本地的事务。
一个是分布式, 比如我们网购,在网上付钱,但等到货物到手需要经过远程商家的操作,因为操作不在本地可能出现各种个样的状况最终不能保证一定能拿到货物。这时候怎么保证付出去的钱收回呢?
分布式事务也是一样的概念,即保证分布式应用下,不同服务再操作具有事务性的数据时能向本地事务一样保证事务的正确进行。
怎么实现分布式事务?
一般来说(网上流传), 分布式的解决方案大致有5中:
- XA方案
- TCC方案
- 本地消息表
- 可靠消息最终一致性
- 最大努力通知方案
从字面上看可以,看不出什么东西~,那就来具体一个一个看一下这些方案,到底是何方神圣。
XA
XA即是由Oracle Tuxedo提出的 XA分布式事务协议
XA包含两阶段提交(2PC)和三阶段提交(3PC)
XA 协议有两个主要的角色事务协调者和事务参与者
现在来说下两段提交的流程,第一段
事务协调者会发送准备请求给不同服务,服务之间进行本地事务的操作。但并不提交事务,并返回准备完成的结果给协调者
第二个阶段,协调者在接收到所有节点准备完成后,发送commit命令到各节点。各节点提交事务
从这样的通讯模式从可以看出存在着明显的问题:
- 数据库资源的消耗,所有相关的节点在执行完本地操作后都会等待协调者的提交命令,这时候如果节点数量比较多,或者通讯过程中断开就会造成数据库资源的占用
- 单点问题,所有的操作依赖与协调者。这时候如果协调者宕机就会造成,下面的服务收不到协调者发送的信息而不能完成本地事务。
- 很容易造成数据的不一致性,如果服务A收到提交命令提交了数据,而服务B没有收到。则就会造成数据的不一致
三段提交原理:
三段提交是二段提交的升级,将第一个阶段再拆分成canCommit,和prepareCommit两个阶段,同时引入了超时机制
事务开始时,协调者先发送canCommit命令到各个参与者询问是否可以提交事务,等带参与这恢复后,发送prepareCommit命令,让参与者执行事务操作,最后等接收到参与者返回后,再发送提交操作。
3pc和2pc主要的区别是解决了单点问题,引入超时机制后,参与者在超过时间没有接收到协调者命令则会自动提交,但是还是没有解决数据不一致的问题。
一般而言,XA协议在分布式事务中用的比较少,一般在比较单一环境下的应用,如果跨多各数据库进行操作,可以用到XA方式
Spring 下的XA支持
spring框架下,支持JTA(Java Transaction API)协议。并提供了JTAPlatformTransactionManager管理JTA事务, spring虽然提供了JTA的接入但没有具体的实现,一般可使用Atomikos 进行支持。
TCC(try-confirm-cancel)
TCC与2PC的区别在于,两段式提交主要操作是在数据库层面,而TCC主要的操作是在应用程序的层面。
TCC主要可以解决,要么所有事务全部成功,要么所有事务全部失败的场景。如金融行业这种对于数据比较敏感的业务中。
TCC的模式下,数据操作服务需要实现三个接口 try,confirm, cancel。首先调用try接口看看各服务的资源是否准备完毕,如果正常则锁定资源。
接着调用confirm接口对事务进行操作,如果发现失败了则调用cancel接口。
TCC情况下会造成几个问题:
- cancel方法的空执行,如果try的时候失败了,则会调用cancel方法,这时候实际上事务还没开始。
- 如果在cancel阶段失败,需要引入重试机制,最好保证多次操作的幂等
- 悬挂, 如果在执行try的时候由于网络阻塞而造成中断,则协调器会判断失败调用cancel方法,这时候如果try继续执行,但协调器已经认为事务结束就会造成事务一只悬挂
TCC模式相对上由于代码需要应用曾自己去实现会相对复杂,当前比较流程的TCC框架有:
TCC-transaciont,Himily等,后面有时间可以尝试使用下。
本地消息表
本地消息表的机制如下:
本地消息表机制,概括的讲就是在数据库层面增加一个消息表操作时需要与操作的数据在同一个事务下一起提交。消息小记录当前消息的状态,当前服务实时接收和发送其他服务的消息,通过消息状态判断当前事务的操作时提交还是回滚。来达到数据的最终一致性,稍微画个图大致过程如下():
可靠消息最终一致
可靠消息最终一致性方案,其实和消息表差不多。其区别在于取消了本地消息表,完全由消息中间件如Kafka,rabbitMQ等去传递消息。但是需要保证消息的100%投递。
那这里就引申出一个问题,如何保证消息的100%投递呢?
一般分做到下面几个部分就能保证:
- 发送方发送消息到mq且能接收到正确的应答
- 消费方能成功消费消息,且完成后能成功回应发送方
- 具有完善的补偿机制,当出现状况时进行处理
一般要做到1,2两点需要另外消息确认系统去做消息的判断,并主动从mq中接受和发送消息
业界一般有两种解决方案:
-
消息信息落库(我的理解是和和本地消息表大致一样),需要进行两次数据库操作。大并发量下会增加负荷。
将消息写入表中并将状态标识为0,同时消息系统监听消费者返回的消息,如果确认消息正确投递并执行成功则将状态改为1,如果超过设定的等待阀值仍没有收到消息,则重试投递。
这里有个问题,如果消费端在执行完成后返回确认消息的时网络中断,造成监听程序无法获取确认,这时监听程序判断消息没有正确投递又重新投递了一遍给消费端,但消费端已经执行成功了要怎么处理呢? 我的想法是需要在消费端也需要记录消息是否已处理。 -
延迟投递二次确认
这种方式是去掉了将消息写入表中的步骤, 直接将消息发送到MQ,同时会有一个callback server去监听消费端返回的comfir信息,如果接受到confirm信息则表示消费端正常消费,将消息做持久化。这时生产端再次发送确认消息,如果callback 服务器中已有记录的消息说明消息已成功无需再次投递。
这种方式不能保证消息的100%投递,但再高并发环境下,可以提升处理速度
而保证事务也可以利用这种机制,保证事务的一致性。
最大努力通知
最大努力通知与可靠消息最终一致性的区别,在于最大努力通知的关键在于,消息的发送方保证消息投递到了消息接收方,并通过重试及校验机制验证最终结果的一致性。
引用一张别人的图 :原始地址
Saga事务
另外有在网上看到这种事务处理方式,这里也记录学习一下。
其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
- Saga = Long Live Transaction (LLT,长活事务)
- LLT = T1 + T2 + T3 + … +Ti(Ti为本地短事务)
- 每个本地事务Ti 有对应的补偿 Ci
Saga的两种恢复策略:
- 向后恢复,比如T1,T2,Ti 执行失败,则执行Ci,C1,C2恢复
- 向前重试,比如T1,T2,Ti执行失败,则进行Ti重试
Saga的问题:
没有进行事务的隔离, 容易造成数据的不一致。解决方案主要是通过代码层面上的控制,加锁,或存储当前状态等。
总结一下:
对于分布式情况下,要保证事务则需要使用到解决方案,当前行业里也有比较流程的解决方案况下,主要还是要理解一下。具体解决方案的思想。当然实际生产应用中要根据业务需求优化和架构出更好的解决方案,同时也要尽量保证整体系统的性能和稳定性。