深圳幻海软件技术有限公司 欢迎您!

再谈分布式事务,你了解多少?

2023-02-28

前言三年前,我写了第一篇和分布式事务相关的文章再有人问你分布式事务,把这篇扔给他,后面陆续也写了一些和分布式事务相关的文章:如何能在实战中完成分布式事务深度剖析一站式分布式事务方案Seata-Server深度剖析一站式分布式事务方案Seata-Cient解密分布式事务框架-Fescar时隔三年,回看

前言

三年前,我写了第一篇和分布式事务相关的文章再有人问你分布式事务,把这篇扔给他,后面陆续也写了一些和分布式事务相关的文章:

  • 如何能在实战中完成分布式事务
  • 深度剖析一站式分布式事务方案Seata-Server
  • 深度剖析一站式分布式事务方案Seata-Cient
  • 解密分布式事务框架-Fescar

时隔三年,回看之前的文章,之前的确也有很多漏掉的一些知识,所以今天在这里再次和大家总结下分布式事务相关的东西。

事务

首先还是先说一下事务的定义吧,事务的英语是transaction,我们查找词典可以发现这个单词的中文解释是交易,买卖等含义,所以我们可以知道事务一定和交易密不可分他们才能共享一个英文单词,而交易的定义是什么呢?有句俗话说得好,一手交钱,一手交货,那这个就是交易的规则,而这个同时也是事务的定义。那么事务的官方定义是什么呢?

事务是一系列操作的集合,这些操作要么都做,要么都不做,是一个不可分割的工作单位,是数据库环境中的最小工作单元。

可以发现这个基本和我们一手交钱,一手交货很像,的确在现实的开发环境中,在交易的业务中对事务的保证特别看重,而一些社交类的业务,和资金关系不大的,比如点赞数,评论数,是不会对事务特别看重,这些应该关注的是性能。

事务的类型

之前的文章都没有介绍过事务的类型相关,直到之前看了一篇文章,才知道事务是分了5种类型的,这里也介绍给大家:

扁平事务

我们日常使用的基本都是扁平事务,以begin开始,然后以commit 或者 rollback结束.

begin;
do xxxx;
commit/rollback;
  • 1.
  • 2.
  • 3.

带保存点的扁平事务

增加了SavePoint机制,内存保存,如果数据库宕机,savepoint将会丢失。

begin

insert into xxx

savepoint a

insert into yyy

rollback to a
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这里的rollback to a只会回滚到保存点a这里,不会整个事务都回滚

链式事务

当我们提交事务后,相当于执行了 COMMIT AND CHAIN,也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务。如果回滚只会回滚当前节点。

通过下面sql语句可以再mysql里面查到当前是否开启链式模式,以及如何开启

select @@completion_type
set @@completion_type = 1 // 0无链式, 
  • 1.
  • 2.

嵌套事务

  • 嵌套事务可以是一棵树,其中的叶节点可以使扁平事务,也可以是嵌套事务,但是都叫做子事务
  • 某个节点回滚只影响当前节点下面所有的事务
  • 子节点的提交是会根据父节点提交才会最后提交,也就说,所有事务的保存只能再最顶层提交,才会生效。
  • mysql不支持嵌套事务,Oracle支持

分布式事务

通常是一个在分布式环境下运行的扁平事务,需要根据数据所在位置访问网络中的不同节点,一般来说对于分布式事务,其同样需要满足单机 ACID 特性,要么都发生,要么都失效。但是实际实现的情况,可能远比理想的更为复杂,所以通常会降低要求。

单机事务

上面讲了五种类型的事务,前4种其实都可以归结为单机事务,而单机事务也是后端程序员中经常接触到的,所以先简单讲讲单机事务的核心关键点:

ACID

ACID是单机事务的四大特性,由这四个特性可以定义到底什么条件下才能算作事务。

  • A:原子性——事务内的操作要么全部提交,要么全部回滚。
  • C:一致性——一个事务执行之前和执行之后数据库都必须处于一致性状态(数据库的数据要么处于事务前的状态,要么处于事务后的状态)。
  • I:隔离性—— 在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。
  • D:持久性——事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态

实现关键

这里只讲一下mysql的一些实现关键:

整体来说事务的ACID是通过InnoDB日志和锁来保证:。

  • 事务的隔离性是通过数据库锁的机制实现的
  • 持久性通过redolog(重做日志)来实现。
  • 原子性通过Undolog来实现。
  • 一致性依靠三面上个特性来实现

redolog,undolog,binlog通常会容易搞混淆,这里简单说一下

undolog:用于回滚和mvcc,可以理解他用于记录之前版本的数据。

redolog:数据库为了加快刷盘速度,采用了WAL的方式,也就是先顺序预写日志,然后再异步去刷盘,而redolog就是顺序写日志的产物。

binlog:上面都是innodb引擎的日志,binlog是mysql-server的日志,用于主从同步等作用。

代码使用

java程序员的话如果使用的是spring,那么使用本地事务会有两种手段:

声明式事务

其实就是直接加个注解,spring自己会做一个切面代理,然后使用事务,这种方法使用得应该也是最多的,因为他是最简单的,但是说如果只想控制这个方法部分代码进入事务,或者调用同一个类的方法使用事务,那就不太能使用这种方法。

编程式事务

编程式事务,实现起来稍显麻烦,需要自己手写很多代码,但是能解决上面说的那个问题。这种方式更加灵活多变。

分布式事务

为什么需要分布式事务

我们会发现分布式事务在最近几年里提到的声音越来越多,这是究竟为什么呢?在《架构即未来》这本书中提到了AKF模型是软件架构扩展的基础模型

如上图,如果我们要对一个软件进行扩展,那么需要以AKF模型为基础,从三个维度进行扩展。

  • X轴:x轴的意思就是将以前的单机,变成集群,从台机器,扩容成N台。
  • Y轴:以前的单体服务将所有业务代码都写在一个服务里面,而Y轴做的就是将这些业务模块分开,拆分成微服务。
  • Z轴:很多业务瓶颈在数据库,通常我们可以做数据库分库分表,单元化等等

在Y轴中我们之前的单体服务被拆分成了多个服务,那就有可能以前一个事务的内容,可能出现在了多个服务中,比如一个订单扣除积分优惠券,之前是一个服务,如果拆分出来了有积分服务和优惠券服务,那么我们之前的本地事务就会失效,就需要引入分布式服务来保证一个订单中的全部扣减都是成功的。

在Z轴中,如果我们做了分库分表,也会破坏本地事务,如果大家都是一个库自然还能使用分库分表,但如果操作了不同库那么就无法保证事务,那么也需要引入分布式事务。

而AKF又是现代软件扩展的基础模型,三者有其二可能都需要引入分布式事务,所以,如果要对软件进行扩展,那么分布式事务必不可少。

分布式的理论知识

CAP和BASE的理论知识应该很多人都知道,但是我这里还是需要介绍一下,因为分布式事务离不开这两个东西。

CAP

CAP定理,又被叫作布鲁尔定理。CAP是分布式系统的入门理论。

  • C (一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
  • A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。
  • P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

BASE

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。

1.基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。

2.软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。

3.最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务解决方案

刚性事务

刚性事务追求的是强一致性事务。

DTP/XA

DTP的XA规范(全称为Distributed Transaction Processing The XA Specification)的制定者是X/Open,即现在的Open Group。

熟悉数据库自带的分布式事务支持的同学,其实就知道这个就是XA,

这里的AP你可以理解成是我们自己的业务服务,RM则是我们使用的数据库通常都是使用mysql而mysql也自带支持XA,TM可以是一个单独的服务,也可以是我们自己的业务服务承担,他的作用是做事务管理。如果是用mysql的话,使用XA有如下代码:

XA BEGIN '123';
insert into xxx;
XA END '123'; // 链接断开会失去这次事务
XA PREPARE '123'; // 二阶段准备会持久化
XA COMMIT '123'; //prepare全部成功/或者有一个失败就回滚
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

当然XA其实有很多缺点:

1.数据锁定:数据在整个事务处理过程结束前,都被锁定,读写都按隔离级别的定义约束起来。

2.协议阻塞:XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。

3.性能差:性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。

柔性事务

因为刚性事务的实现成本较大,对于现在互联网的业务来说很多不愿意承受这么大的成本性能损失,愿意牺牲一定的一致性来保证性能,所以我们这里叫做柔性事务。

消息最终一致

适用于很多异步任务,在我们的场景中,比如异步审核笔记,异步发放积分(适用于加法的业务)。而消息最终一致也有两种实现方法。

消息表

qmq实现事务消息的方法是利用的消息表。首先我们需要有一张这样的消息表,用来和我们业务在同一个事务里面一起保存。

CREATE TABLE `msg_queue` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `topic` varchar(64) NOT NULL,
  `nameServer` varchar(64) NOT NULL,
  `status` smallint(6) NOT NULL DEFAULT '0' COMMENT '消息状态',
  `error` int unsigned NOT NULL DEFAULT '0' COMMENT '错误次数',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='记录业务系统消息';
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

从上面图上可以看出,我们会有单独的task在不断扫描我们的消息表,如果消息表没有被删除掉,代表之前没有发送成功,那么我们需要做发送,这里需要说的是,我们一定要保证这个消息是幂等的,因为这里的发送完之后删除消息,并不能保证数据是一致的,有可能一个消息会发送多次,最后才能被删掉。

使用的代码如下:

@Transactional
public void pay(Order order){
    saveOrder(order);
    messageProducer.sendMessage(buildMessage(order)); //要写在最后
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

rocketmq事务消息

rockemq的事务消息如图上所示分为四个阶段:

  • 第一阶段:先发送一个prepare消息,会获取到一个prepare的消息id
  • 第二阶段:执行本地事务,如果执行成功,则发送commit/失败则rollback。
  • 第三阶段:rocketmq-server根据消息结果,如果成功就投递给consumer,不成功则把消息删除掉。
  • 第四阶段: 如果二阶段没有上报消息结果,那就需要进行回查。

最大努力通知

适合于开放平台,外部的第三方系统想要保证最终一致。比如微信,支付宝的开放平台。下面我贴一个微信支付平台的执行流程:

从上面可以看到,最大努力通知需要保证两点:

  • 有限次数的重试,一般重试策略采用指数退避
  • 需要提供查询接口,来防止通知失败。

TCC

支付的同学比较常用的,虽然是柔性事物,但是目标是有刚性的效果。隔离性比较强。

举个例子:有积分服务,券服务,余额服务,如果用户一次订单想同时扣减这三个怎么能保证。用其他的模式可以吗?

消息最终一致和最大努力通知,都不太适合,无法保证隔离型,用户重复使用资产。XA性能差。

所以这里我们选择使用了TCC,分三个方法:

  • try: 锁定,通常用一个字段或者记录。(演化成saga直接try commit合并)
  • commit: 提交资源
  • cancel:释放资源

SAGA

Saga是30年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

  • 适用于无法提供TCC接口(遗留系统,外部系统),一般来说提供提交 和 回滚接口即可 ,这里的可以看做业务上的接口,生成订单 对应 的删除订单就是回滚,不需要单独命名回滚接口。
  • 不看隔离性,一阶段就生效
  • 想异步执行
  • 想支持正向重试(tcc,try 为什么不能正向重试,资源一直被业务隔离,需要释放隔离性)

SEATA

当然上面介绍了很多种分布式事务,有同学会想,说了这么多但是我该怎么实现呢?那我在这里推荐使用seata,seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。为用户提供了 AT、TCC、SAGA 和 XA 事务模式。

有兴趣的可以访问seata官网:https://seata.io/zh-cn 。我这里就不具体介绍了,或者看我之前的文章也有很多介绍。

最后

时隔这么久,再次写了一下关于分布式事务相关的,这次算对之前的是补充,当然也算是对分布式事务的总结。希望大家能在自己的业务中能找到合适自己的分布式事务的方法。