分布式事务解决方案总结

1. 背景

数据库里的事务大家都不陌生,而在微服务架构中由于一个任务执行可能涉及多个微服务,要想在分布式系统实现事务 就要用到分布式事务了。

2.基础知识

事务(Transaction),一般指一个事情,一个任务,或者一个执行单位。事务可以看做是一个完整的任务,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

事务的ACID属性
事务应该具有四个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

  • 原子性(Atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
  • 一致性(Consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。
  • 隔离性(Isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability)。持久性也称永久性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。

分布式事务

分布式事务: 分布式系统会把一个应用系统拆分为可独立部署的多个服务,服务与服务之间通过远程协作完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。

比如存在一个订单的微服务,一个库存的微服务,当订单完成要同步减少库存,要在事务上确保完整和一致。

分布式事务产生的情景

  • 单体架构中使用了“分库”,即一个应用要操作多个数据库完成任务。由于跨数据库实例而需要用到分布式事务。
  • 微服务架构中经常要分库分表,一般要求一个微服务操作自己的数据库,需要访问其他服务的数据时要通过接口来操作。那么一个任务需要协调多个微服务完成任务时,需要用到分布式事务
  • 单个数据库被多个微服务调用,由于跨JVM进程,数据库的事务就失效了,这时需要用到分布式事务。

分布式事务基础理论
提到分布式就要涉及到CAP理论了:

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • Consistency(一致性):在分布式系统中的所有数据副本,在同一时刻是否同样的值。
  • Availability(可用性):保证每个请求不管成功或者失败都有响应。
  • Partition tolerance(分区容错性):系统中任意信息的丢失或失败不会影响系统的继续运作。

因此,要么AP,要么CP,要么AC,但是不存在CAP。

而分布式系统中由于拆分成多个微服务部署在不同的网络分区,总是要有 P 的。分区容忍性又是不可或缺的。

  • CA 放弃分区容忍性,即不进行分区。比如就一个节点,不用考虑网络不通或其他节点挂掉的问题,那么系统将不是一个标准的分布式系统。

  • CP 放弃可用性,追求一致性。比如银行系统中跨行转账,可以不可用,但不能有错账。

  • AP 放弃一致性,追求可用性。这是很多分布式系统设计时的选择,通常实现 AP 都会保证最终一致性。

  • 强一致性是指数据在一段时间内严格一致。

  • 最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。

3. 分布式事务解决方案

(1) 2PC

Two Phase Commitment Protocol: 2PC 即两阶段提交协议,是将整个事务流程分为两个阶段: 准备阶段(Prepare phase)、提交阶段(commit phase)

二阶段提交是一种强一致性设计,它引入一个事务协调者的角色来协调管理各个参与者(也可称之为各本地资源)的提交和回滚。

它由两个阶段组成:

  • 准备阶段(或投票阶段),在该阶段,协调者尝试准备所有事务的参与者以采取必要的准备步骤并投票,投票结果要么“是”:进入提交阶段,或“否”:中止(如果本地部分检测到问题)
  • 提交阶段,根据参与者的再次投票,协调者决定是提交(仅当所有参与者都投“是”)或者中止事务,并将结果通知所有参与者。然后,参与者使用他们的本地事务资源执行所需的操作(提交或中止)。

缺点: 最大缺点是它是一个阻塞协议。如果协调器永久失败,一些参与者将永远无法解决他们的事务。

2PC 适用于数据库层面的分布式事务场景,而我们业务需求有时候不仅仅关乎数据库,也有可能是上传图片发送文本消息等。

可选的技术方案有:JTA

(2) TCC

TCC 是指 Try - Confirm - Cancel 。

  • Try 阶段:完成所有业务检查(一致性),预留业务资源(准隔离性)
  • Confirm 阶段:确认执行业务操作,不再做任何业务检查, 只使用Try阶段预留的业务资源。
  • Cancel 阶段: 取消Try阶段预留的业务资源。

TCC 提供了三个阶段约定,因此可适用于 业务操作,而不局限在数据库。

TCC 不会一直持有资源锁,它的理念是 “ 先检查资源可用,再执行,大不了就回滚 ”
1、try 是保证资源预留的业务逻辑的正确性。
2、confirm/cancel 执行的本地事务逻辑确认/取消预留资源,以保证最终一致性。

  • 优点:有效了的避免了 2PC 提交占用资源锁时间过长导致的性能地下问题。
  • 缺点:主业务服务和从业务服务都需要进行改造,从业务方改造成本更高。

(3) 本地消息表

本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。

  • 1.1、就是会有一张存放“本地消息”的表,它和业务数据表在一个数据库中,以充分利用数据库的事务。

  • 1.2、在执行业务的时候 “ 将业务的执行 ” 和 “将消息放入消息表中的操作” 放在同一个事务中,以确保是一起执行的。

  • 2.1、再需要一个后台任务,它定时去读取“ 本地消息表 ”,筛选出还“ 未成功 ”的消息,按消息描述执行业务操作,如果执行成功则移除消息(或更新消息状态),如果执行失败则会继续重试。当然这里的过程也可以结合利用 MQ(消息队列)来发送消息到其他业务服务。

本地消息表实现的是最终一致性,容忍了数据暂时不一致的情况。

优点是简单。
缺点是严重依赖数据库。

(4) 消息事务

消息事务 利用消息中间件来实现,“事务发起方” 在执行完成本地事务后发出一条消息,然后 “事务参与方(接收消息的一方)" 接收消息并处理任务,它强调是最终一致性。

可以利用 RocketMQ 的 Half Message(半消息)实现一个消息事务。

  • 1.1、服务A,先给 Broker(消息中间件) 发送一个 Half Message(半消息),其实这个半消息已发送到 Broker端,但是此消息的状态被标记为"不能投递",消费者还看不到,处于这种状态下的消息称为半消息。
  • 1.2、发送完 半消息后,服务A 执行业务操作(本地事务),再根据操作结果:
    ___ 如果成功,则向 Broker 发送 一个 Commit 命令,这时 半消息就变成了正儿八经的消息,就可以被消费者消息了。
    ___ 如果失败,则发送一个 RollBack 命令,该消息则会被删除。
  • 2.0、参与方(消息消费者)读取消息并执行任务。
  • 3.0、RocketMQ 的反查:对于 半消息 RocketMQ 会自动定时轮询回调你的接口,询问这个处理的处理情况。借助这点,服务A实现一个回调,根据实际处理结果 Commit 或者 Rollback,加强一致性判断。

可靠消息最终一致性方案。

如果不用RocketMQ,也可以基于 ActiveMQ 或者 RabbitMQ 自己封装一套这套逻辑出来。

(5) 最大努力通知方案

即 用最大努力,多次反复尝试,直至任务完成,尽力最终让事务都一致了。

比如 用一个后台任务定时去查看未完成的消息,然后去执行对应的任务(调用服务),如果仍然都失败则记录下转由人工处理。

适用于对时间不敏感的业务,例如短信通知。

最大努力通知方案是最终一致性方案。

(6)对比

  • 2PC 是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。

  • TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。

  • TCC模型中的主业务服务 相当于 CAP 模型中的AP

总结: 具体选型要看场景,TCC 是强一致方案适合要求严格的场景; 其他则考虑使用 可靠消息的最终一致性方案。单体架构操作多数据库则考虑 JPA。

4. 可选方案:

JTA

Java Transaction API,通常称为JTA,是用于管理 Java中的事务的API 。它允许我们以资源无关的方式启动,提交和回滚事务。

JTA的真正强大之处在于它能够在单个事务中管理多个资源(如数据库,消息服务)。

JTA 是 两阶段提交 的实现。

在JTA中处理事务的第一种方法是使用@Transactional注解。

@Bean("dataSource1")
public DataSource dataSource() throws Exception {
    return createDatasource("jdbc:hsqldb:mem:DB11");
}

@Bean("dataSource2")
public DataSource dataSourceAudit() throws Exception {
    return createDatasource("jdbc:hsqldb:mem:DB22");
}

@Transactional
public void executeTrans(String para) {
    ervice1.do(.....);
    ervice21.do(.....);
    if ( .... ) {
        throw new RuntimeException();
    }
}

XA 方案

2PC的传统方案是在数据库层面实现的,如 Oracle、MySQL 都支持 2PC 协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织 Open Group 定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。

Seata 方案

Seata 是由阿里中间件团队发起的开源项目,它是一个是开源的分布式事务框架。

传统 2PC 的问题在 Seata 中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT 模式(即 2PC)及 TCC 模式的分布式事务解决方案。

RocketMQ 实现消息事务

利用 RocketMQ 的半消息实现。

5. 扩展

一个 TCC For REST 的示例实现

TCC 模型涉及到 参与者,事务协调者,和应用, 各个需要提供的API。

  • 参与者 API:从业务服务需要提供的API,其需要提供try接口供主业务服务调用,需要提供confirm、cancel接口供事务管理器调用。这里将从业务服务称之为Participant。
  • 事务协调者 API:其需要提供事务日志上报接口服务,向主业务活动提供 各个阶段各个从业务活动资源 是否预留成功的消息。
  • Application:主业务服务,其不需要提供任何接口,只需要操作上述对象。

参与者 API提供:
1、try 接口,用于try阶段的检查资源

GET /part/123

2、confirm 接口:confirm 阶段的操作

PUT /part/123

其应该有超时判断。

3、cancel 接口(可选实现):用于取消资源预留

PUT /part/123

如果取消失败,也不会影响结果。资源预留要有一个expires截止时间,超过这个截止时间,参与者就可以主动取消这个预留的资源。

事务协调者提供的API
职责有:

  • 所有的try接口都调用成功了,因此主业务服务希望 事务协调者 向各个从业务服务进行confirm
  • try接口部分成功,部分失败。因此主业务服务希望 事务协调者 对已经try成功的从业务服务都进行cancel

接口有:
1、confirm接口:

PUT /coordinator/confirm

然后协调器会对参与者逐个发起Confirm请求。

2、cancel接口

PUT /coordinator/cancel

然后协调器会对参与者逐个发起 cancel 请求。

6.参考:

https://www.oracle.com/java/technologies/jta.html
https://github.com/changmingxie/tcc-transaction
http://www.tianshouzhi.com/api/tutorials/distributed_transaction/388

你可能感兴趣的:(分布式事务解决方案总结)