0%

分布式事务实现

在分布式系统中,同时满足“CAP定律”中的“一致性”、“可用性”和“分区容错性”三者是不可能的。在互联网领域的绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

XA

XA规范主要定义了事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口. XA引入事务管理器是因为在分布式系统中,从理论上讲两台机器上无法达到一致的状态,需要引入一个外部点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源, 资源管理器负责控制和管理实际资源 。XA是一个两阶段提交协议,该协议分为以下两个阶段:

  1. 事务协调器要求每个涉及到事务的数据库预提交,并反映是否可以提交 。
  2. 事务协调器要求每个数据库提交数据 。

如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息 。

JTA

JTA作为JAVA平台上的事务规范,同时定义了对XA事务的支持;在JTA中,事务管理器抽象为javax.transaction.TransactionManager接口,通过底层事务服务(即JTS)实现,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由J2EE容器所提供的JTA实现(如JBOSS)和独立的JTA实现(如JOTM,Atomikos)。

JTA本质上是两阶段提交,实现复杂,牺牲了可用性,对性能影响较大, 适合对数据强一致(其实也不能100%保证强一致)要求很高的关键领域; 大部分互联网业务都不会采用两阶段提交的方式 。

链式事务管理

这种方式也Spring提供的,可以将两个或多个数据库资源的事务串联到一起,公用一个TransactionManager来实现对多个资源的事务。配置方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  <bean id="transactionManager" class="org.springframework.data.transaction.ChainedTransactionManager">
<property name="transactionManagers">
<list>
<bean
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource1" />
</bean>
<bean
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource2" />
</bean>
</list>
</property>
</bean>
</code>

针对多个数据库实现事务。使用这种方式时,在Spring事务提交的时候,它会依次调用里面的多个dataSource的commit()方法,如果业务方法出错,就会按照相反的顺序调用rollback()方法。这种方法可能会出现先前的提交成功,之后的提交失败,所以还是会有事务失败的可能。

实现简单,但可能会出现先前的提交成功,之后的提交失败,所以还是会有事务失败的可能

最大努力一次提交(Best Efforts 1PC)

在一个系统中使用数据库和带事务功能的消息中间件,业务流程如下

  1. 开始消息事务
  2. 发送消息
  3. 开始数据库事务
  4. 更新数据库
  5. 提交数据库事务
  6. 提交消息事务

有两个事务,分别是DB的和JMS的事务,事务的开启和提交都是相互独立的。依次提交这两个事务,只要第二个事务顺利提交,整个方法就能够保证数据的一致性。实际上,在绝大多数情况下,只要数据库和MQ能够正常访问,这也确实能够保证。所以,这种方式就叫’最大努力’一次提交。

使用这种方式,事物提交的顺序是非常重要的。假设在提交messaging transaction的时候发生错误,这时数据库的事务已经提交,无法回滚,但是消息的事务被回滚,那么这一条消息会被重新放回队列中,该业务方法会被再次触发,再次在一个新的事务中处理。但是,这时数据的处理已经完成,只是最后JMS的事物提交出错,那么就需要通过防止重复提交的方式,来避免数据库的再次处理。

事务补偿型(TCC型事务–Try/Confirm/Cancel)

其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的, 即:只要Try成功,Confirm一定成功
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放

在一个长事务,一个由两台服务器一起参与的事务,服务器A发起事务,服务器B参与事务,B所处理时间可能比较长。如果按照ACID的原则,要保持事务的隔离性、一致性,服务器A中发起的事务中使用到的事务资源将会被锁定,不允许其他应用访问到事务过程中的中间结果,直到整个事务被提交或者回滚。这就造成事务A中的资源被长时间锁定,系统的可用性将不可接受。服务器A的事务如果执行顺利,那么事务A就先行提交,如果事务B也执行顺利,则事务B也提交,整个事务就算完成。但是如果事务B执行失败,事务B本身回滚,这时事务A已经被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态。这样的事务模型,是牺牲了一定的隔离性和一致性的,但是提高了事务的可用性。

与两阶段提交相比实现及流程相对简单,但应用层要写很多补偿代码(而且补偿也不能保证一定成功)

本地流水表实现最终一致性

以电商下单场景为例,主要涉及到两个操作,扣减库存生成订单,因为两个操作在不同的数据库,无法保证强一致性,可以通过本地流水表来实现最终一致性 , 具体流程如下:

  • 生成交易操作唯一标示token
  • 事务一(库存系统):
    • 冻结库存
    • 根据下单流水号生成商品的库存冻结记录,冻结记录主要包括skuId,token,冻结数量,状态 .状态有3种状态: 已冻结下单成功扣减下单失败释放,初始状态为已冻结
  • 如果事务一失败,直接返回;如果成功进入事务二
  • 事务二(订单系统, 本地事务):根据token生成订单,订单的状态主要包括:未支付,已支付,超时未支付,订单的初始状态为未支付
  • 事务二如果成功,则进行后续的流程,
  • 事务二如果失败,调用库存系统的回滚接口,返回下单失败;
  • 定时任务: 因为存在事务一成功事务二失败的情况,这样会冻结商品的部分库存,所以可以捞取出创建超过一定时间状态为已冻结的所有冻结记录,根据每个冻结记录的token去订单表查询,若不存在对应的订单,则将冻结记录的状态更新为下单失败释放,并回滚商品库存数量

异步确保型

将一些有同步的事务操作变为异步操作,避免对数据库事务的争用;继续以以电商下单场景为例,支付成功后增加用户积分;

  • 事务一(订单系统),订单状态修改为支付成功,发送支付成功消息
  • 事务二(用户系统),用户系统接到支付成功消息后,增加用户积分

MQ事务消息

一些第支持事务消息MQ,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,其思路大致为:

  • 第一阶段Prepared消息,会拿到消息的地址。
  • 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。

在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,是回滚还是继续发送确认消息。这样就保证了消息发送保证与本地事务同时成功或同时失败

分布式事务实现的原则

  • 大事务拆成小事务,每个小事务都是单机上的事务
  • 补偿 + 重试, 业务上设计补偿机制,而且保证补偿失败后有重试机制
  • 幂等, 保证每次事务操作是幂等的,保证幂等的方式可以采用:
    • 状态值,每次写操作的时候检查状态值
    • 唯一标示,每次写操作都带入业务唯一标示