数据库事务:
**事务:**数据库的事务是指一组sql语句组成的数据库逻辑处理单元,在这组的sql操作中,要么全部执行成功,要么全部执行失败; 事务是一组不可再分割的操作集合。
事务是数据库管理系统的(DBMS)执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。主要用于执行DML语句。
**逻辑处理单元 是数据库的最小工作单元,不可再分。 **
**TCL:**Transaction Control Language事务控制语言。
**隐式(自动)事务:**没有明显的开启和结束,本身就是一条事务可以自动提交,比如insert、update、delete
**显示事务:**具有明显的开启和结束,
mysql
#使用显示事务:
1.set autocommit=0;关闭自动提交
2.start transaction;开启事务,可以省略
3.编写一组逻辑SQL语句,
注意:SQL语句支持的是insert、update、delate。select语句没意义。
4.savepoint 回滚点名;设置回滚点
5.结束事务
- commit;提交
- rollback;回滚
- rollback to 回滚点名;回滚到设置回滚点的地方。
delete和truncate在事务使用时的区别:
mysql
#演示delete
set autocommit=0;
start transaction;
delete from account;
rollback;
#演示truncate
set autocommit=0;
start transaction;
truncate table account;
rollback;
mysql
#关闭自动提交事务:
set session autocommit=off;
#自动提交事务:
set session autocommit=on;
#开始事务:
begin/start transaction
#提交事务:
commit
#回滚事务:
rollback
#事务的创建
#隐式事务:事务没有明显的开启和结束的标记
比如insert、update、delete语句
#显示事务的语句:事务具有明显的开启和结束的标记
前提:必须先设置自动提交功能为禁用
set autocommit=0;
#开启事务
start transaction;
语句一;
语句二;
...
#结束事务的语句;
commit;#提交事务
或
rollback;#回滚事务
事务执行顺序:
1、先执行begin/start transaction,
2、再执行语句,
3、在执行提交/回滚事务。
执行开始事务后,不提交,关闭sql窗口,会自动回滚。
事务的四大特性(ACID):
原子性(Atomicity):
指的是一个事务中的操作要么全部成功,要么全部失败。
一致性(Consistency):
事务前后数据的完整性必须保持一致。事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
例如,在一次转账过程中,从某一个账户中扣除的金额必须与另一账户中存入的金额相等。支付号账号100 你读取到余额要取,有人向你转100 但是事务没提交(这时候你读到的余额应该是100,而不是200)这种就是一致性
隔离性(Isolation):
多个用户并发访问数据库时,一个事务的修改在最终提交前,对其他事务时不可见的。
持久性(Durability):
指的是一旦事务提交,所做的修改就会永久保存到数据库中。
事务日志:
事务的隔离性 是由锁和MVCC机制实现的。
Undo Log:
事务的**"一致性"是由"Undo Log"**实现的。
Undo Log在事务中起到两方面作用:回滚事务 和多版本并发事务(MVCC)。
在MySQL启动事务之前,会先将要修改的记录存储到Undo Log中。如果数据库的事务回滚或者MySQL数据库崩溃,可以从Undo Log中读取相应的数据记录进行回滚操作。数据库写入数据到磁盘之前,会先把数据缓存在内存中,事务提交时才会写入磁盘。
与Redo Log相反,Undo Log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到Undo Buffer,在适合的时间把Undo Buffer中的内容刷新到磁盘。
Undo Buffer与Redo Buffer一样,也是环形缓冲,但当缓冲满的时候,Undo Buffer中的内容会也会被刷新到磁盘;与Redo Log不同的是,磁盘上不存在单独的Undo Log文件,所有的Undo Log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。
Rollback Segment:回滚段这个概念来自Oracle的事物模型,在Innodb中,Undo Log被划分为多个段,具体某行的Undo Log就保存在某个段中,称为回滚段。可以认为Undo Log和回滚段是同一意思。
Redo Log:
事务的**"原子性"和 "持久性"是由"Redo Log"**实现的。它确保MySQL事务提交后,事务所涉及的所有操作要么全部成功,要么全部失败。
Redo Log也被称作重做日志,它是在InnoDB存储引擎中产生的。Redo Log主要记录的是物理日志,也就是对磁盘上的数据进行的修改操作。Redo Log往往用来恢复提交后的物理日志,不过只能恢复到最后一次提交的位置。
Redo Log就是保存执行的SQL语句到一个指定的Log文件,当Mysql执行recovery时重新执行redo log记录的SQL操作即可。
当客户端执行每条(更新语句)时,Redo Log会被首先写入Log Buffer;当客户端执行commit命令时,Log Buffer中的内容会被视情况刷新到磁盘。Redo Log在磁盘上作为一个独立的文件存在,即Innodb的Log文件。
持久性:重做日志写入磁盘后,即便发生宕机导致缓冲中的脏页丢失,我们启动时依然可以根据重做日志对事务进行恢复,这边保证了事务的持久性。
原子性:我们知道原子性的含义即为要么一组sql都执行成功,要么都失败。重做日志能够确保数据库宕机后,事务要么整个丢失,要么整个恢复,因此它保证了事务的原子性。
Redo什么时候写入磁盘:
1.事务提交之前
2.Redo Buffer满了
Bin Log:
Redo Log是InnoDB存储引擎特有的日志,MySQL也有其本身的日志,这个日志就是BinLog,即二进制日志。
BinLog是一种记录所有MySQL数据库表结构变更以及表数据变更的二进制日志。不记录查询操作的日志。
两个应用场景:
主从复制:在主数据库上开启BinLog,主数据库吧BinLog发送至从数据库,从数据库获取BinLog后通过I/O线程将日志写到中继日志,也就是Relay Log中。然后通过SQL线程将Relay Log中的数据同步至从数据库,从而达到主从数据库数据的一致性。
数据恢复:当MySQL数据库发生故障或者崩溃时,可以通过BinLog进行数据恢复。例如:使用mysqlbinlog等工具进行数据恢复。
Relay Log:
Relay log,我们翻译成中文,一般叫做中继日志。一般情况下它在MySQL主从同步读写分离集群的从节点才开启。主节点一般不需要这个日志。
master主节点的binlog传到slave从节点后,被写道relay log里,从节点的slave sql线程从relaylog里读取日志然后应用到slave从节点本地。从服务器I/O线程将主服务器的二进制日志读取过来记录到从服务器本地文件,然后SQL线程会读取relay-log日志的内容并应用到从服务器,从而使从服务器和主服务器的数据保持一致。
它的作用可以参考如下图,从图片中可以看出,它是一个中介临时的日志文件,用于存储从master节点同步过来的binlog日志内容,它里面的内容和master节点的binlog日志里面的内容是一致的。然后slave从节点从这个relaylog日志文件中读取数据应用到数据库中,来实现数据的主从复制。
开启relaylog可以通过参数relay_log
来配置。在my.cnf
配置文件中,增加如下配置就可以开启relaylog。
shell
[mysqld]
# 启用中继日志,其中mysql-relay表示日志的文件名称,文件存放在datadir参数指向的目录下面。
relay-log=mysql-relay
# relaylog的其他选项配置
relay_log_info_repository=table
relay_log_recovery=on
sync_relay_log=1
sync_relay_log_info=1
区别:
BinLog和Redo Log的区别:
BinLog和RedoLog在一定程度上都能恢复数据,但是二者有着本质的区别。
- BinLog是MySQL本身就拥有的,不管使用何种存储引擎,BinLog都存在。而RedoLog是InnoDB存储引擎特有的,只有InnoDB存储引擎寸会输出RedoLog。
- BinLog是一种逻辑日志,记录的是对数据库的所有修改操作。而RedoLog是一种物理日志,记录的是每个数据页的修改。
- RedoLog具有幂等性,多次操作的前后状态是一致的。而BinLog不具有幂等性,记录的是所有影响数据库的操作。例如插入一条数据后再将其删除,则RedoLog前后的状态未发生变化,而BinLog就会记录插入操作和删除操作。
- BinLog开启事务时,会将每次提交的事务一次性写入内存缓冲区,如果未开启事务,则每次成功执行插入,更新和删除语句时,就会将相应的事务信息写入内存缓冲区。而RedoLog是在数据准备修改之前将数据写入缓冲区的RedoLog中,然后在缓冲区中修改数据。而且在提交事务时,先将RedoLog写入缓冲区,写入完成后再提交事务。
- BinLog只会在事务提交时,一次性写入BinLog,其日志的记录方式与事务的提交顺序有关,并且一个事务的BinLog中间不会插入其他事务的BinLog。而RedoLog记录的是物理页,最后一个提交的事务记录会覆盖之前所有未提交的事务记录,并且一个事务的RedoLog中间会插入其他事务的RedoLog。
- BinLog是追写加入,写完一个日志文件再写下一个日志文件,不会覆盖使用,而RedoLog是循环写入,日志空间的大小是固定的,会覆盖使用。
- BinLog一般用于主从复制和数据恢复,并且不具备崩溃自动恢复的能力。而RedoLog是在服务器发生故障后重启MySQL,用于恢复事务已提交但未写入数据表的数据。
事务并发的三大问题:
事务并发的三大问题其实都是数据库读写一致性问题,必须由数据库提供一定的事务隔离机制开解决。
并发事务就是:多个事务同时操作同一个数据库的相同数据时。
脏读:
sql
一个事务对数据进行了增删改查,但是未提交事务。另一个事物可以读取到未提交的数据,如果第一个事务进行了回滚,那么第二个事务就读到了脏数据。
例子:
领导给张三发工资,10000元已打到张三账户,但该事务还未提交,正好这时候张三去查询工资,发现10000元已到账。这时领导发现张三工资算多了5000元,于是回滚了事务,修改了金额后将事务提交。最后张三实际到账的只有5000元。
不可重复读:
sql
两次读到的数据不一样。一次事务发生了两次读操作,两个读操作之间发生了另一个事务对数据修改操作,这时候第一次和第二次读到的数据不一致。不可重复度关注点在数据更新和删除,通过行级锁可以实现可重复读的隔离级别。
例子:
张三需要转正1000元,系统读到卡余额有2000元,此时张三老婆正好需要转正2000元,并且在张三提交事务前把2000元转走了,当张三提交转账是系统提示余额不足。
幻读:
sql
幻读(Phantom Reads):数据库中有三条数据 将数据库中的所有数据都修改为name为aa,但是当我修改完还没提交的时候,另一个事务进来了 添加了一条数据 并提交了,然后,当我那天修改数据的事务提交的时候发现4行受到影响,哎 我里面明明只有3条数据 为什么会4条受到影响呢?这就是幻读
幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。
相对于不可重复读,幻读更关注其它事务的新增数据。通过行级锁可以避免不可重复读,但无法解决幻读的问题,想要解决幻读,只能通过Serializable隔离级别来实现。
例子:
张三老婆准备打印张三这个月的信用卡消费记录,经查询发现消费了两次共1000元,而这时张三刚按摩完准备结账,消费了1000元,这时银行记录新增了一条1000元的消费记录。当张三老婆将消费记录打印出来时,发现总额变为了2000元,这让张三老婆很诧异。
更新丢失/快照读:
sql
第一类更新丢失(回滚丢失):A事务在回滚时,"不小心"将B事务已经转入账户的金额给抹去了。
原因:在MySQL数据库,任何隔离级别都不允许第一类更新丢失。(加事务隔离级别)
第二类更新丢失(覆盖丢失):A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:
原因:MySQL可重复读默认采用的是快照读。快照读的一个问题也就是没有办法获取最新的数据。所以快照读是第二类更新丢失的一个主要原因。
解决快照读:基本两种思路,一种是悲观锁,另外一种是乐观锁;
简单的说就是一种假定这样的问题是高概率的,最好一开始就锁住,免得更新老是失败;另外一种假定这样的问题是小概率的,最后一步做更新的时候再锁住,免得锁住时间太长影响其他人做有关操作。
MySQL事务隔离级别:
**数据库事务的隔离性:**一个事务与其他事务隔离的程度称为隔离级别。数据库系统必须具有隔离并发运行各个事务的能力,使他们不会相互影响 避免各种并发问题。
隔离级别越高,数据一致性就越好,但并发性越弱。
**读未提交:**事务中的修改即使未提交也是对其它事务可见。(脏读,不可重复度,幻读)
**读已提交:**事务提交后所做的修改才会被另一个事务看见,可能产生一个事务中两次查询的结果不同。(不可重复度,幻读)
**可重复读:**只有当前事务提交才能看见另一个事务的修改结果。解决一个事务中两次查询的结果不同的问题 默认。(幻读)
**串行化:**只有一个事务提交之后才会执行另一个事务。解决事务并发所有问题!执行效率慢,使用时慎重。
隔离级别 | 隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
read-uncommit (读未提交) | 0 | 是 | 是 | 是 |
read-commit (读已提交) | 1 | 否 | 是 | 是 |
repeatable read (可重复读)。Mysql默认 | 2 | 否 | 否 | 是 |
serializable (串行化) | 3 | 否 | 否 | 否 |
在InnoDB中Repeatable-Read(可重复读)可以解决脏读、不可重复读、幻读三大问题。
read-uncommit 不加锁
Serializable 所有的select语句都会被隐式的转化为select... lock in share mode (共享锁),会和update、delete互斥。
Oracle支持的2种事务隔离级别:read commiten,serializable。默认的事务隔离级别为:read commited。
mysql
#查看事务隔离级别
select @@tx_isolation;
#设置隔离级别
#语法:set session|global transaction isolation level 隔离级别;
set session transaction isolation level read uncommittid;
set session transaction isolation level read committid;
set session transaction isolation level read repeatable read;
set session transaction isolation level read serializable;
#设置当前MySQL连接的隔离级别:
set transaction isolation level read committid;
#设置数据库系统的全局的隔离级别:
set global transaction isolation level read committed;
#savepoint的使用
set autocommit=0;
start transaction;
delete from account where id=25;
savepoint a;#设置保存点,a为保存点名(随便取)
delete from account where id=28;
rollback to a;#回滚到保存点
事务隔离级别解决方案:
两者可以协同使用
1、第一种:在读取数据前,对其加锁,阻止其他事务对数据进行修改。(基于锁的并发控制,LBCC)
2、第二种:生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别的(语句级或事务级)的一致性读取。(多版本并发控制,MVCC)
MVCC:
**MVCC是 不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案 **
即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
多版本的意思就是数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事物隔离级别去判断读取哪个版本的数据。
MVCC在mysql的Innodb引擎中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。他无非就是乐观锁的一种实现方式。在Java编程中,如果把乐观锁看成一个接口,MVCC便是这个接口的一个实现类而已。
**多版本并发控制:**读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链。
MVCC只在read committed 和repeatable read两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为read uncommitted总是读取最新的数据行,而不是符合当前事务版本的数据行。而serializable则会对所有读取的行都加锁。
基本原理:
MVCC的实现,通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。
在并发读写数据库时,读操作可能会不一致的数据(脏读)。为了避免这种情况,需要实现数据库的并发访问控制,最简单的方式就是加锁访问。由于,加锁会将读写操作串行化,所以不会出现不一致的状态。但是,读操作会被写操作阻塞,大幅降低读性能。
在java concurrent包中,有copyonwrite系列的类,专门用于优化读远大于写的情况。而其优化的手段就是,在进行写操作时,将数据copy一份,不会影响原有数据,然后进行修改,修改完成后原子替换掉旧的数据,而读操作只会读取原有数据。通过这种方式实现写操作不会阻塞读操作,从而优化读效率。而写操作之间是要互斥的,并且每次写操作都会有一次copy,所以只适合读大于写的情况。
MVCC的原理与copyonwrite类似,全称是Multi-Version Concurrent Control,即多版本并发控制。在MVCC协议下,每个读操作会看到一个一致性的snapshot,并且可以实现非阻塞的读。MVCC允许数据具有多个版本,这个版本可以是时间戳或者是全局递增的事务ID,在同一个时间点,不同的事务看到的数据是不同的。
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。
InnoDB三个隐藏字段:
在Mysql中MVCC是在Innodb存储引擎中得到支持的,Innodb为每行记录都实现了三个隐藏字段:
- 6字节的事务ID(DB_TRX_ID )
- 7字节的回滚指针(DB_ROLL_PTR)
- 隐藏的ID(rowid)
(列名 | 是否必须 | 描述 |
---|---|---|
row_id | 否 | 行ID,唯一标识一条记录(如果定义主键,它就没有啦) |
transaction_id | 是 | 事务ID |
roll_pointer | 是 | DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本 |
RR和RC是什么:
RC(read commit:读已提交),RR(repeatable read:可重复读)
1、 RR 支持间隙锁gap lock和next-key lock临键锁,而RC则没有间隙锁gap lock。因为MySQL的RR需要间隙锁gap lock来解决幻读问题。而RC隔离级别则是允许存在不可重复读和幻读的。所以RC的并发一般要好于RR;
2、RC 隔离级别,通过 where 条件走非索引列过滤之后,不符合条件的记录上的行锁,会释放掉(虽然这里破坏了"两阶段加锁原则");但是RR隔离级别,通过 where 条件走非索引列过滤之后,即使不符合where条件的记录,也是会加行锁。所以从锁方面来看, RC 的并发应该要好于RR;可以减少一部分锁竞争,减少死锁和锁超时的概率。
Innodb的锁:
Innodb提供了基于行的锁,如果行的数量非常大,则在高并发下锁的数量也可能会比较大,据Innodb文档说,Innodb对锁进行了空间有效优化,即使并发量高也不会导致内存耗尽。
**对行锁有分两种:**排他锁、共享锁。共享锁针对读,排他锁针对写,完全等同读写锁的概念。
如果某个事务在更新某行(排他锁),则其他事物无论是读还是写本行都必须等待;
如果某个事物读某行(共享锁),则其他读的事物无需等待,而写事物则需等待。通过共享锁,保证了多读之间的无等待性,但是锁的应用又依赖Mysql的事务隔离级别。
分布式事务:
分布式事务目的主要是为了保证结果一致性。
分布式事务,就是指不是在单个服务或单个数据库架构下产生的事务。
1.跨数据源,
2.跨服务,
3.跨服务又跨数据源,
解决方案:
- 基于XA协议的强一致性事务方案,保证强一致性就必然带来性能上的影响。(2PC,3PC)
- 基于Base的弱一致性事务解决方案。(TCC,基于可靠消息的最终一致性方案,Seata的Saga事务模型)。
我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:
mysql
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
但是在分布式环境下,会变成下边这样:
mysql
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;
可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
分布式事务产生的场景
1、典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务。

2、单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨数据库实例产生分布式事务。

3、多服务访问同一个数据库实例 比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。

分布式事务的基础:
我们之前说过数据库的 ACID 四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论。
CAP:
C (一致性) :对某个指定的客户端来说,读操作能返回最新的写操作。
对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。
合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。
P (分区容错性) :当出现网络分区后,系统能够继续工作。打个比方,这里集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
熟悉 CAP 的人都知道,三者不能共有,在分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象。
如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是 A 又不允许,所以分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
对于 CP 来说,放弃可用性,追求一致性和分区容错性,我们的 ZooKeeper 其实就是追求的强一致。
对于 AP 来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的 BASE 也是根据 AP 来扩展。
同时 CAP 中选择两个,比如你选择了 CP,并不是叫你放弃 A。因为 P 出现的概率实在是太小了,大部分的时间你仍然需要保证 CA。
BASE:
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写,是对 CAP 中 AP 的一个扩展。
基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。
最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。
BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。
BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
分布式事务解决方案:
分阶段提交:
在该模型中,一个分布式事务(全局事务)可以被拆分成多个本地事务,运行在不同的AP和RM上,每个本地事务的ACID很好实现,但是全局事务必须保证每一个本地事务都能同时成功,若有一个本地事务失败,则所有事务都必须回滚。但问题是,本地事务处理过程中,并不知道其他事务的运行状态,因此就需要CRM来通知各个本地事务,同步事务执行的状态。
因此,各个本地事务的通信必须有统一的标准,否则不同的数据库就无法通信。XA 就是X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始,提交,终止,回滚等接口,各个数据库厂商都必须实现这些接口。
本地消息表:
本地消息表:创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统。
基本设计思想: 将远程分布式事务拆分成一系列的本地事务。如果不考虑性能及设计优雅,借助关系型数据库中的表即可实现。
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。
- 消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
- 处理消息队列中的消息,完成自己的业务逻辑。
- 如果本地事务处理成功,则表明已经处理成功了。
- 如果本地事务处理失败,那么就会重试执行。
- 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。
- 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
本地消息表优点:
- 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
本地消息表缺点:
- 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
- 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的
举个经典的跨行转账的例子来描述。
第一步伪代码如下,扣款1W,通过本地事务保证了凭证消息插入到消息表中。
sql
begin transaction
update a set amout=amout-10000 where userid=1;
insert into message(userid,amout,status)value(1,10000,1);
end transaction
commit;
第二步,通知对方银行账户上加1W了。
通知方式可采用时效性高的MQ,由对方订阅消息并监听,有消息时自动触发事件,或者
采用定时轮询扫描的方式,去检查消息表的数据。
两种方式其实各有利弊,仅仅依靠MQ,可能会出现通知失败的问题。而过于频繁的定时轮询,效率也不是最佳的(90%是无用功)。所以,我们一般会把两种方式结合起来使用。
解决了通知的问题,又有新的问题了。万一这消息有重复被消费,往用户帐号上多加了钱,那岂不是后果很严重?
仔细思考,其实我们可以消息消费方,也通过一个"消费状态表"来记录消费状态。在执行"加款"操作之前,检测下该消息(提供标识)是否已经消费过,消费完成后,通过本地事务控制来更新这个"消费状态表"。这样子就避免重复消费的问题。
总结:上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了"最终一致性"。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案 https://queue.acm.org/detail.cfm?id=1394128。
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。
人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用 100 元去买一瓶水的例子。
- 当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和减去水的库存写入到本地消息表,放入同一个事务(依靠数据库本地事务保证一致性)。
- 这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫它减去水的库存,到达商品服务器之后,这时得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。
- 商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器在本地消息表进行状态更新。
- 针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的。
如果已经接收,再判断是否执行,如果执行在马上又进行通知事务;如果未执行,需要重新执行由业务保证幂等,也就是不会多扣一瓶水。
本地消息队列是 BASE 理论,是最终一致模型,适用于对一致性要求不高的情况。实现这个模型时需要注意重试的幂等。
MQ消息:
基于MQ的事务消息方案主要依靠MQ的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC的思路,是二阶段提交的广义拓展。
半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知MQ发送;

- 事务发起方首先发送半消息到MQ;
- MQ通知发送方消息发送成功;
- 在发送半消息成功后执行本地事务;
- 根据本地事务执行结果返回commit或者是rollback;
- 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅方;
- 订阅方根据消息执行本地事务;
- 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
- 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;
- Consumer端的消费成功机制有MQ保证;
MQ事务消息对比本地消息表
MQ事务消息:
- 需要MQ支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理;
- 具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能;
DB本地消息表:
- 使用了数据库来存储事务消息,降低了对MQ的要求,但是增加了存储成本;
- 事务消息使用了异步投递,增大了消息重复投递的可能性;
非事物消息:
通常情况下,在使用非事务消息支持的MQ产品时,我们很难将业务操作与对MQ的操作放在一个本地事务域中管理。通俗点描述,还是以上述提到的"跨行转账"为例,我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功。这样一致性似乎很难保证。
先从消息生产者这端来分析,请看伪代码:
java
public void trans(){
try{
//1.操作数据库
bool result = dao.update(model); //操作数据库失败,会抛出异常
//2.如果第一步成功,则操作消息队列(投递消息)
if(result){
mq.append(model); //如果mq.append方法执行失败(投递消息失败),方法内部会抛出异常
}
}catch(Exception ex){
rollback(); //如果发生异常,则回滚
}
}
根据上述代码及注释,我们来分析下可能的情况:
操作数据库成功,向MQ中投递消息也成功,皆大欢喜
操作数据库失败,不会向MQ中投递消息了
操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚
从上面分析的几种情况来看,貌似问题都不大的。那么我们来分析下消费者端面临的问题:
消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致
尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果
如何保证消息与业务操作一致,不丢失?
主流的MQ产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的(有些MQ可以自定义重试次数)。
如何避免消息被重复消费造成的问题?
保证消费者调用业务的服务接口的幂等性
通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)
总结:这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。如果MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。
事物消息:
它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:
1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务
4、A发送提交消息给消息中间件
通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操作
步骤二出错,则整个事务失败,不会执行A的本地操作
步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
RocketMQ事务消息:
在 RocketMQ 中实现了分布式事务,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部。

基本流程如下:
第一阶段 Prepared 消息,会拿到消息的地址。
第二阶段执行本地事务。
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。
如果确认消息失败,在 RocketMQ Broker 中提供了定时扫描没有更新状态的消息。
如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在 RocketMQ 中是以 Listener 的形式给发送者,用来处理。
如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这时就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失。
TCC:
TCC(补偿事务):针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动。
**Try:**预定操作资源 。说白了就是"占座"的意思,在正式开始执行业务逻辑之前,先把要操作的资源占上座。
**Confirm:**真正执行业务(提交)。在这阶段可对 Try 阶段锁定的资源进行各种 CRUD 操作。不做检查 只要Try成功 Confirm一定成功。
**Cancel:**释放Try阶段预留的业务资源(回滚操作);需要通过业务代码,对 Confirm 阶段执行的操作进行人工回滚。
TM(事务的调用方)首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。
**优势:**TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其他事务的执行结果。而如果其他事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
劣势:
- 业务侵入:需要人为编写代码实现try,confirm,cancel,代码侵入较多。
- 开发成本高:一个业务需要拆分成3个步骤,分别编写业务实现,业务编写比较复杂。
- 安全性考虑:cancel动作如果执行失败,资源就无法释放,需要引入重试机制呢,而重试可能导致重复执行,还要考虑重试时的幂等问题。
TCC模型对业务的侵入性较强,改造的难度较大,每个操作都需要有try、confirm、cancel三个接口实现。
TCC 中会添加事务日志,如果Confirm(证实)或者Cancel(取消)阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等。
整个TCC业务分成两个阶段完成:
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。
第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。
需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。
TCC 事务机制相比于上面介绍的 XA,解决了如下几个缺点:
-
解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
-
同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
-
数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。

对于 TCC 的解释:
Try 阶段:尝试执行,完成所有业务检查(一致性),预留必需业务资源(准隔离性)。
Confirm 阶段:确认真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,Cancel 操作满足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
举个简单的例子:
如果你用 100 元买了一瓶水,
Try 阶段:你需要向你的钱包检查是否够 100 元并锁住这 100 元,水也是一样的。
如果有一个失败,则进行 Cancel(释放这 100 元和这一瓶水),如果 Cancel 失败不论什么失败都进行重试 Cancel,所以需要保持幂等。
如果都成功,则进行 Confirm,确认这 100 元被扣,和这一瓶水被卖,如果 Confirm 失败无论什么失败则重试(会依靠活动日志进行重试)。
对于 TCC 来说适合一些:
强隔离性,严格一致性要求的活动业务。
执行时间较短的业务。
TCC解决方案是什么?
TCC是两阶段提交协议。
TCC(Try-Confirm-Cancel)是一种常用的分布式事务解决方案,它将一个事务拆分成三个步骤:
- T(Try):业务检查阶段,这阶段主要进行业务校验和检查或者资源预留;也可能是直接进行业务操作。
- C(Confirm):业务确认阶段,这阶段对Try阶段校验过的业务或者预留的资源进行确认。
- C(Cancel):业务员回滚阶段,这阶段和上面的C(Confirm)是互斥的,用于释放Try阶段预留的资源或者业务。
TCC空回滚是解决什么问题的?
在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法。比如当Try请求由于网络延迟或故障等原因,没有执行,结果返回了异常,那么此时Cancel就不能正常执行了,因为Try没有对数据进行修改,如果Cancel(取消)进行了对数据的修改,那就回导致数据不一致。
解决思路是关键,就是要识别出这个空回滚。思路很简单就是需要知道Try阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。建议TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,第一阶段Try方法里会插入一条记录,表示Try阶段执行了。Cancel接口里读取该记录,如果改记录存在,则正常回滚;如果改记录不存在,则是空回滚。
如何解决TCC幂等问题?
为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的二阶段Confirm和Cancel接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。
解决思路在上述分支事务记录中增加执行状态,每次执行前豆查询该状态。
如何解决TCC中悬挂问题?
悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行。
出现问题是在调用分支事务Try时,由于网络发生拥堵,造成了超时,TM就会通知RM回滚该分布式事务,可能回滚完成后,Try请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一节阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后无法继续处理。
解决思路是,如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,判断分支事务记录表中是否已经有二阶段事务记录,如果有则不执行Try。
二阶段提交(2PC)
基于XA协议的两阶段三阶段提交 2PC 3PC
2PC提交协议是什么?
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作,还是中止操作。
**优势:**2PC技术很成熟,可以保证强一致。
**劣势:**单点故障问题。阻塞问题:资源锁定问题
两个阶段是指:
- 第一阶段:准备阶段(投票阶段)
- 第二阶段:提交/回滚阶段(执行阶段)
准备阶段:
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种"万事具备,只欠东风"的状态。
可以进一步将准备阶段分为以下三个步骤:
1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。
提交/回滚阶段:
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;
否则,发送提交(Commit)消息;
参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
当协调者节点从所有参与者节点获得的相应消息都为"同意"时;
1)协调者节点向所有参与者节点发出"正式提交(commit)"的请求
2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
3)参与者节点向协调者节点发送"完成"消息。
4)协调者节点受到所有参与者节点反馈的"完成"消息后,完成事务。
如果任一参与者节点在第一阶段返回的响应消息为"中止",或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
1)协调者节点向所有参与者节点发出"回滚操作(rollback)"的请求。
2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
3)参与者节点向协调者节点发送"回滚完成"消息。
4)协调者节点受到所有参与者节点反馈的"回滚完成"消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。
两阶段提交举例:
第一阶段,张老师作为"协调者",给小强和小明(参与者、节点)发微信,组织他们俩明天8点在学校门口集合,一起去爬山,然后开始等待小强和小明答复。
第二阶段,如果小强和小明都回答没问题,那么大家如约而至。如果小强或者小明其中一人回答说"明天没空,不行",那么张老师会立即通知小强和小明"爬山活动取消"。
如果小强没看手机,那么张老师会一直等着答复,小明可能在家里把爬山装备都准备好了却一直等着张老师确认信息。更严重的是,如果到明天8点小强还没有答复,那么就算"超时"了。
解析:
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
准备阶段:
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种"万事俱备,只欠东风"的状态。
提交阶段:
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
局限性和适合场景:
所有的操作必须是事务性资源(XA协议),存在使用局限性(微服务架构下多数使用HTTP协议),比较适合传统的单体应用;
由于是强一致性,资源需要在事务内部等待,性能影响较大,吞吐率不高,不适合高并发与高性能的业务场景;
对于场景A和B,使用该方案具有一定意义。
落地方案
在JavaEE平台下,WebLogic、Webshare等主流商用的应用服务器提供了JTA的实现和支持。而在Tomcat下是没有实现的,这就需要借助第三方的框架Jotm、Automikos等来实现,两者均支持spring事务整合。
详细请阅读:http://blog.jobbole.com/95632/
2PC提交协议有什么缺点:
1、同步阻塞问题。执行过程中,所有参与者都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。(1PC:准备阶段,只执行slq,而不提交,并且占用数据库连接资源)
2、单点故障。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提价的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交了。
处理方法:补偿。手动补偿,脚本补偿。
1)服务1:0->1
2)服务2:a->b
如果 (服务1数据是0&&服务2数据是a){数据没问题}
正确的例子:(服务1是0,服务2是a),(服务1是1,服务2是b)。
不正确的例子:(服务器1是0,服务器2是b),(服务器1是1,服务器2是a)。
说到 2PC 就不得不聊数据库分布式事务中的 XA Transactions。

在 XA 协议中分为两阶段:
事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
事务协调器要求每个数据库提交数据,或者回滚数据。
优点:
尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。
缺点:
单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。
比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。
二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
准备阶段
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种"万事俱备,只欠东风"的状态。
可以进一步将准备阶段分为以下三个步骤:
1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。
提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
当协调者节点从所有参与者节点获得的相应消息都为"同意"时:

1)协调者节点向所有参与者节点发出"正式提交(commit)"的请求。
2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
3)参与者节点向协调者节点发送"完成"消息。
4)协调者节点受到所有参与者节点反馈的"完成"消息后,完成事务。
如果任一参与者节点在第一阶段返回的响应消息为"中止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

1)协调者节点向所有参与者节点发出"回滚操作(rollback)"的请求。
2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
3)参与者节点向协调者节点发送"回滚完成"消息。
4)协调者节点受到所有参与者节点反馈的"回滚完成"消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。
二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。
2PC提交:
二阶段提交协议是将食物的提交过程分成提交事务请求和执行事务提交两个阶段进行处理。
阶段1:提交事物请求
- 事务询问:协调者向所有的参与者发送事物内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应
- 执行事务:各参与者节点执行事物操作,并将Undo和Redo信息记入事务日志中
- 如果参与者成功执事务操作,就反馈给协调者Yes响应,表示事物可以执行,如果没有成功执行事务,就反馈给协调者No响应,表示事务不可以执行
- 二阶段提交一些的阶段一夜被称为投票阶段,即各参与者投票票表明是否可以继续执行接下去的事物提交操作
阶段二:执行事物提交
- 假如协调者从所有的参与者或得反馈都是Yes响应,那马就会执行事务提交。
- 发送提交请求:协调者向所有参与者节点发出Commit请求
- 事务提交:参与者接受到Commit请求后,会正式执行事物提交操作,并在完成提交之后放弃整个事务执行期间占用的事务资源
反馈事务提交结果:参与者在完成事物提交之后,向协调者发送ACK消息 - 完成事务:协调者接收到所有参与者反馈的ACK消息后,完成事物
中断事务
- 假如任何一个参与者向协调者反馈了No响应,或者在等待超市之后,协调者尚无法接收到所有参与者的反馈响应,那么就中断事物。
- 发送回滚请求:协调者向搜优参与者节点发出Rollback请求
- 事物回滚:参与者接收到Rollback请求后,会利用其在阶段一种记录的Undo信息执行事物回滚操作,并在完成回滚之后释放事务执行期间占用的资源。
- 反馈事务回滚结果:参与则在完成事务回滚之后,向协调者发送ACK消息
中断事务:协调者接收到所有参与者反馈的ACk消息后,完成事务中断、
优缺点
- 原理简单,实现方便
- 缺点是同步阻塞,单点问题,脑裂,保守
三阶段提交(3PC):
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。
- 三阶段提,也叫三阶段提交协议,是二阶段提交(2PC)的改进版本。
- 与两阶段提交不同的是,三阶段提交有两个改动点。**引入超时机制。**同时在协调者和参与者中都引入超时机制。在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
- 阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

与两阶段提交不同的是,三阶段提交有两个改动点。
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
CanCommit阶段:
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
1、事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
2、响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。
PreCommit阶段:
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。
一、加入协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
1)发送预提交请求:协调者向参与者发送PreCommitt请求,并进入Prepared阶段。
2)事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志
3)响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
二、假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
1)发送中断请求:协调者向所有参与者发送abort请求。
2)中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
pre阶段参与者没收到请求,rollback。
doCommit阶段:
该阶段进行真正的事务提交,也可以分为以下两种情况。
一、执行提交
1)发送提交请求:协调接受到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
2)事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
3)响应反馈:事务提交完之后,向协调者发送ACK响应。
4、完成事务:协调者接收到所有参与者的ack响应之后,完成事务。
二、中断事务:协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
1)发送中断请求:协调者向所有参与者发送abort(中止)请求
2)事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
3)反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息。
4)中断事务:协调者接受到参与者反馈的ACK消息之后,执行事务的中断。
3PC(three-phase commit)即三阶段提交,是2阶段提交的改进版,其将二阶段提交协议的"提交事务请求"一分为二,形成cancommit,precommit,docommit三个阶段。
除了在2PC的基础上 增加了CanCommit阶段 ,还引入了超时机制。一旦事务参与者指定时间没有收到协调者的commit/rollback指令,就会自动本地commit,这样可以解决协调者单点故障的问题。
CanCommit阶段:
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
-
事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
-
响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
PreCommit阶段:
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
-
发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
-
事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
-
响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
-
发送中断请求 协调者向所有参与者发送abort请求。
-
中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
doCommit阶段:
该阶段进行真正的事务提交,也可以分为以下两种情况。
执行提交
-
发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
-
事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
-
响应反馈 事务提交完之后,向协调者发送Ack响应。
-
完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
-
发送中断请求 协调者向所有参与者发送abort请求
-
事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
-
反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
-
中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )
3PC存在的问题:
第一点:降低阻塞范围:
相对于二阶段提交协议,三极端提交协议的最大的优点就是降低了事务参与者的阻塞的范围,并且能够在出现单点故障后继续达成一致。对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
第二点:最后提交以前状态一致
通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
第三点:依然可能数据不一致
三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是参与者接收到 precommit 消息后,如果出现网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。
2PC与3PC的区别
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
3PC(three-phase commit)即三阶段提交,是2阶段提交的改进版,其将二阶段提交协议的"提交事务请求"一分为二,形成cancommit,precommit,docommit三个阶段。
1、3PC比2PC多了一个 CanCommit 阶段。减少了不必要的资源浪费。因为2PC在第一阶段会占用资源,而3PC在这个阶段不占用资源,只是校验一下sql,如果不能执行,就直接返回,减少了资源占用。
2、引入超时机制。同时在协调者和参与者总都引入超时机制。
- 2PC:只有协调者有超时机制,超时后,发送回滚指令。
- 3PC:协调者和参与者都有超时机制。
协调者超时:can commit,pre commit中,如果收不到参与者的反馈,则协调者向参与者发送中断指令。
参与者超时:pre commit阶段,参与者进行中断;do commit阶段,参与者进行提交。
Saga 事务:
其核心思想是:将长事务分布式事务拆分为多个本地短事务,由 Saga 事务协调器协调。
每个本地事务都有相应的执行模块 和补偿模块(对应TCC中的Confirm和Cancel),当Saga事务中任意一个本地事务出错时,调用补偿方法恢复之前的事务,达到事务最终一致性。
saga 模型由三部分组成:
- LLT(Long Live Transaction):由一个个本地事务组成的事务链**。**
- 本地事务:事务链由一个个子事务(本地事务)组成,LLT = T1+T2+T3+...+Ti。
- 补偿:每个本地事务 Ti 有对应的补偿 Ci。
Saga 的执行顺序有两种:
T1,T2,T3,...,Tn。
T1,T2,...,Tj,Cj,...,C2,C1,其中 0 < j < n 。
Saga 定义了两种恢复策略:
- 向后恢复:即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction,这种做法的效果是撤销掉之前所有成功的 sub-transation,使得整个 Saga 的执行结果撤销。
- 向前恢复:适用于必须要成功的场景,执行顺序是类似于这样的:T1,T2,...,Tj(失败),Tj(重试),...,Tn,其中 j 是发生错误的 sub-transaction。该情况下不需要 Ci。
这里要注意的是,在 Saga 模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。
Saga 模型可以满足事务的三个特性ACD:
- 原子性:Saga 协调器协调事务链中的本地事务要么全部提交,要么全部回滚。
- 一致性:Saga 事务可以实现最终一致性。
- 持久性:基于本地事务,所以这个特性可以很好实现。
Saga缺乏隔离性会带来脏读,幻读,不可重复读的问题。由于Saga 事务和 TCC 事务一样,都是强依靠业务改造,因此需要在业务设计上去解决这个问题:
- 在应⽤层⾯加⼊逻辑锁的逻辑。
- Session 层⾯隔离来保证串⾏化操作。
- 业务层⾯采⽤预先冻结数据的方式隔离此部分数据。
- 业务操作过程中通过及时读取当前状态的⽅式获取更新。
还是拿 100 元买一瓶水的例子来说,这里定义:
T1 = 扣 100 元,T2 = 给用户加一瓶水,T3 = 减库存一瓶水。
C1 = 加100元,C2 = 给用户减一瓶水,C3 = 给库存加一瓶水。
我们一次进行 T1,T2,T3 如果发生问题,就执行发生问题的 C 操作的反向。
上面说到的隔离性的问题会出现在,如果执行到 T3 这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。
这就是事务之间没有隔离性的问题。可以看见 Saga 模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。
也可以在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。(具体实例:可以参考华为的 Service Comb)
Saga对比TCC:
-
Saga和TCC都是补偿型事务;
-
Saga无法保证隔离性;
-
Saga 对业务侵入较小,只需要提供一个逆向操作的Cancel即可;而TCC需要对业务进行全局性的流程改造;
-
Saga相比TCC的缺点是缺少预留动作,导致补偿动作的实现比较麻烦:Ti就是commit,比如一个业务是发送邮件,在TCC模式下,先保存草稿(Try)再发送(Confirm),撤销的话直接删除草稿(Cancel)就行了。而Saga则就直接发送邮件了(Ti),如果要撤销则得再发送一份邮件说明撤销(Ci),实现起来有一些麻烦。如果把上面的发邮件的例子换成:A服务在完成Ti后立即发送Event到ESB(企业服务总线,可以认为是一个消息中间件),下游服务监听到这个Event做自己的一些工作然后再发送Event到ESB,如果A服务执行补偿动作Ci,那么整个补偿动作的层级就很深。
不过没有预留动作也可以认为是优点:
有些业务很简单,套用TCC需要修改原来的业务逻辑,而Saga只需要添加一个补偿动作就行了。
TCC最少通信次数为2n,而Saga为n(n=sub-transaction的数量)。
有些第三方服务没有Try接口,TCC模式实现起来就比较tricky了,而Saga则很简单。
没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单(请对比Saga的恢复策略和TCC的异常处理)。
可靠消息最终一致性:
可靠消息最终一致性方案是指当事务发起方执行完本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功(不成功就重试)(往MQ发送消息一定能成功,接收消息失败就重试,只要MQ中消息事务不丢失就一定能成功)。消息可靠!
**场景:**从业务不能导致主业务回滚。
保证MQ消息的可靠:
-
本地消息表:把消息持久化道数据库中。实现有简化版本和解耦合版本两种方式。
-
独立消息服务:引入一个独立的消息服务,来完成对消息的持久化,发送,确认,失败重试等一系列行为。
-
RocketMQ自带的事务消息:就是本地消息表。
-
RabbitMQ自带的事务消息:消息确认机制(阻塞事务)。
优势:
- 业务相对简单,不需要编写三个阶段业务。
- 是多个本地事务的结合,因此资源锁定周期短,性能好。
劣势:
- 依赖于MQ的消息可靠性。
- 消息发起者可以回滚,但是消息参与者无法引起事务回滚,
- 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况。
可靠消息一致性:
关注的是交易过程的事务一致,以异步的方式完成交易。可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
最终一致性方案:
发起通知方尽最大的努力将业务处理结果通知为接收通知方。无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠性机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
最终一致性:
并不要求参与事务的各个节点数据时刻保持一致,允许存在中间状态,只要一段时间后,能够达到数据的最终一致状态接口。
最大努力通知:
最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方(订阅发布??)。
本质是通过引入定期校验机制实现最终一致性,对业务的侵入性较低,适合于对最终一致性敏感度比较低、业务链路较短的场景。
最大努力通知解决方案:要实现最大努力通知,可以采用 MQ 的 ACK 机制。
ACK (Acknowledgement):即确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。

- 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。
- 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。
- 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
- 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
- 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。
特点
- 用到的服务模式:可查询操作、幂等操作;
- 被动方的处理结果不影响主动方的处理结果;
- 适用于对业务最终一致性的时间敏感度低的系统;
- 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;
最大努力通知方案的关键是什么?
1、有一定的消息重复通知机制。因为接受通知方可能没有接受到通知,此时要有一定的机制对消息重复通知。
2、消息校对机制。如果尽最大努力也没有通知到接受方,或者接受方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子:

交互流程:
1、账户系统调用充值系统接口
2、充值系统完成支付处理向账户系统发起充值结果通知,若通知失败,则充值系统按策略进行重复通知
3、账户系统接收到充值结果通知修改充值状态。
4、账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
通过上边的例子我们总结最大努力通知方案的目标:
目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。
具体包括:
1、有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
2、消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
Seata:
Seata:阿里开源的分布式事务框架,支持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的。
Seata(Simple Extensible Autonomous Transaction Architecture) 是 阿里巴巴开源的分布式事务中间件,以高效并且对业务 0 侵入的方式,解决微服务场景下面临的分布式事务问题。
github链接:https://github.com/seata
目前Seata还处于不断开源升级中,并不建议在线上使用,生产环境可以考虑使用阿里云商用的GTS,附上Seata目前的升级计划,可以考虑在V1.0,即服务端HA集群版本进行线上使用

Seata解决方案
解决分布式事务问题,有两个设计初衷
- 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
- 高性能:减少分布式事务解决方案所带来的性能消耗
seata中有两种分布式事务实现方案,AT及TCC
- AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
- TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
AT模式(业务侵入小)
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口
角色如下

- Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager(TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
基本处理逻辑如下

Branch就是指的分布式事务中每个独立的本地局部事务
第一阶段
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在

基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源
这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的
同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果
第二阶段
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成

如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚

TCC(高性能)
seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的协调来达到全局事务的一起提交及回滚,详情参考demo回滚

1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas的论文,讲述了saga模式如何处理长事务。
关于saga的介绍请大家参见文章,写的非常详细:
分布式事务:Saga模式
saga提供了两种实现方式,一种是编排,另一种是控制。seata的实现方式是后者。seata的控制器使用状态机驱动事务执行。
同AT模式,在saga模式下,seata也提供了RM、TM和TC三个角色。TC也是位于sever端,RM和TM位于客户端。TM用于开启全局事务,RM开启分支事务,TC监控事务运行。
在使用saga模式前,我们需要先定义好状态机,seata提供了网址可以可视化编辑状态机:
http://seata.io/saga_designer/index.html
编辑好后,将状态机以JSON格式导出文件(导出还需要我们手工编辑,我没有找到通过页面直接导出的方式)。seata提供了DbStateMachineConfig类解析状态机文件,并将解析好的内容写入数据库。这样状态机的定义以后可以直接从数据库获取。
在saga模式下,一个状态机实例就是一个全局事务,状态机中的每个状态是分支事务。
下面是seata提供的JSON格式的状态机定义例子:
json
{
"Name": "reduceInventoryAndBalance",//定义状态机的名字
"Comment": "reduce inventory then reduce balance in a transaction",
"StartState": "ReduceInventory",//定义状态机的初始状态,也就是状态机开始运行时第一个执行的状态
"Version": "0.0.1",
//下面是状态的定义
"States": {
"ReduceInventory": {
"Type": "ServiceTask",//状态类型,该类型状态表示一个分支事务
"ServiceName": "inventoryAction",//调用的bean对象名,当前版本只支持spring容器的bean
"ServiceMethod": "reduce",//调用的方法
"CompensateState": "CompensateReduceInventory",//如果事务需要回滚,那么就调用该补偿状态
"Next": "ChoiceState",//当前状态执行成功后,下一个需要执行的状态
//执行ServiceMethod方法的入参
"Input": [
"$.[businessKey]",
"$.[count]"
],
//ServiceMethod方法的返回值
"Output": {
"reduceInventoryResult": "$.#root"
},
//判断当前状态执行后,是否成功
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
}
},
//选择状态,该状态里面有一个判断,可以根据判断结果执行不同的状态
"ChoiceState":{
"Type": "Choice",
"Choices":[
{
"Expression":"[reduceInventoryResult] == true",
"Next":"ReduceBalance"
}
],
"Default":"Fail"
},
"ReduceBalance": {
"Type": "ServiceTask",
"ServiceName": "balanceAction",
"ServiceMethod": "reduce",
"CompensateState": "CompensateReduceBalance",
"Input": [
"$.[businessKey]",
"$.[amount]",
{
"throwException" : "$.[mockReduceBalanceFail]"
}
],
"Output": {
"compensateReduceBalanceResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
//如果当前状态执行过程中抛出异常,这里可以捕获异常,并执行Next状态
"Catch": [
{
"Exceptions": [
"java.lang.Throwable"
],
"Next": "CompensationTrigger"
}
],
"Next": "Succeed"
},
"CompensateReduceInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryAction",
"ServiceMethod": "compensateReduce",
"Input": [
"$.[businessKey]"
]
},
"CompensateReduceBalance": {
"Type": "ServiceTask",
"ServiceName": "balanceAction",
"ServiceMethod": "compensateReduce",
"Input": [
"$.[businessKey]"
]
},
//补偿触发器状态,该状态表示状态机进入补偿,接下来要开始执行各个状态的补偿状态了
"CompensationTrigger": {
"Type": "CompensationTrigger",
"Next": "Fail"
},
//下面两个状态是终止状态
"Succeed": {
"Type":"Succeed"
},
"Fail": {
"Type":"Fail",
"ErrorCode": "PURCHASE_FAILED",
"Message": "purchase failed"
}
}
}
下图是saga模式下状态机的运行结构:

事务运行过程中,也可能发生回滚。当全局事务需要回滚时,TC发起回滚请求,seata执行补偿状态。

当然事务执行过程中,也可以发生自动补偿。如下图:

TC通知事务回滚和事务执行失败自动补偿两个场景很类似,都是找到所有要补偿的状态,然后顺次调用对应的补偿逻辑,不同的是,第一个场景执行完所有的补偿状态就结束了,而第二个场景是执行完后还要执行CompensationTrigger类型状态的Next状态。另外补偿逻辑不能有Next状态。补偿逻辑使用的也是普通ServiceTask类型,只不过是逆向逻辑。
从上面这些可以看到,seata提供了事务执行失败后的两种处理方式,一种是不停的重试直到成功,另一种是事务回滚发起状态补偿,状态补偿是在状态机定义中设定好的。
seata在运行的过程中,创建了一个处理栈,所有将要运行的状态都在该栈中,栈顶的状态是下一个要运行的状态,当开始运行栈顶状态时,就将状态从栈中弹出。状态机初始运行时,栈中只有一个初始状态,初始状态运行完毕后切换到下一状态,这时seata将下一状态装入栈中,控制器接下来执行该状态。
在saga模式中,控制器不在TC端,而在应用端,TC只是起到监控的作用,比如监控事务执行是否超时,记录事务执行进度等。应用端通过调用状态机引擎的start方法启动状态机运行,接下来状态机可以根据定义自动切换状态直到事务执行结束。
seata运行时,在数据库中创建seata_state_inst、seata_state_machine_def、seata_state_machine_inst三张表,作用分别是保存状态实例、保存状态机定义、保存状态机实例,其中seata_state_machine_def表的数据是在启动的时候或者启动前写入数据库的,其他两张表的数据都是在事务开始运行后保存到数据库。
seata XA模式
Seata 1.2.0 版本重磅发布新的事务模式:XA 模式,实现对 XA 协议的支持。
什么是 XA?
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。
XA 规范 描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范 的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。
XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。
XA 规范 在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范 提供了支持。
什么是 Seata 的事务模式?
Seata 定义了全局事务的框架。
全局事务 定义为若干 分支事务 的整体协调:
-
TM 向 TC 请求发起(Begin)、提交(Commit)、回滚(Rollback)全局事务。
-
TM 把代表全局事务的 XID 绑定到分支事务上。
-
RM 向 TC 注册,把分支事务关联到 XID 代表的全局事务中。
-
RM 把分支事务的执行结果上报给 TC。(可选)
-
TC 发送分支提交(Branch Commit)或分支回滚(Branch Rollback)命令给 RM。
Seata 的 全局事务 处理过程,分为两个阶段:
执行阶段 :执行 分支事务,并 保证 执行结果满足是 可回滚的(Rollbackable) 和 持久化的(Durable)。
完成阶段: 根据 执行阶段 结果形成的决议,应用通过 TM 发出的全局提交或回滚的请求给 TC,TC 命令 RM 驱动 分支事务 进行 Commit 或 Rollback。
Seata 的所谓 事务模式 是指:运行在 Seata 全局事务框架下的 分支事务 的行为模式。准确地讲,应该叫作 分支事务模式。
不同的 事务模式 区别在于 分支事务 使用不同的方式达到全局事务两个阶段的目标。即,回答以下两个问题:
执行阶段 :如何执行并 保证 执行结果满足是 可回滚的(Rollbackable) 和 持久化的(Durable)。
完成阶段: 收到 TC 的命令后,如何做到分支的提交或回滚?
以我们 Seata 的 AT 模式和 TCC 模式为例来理解:
AT 模式
- 执行阶段:
-
- 可回滚:根据 SQL 解析结果,记录回滚日志
- 持久化:回滚日志和业务 SQL 在同一个本地事务中提交到数据库
- 完成阶段:
-
- 分支提交:异步删除回滚日志记录
- 分支回滚:依据回滚日志进行反向补偿更新
TCC 模式
-
执行阶段:
-
- 调用业务定义的 Try 方法(完全由业务层面保证 可回滚 和 持久化)
-
完成阶段:
-
- 分支提交:调用各事务分支定义的 Confirm 方法
- 分支回滚:调用各事务分支定义的 Cancel 方法
什么是 Seata 的 XA 模式?
XA 模式:
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
执行阶段:
可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
完成阶段:
分支提交:执行 XA 分支的 commit
分支回滚:执行 XA 分支的 rollback
补偿型事务模式的问题
本质上,Seata 已经支持的 3 大事务模式:AT、TCC、Saga 都是 补偿型 的。
补偿型 事务处理机制构建在 事务资源 之上(要么在中间件层面,要么在应用层面),事务资源 本身对分布式事务是无感知的。
事务资源 对分布式事务的无感知存在一个根本性的问题:无法做到真正的 全局一致性 。
比如,一条库存记录,处在 补偿型 事务处理过程中,由 100 扣减为 50。此时,仓库管理员连接数据库,查询统计库存,就看到当前的 50。之后,事务因为异外回滚,库存会被补偿回滚为 100。显然,仓库管理员查询统计到的 50 就是 脏 数据。
可以看到,补偿型 分布式事务机制因为不要求 事务资源 本身(如数据库)的机制参与,所以无法保证从事务框架之外的全局视角的数据一致性。
XA 的价值
与 补偿型 不同,XA 协议 要求 事务资源 本身提供对规范和协议的支持。
因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。
比如,上一节提到的库存更新场景,XA 事务处理过程中,中间态数据库存 50 由数据库本身保证,是不会仓库管理员的查询统计 看 到的。(当然隔离级别需要 读已提交 以上)
除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处:
-
业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
-
数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。
-
多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少,为不同语言开发 SDK 较之 AT 模式将更 薄,更容易。
-
传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。
XA 广泛被质疑的问题
不存在某一种分布式事务机制可以完美适应所有场景,满足所有需求。
XA 规范早在上世纪 90 年代初就被提出,用以解决分布式事务处理这个领域的问题。
现在,无论 AT 模式、TCC 模式还是 Saga 模式,这些模式的提出,本质上都源自 XA 规范对某些场景需求的无法满足。
XA 规范定义的分布式事务处理机制存在一些被广泛质疑的问题,针对这些问题,我们是如何思考的呢?
- 数据锁定:数据在整个事务处理过程结束前,都被锁定,读写都按隔离级别的定义约束起来。
java
思考:
数据锁定是获得更高隔离性和全局一致性所要付出的代价。
补偿型 的事务处理机制,在 执行阶段 即完成分支(本地)事务的提交,(资源层面)不锁定数据。而这是以牺牲 隔离性 为代价的。
另外,AT 模式使用 全局锁 保障基本的 写隔离,实际上也是锁定数据的,只不过锁在 TC 侧集中管理,解锁效率高且没有阻塞的问题。
- 协议阻塞:XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。
java
思考:
协议的阻塞机制本身并不是问题,关键问题在于 协议阻塞 遇上 数据锁定。
如果一个参与全局事务的资源 "失联" 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。
这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。
基本思路是两个方面:避免 "失联" 和 增加 "自解锁" 机制。(这里涉及非常多技术细节,暂时不展开,在后续 XA 模式演进过程中,会专门拿出来讨论)
- 性能差:性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。
java
思考:
和不使用分布式事务支持的运行场景比较,性能肯定是下降的,这点毫无疑问。
本质上,事务(无论是本地事务还是分布式事务)机制就是拿部分 性能的牺牲 ,换来 编程模型的简单 。
与同为 业务无侵入 的 AT 模式比较:
首先,因为同样运行在 Seata 定义的分布式事务框架下,XA 模式并没有产生更多事务协调的通信开销。
其次,并发事务间,如果数据存在热点,产生锁冲突,这种情况,在 AT 模式(默认使用全局锁)下同样存在的。
所以,在影响性能的两个主要方面,XA 模式并不比 AT 模式有非常明显的劣势。
AT 模式性能优势主要在于:集中管理全局数据锁,锁的释放不需要 RM 参与,释放锁非常快;另外,全局提交的事务,完成阶段 异步化。
设计目标
XA 模式的基本设计目标,两个主要方面:
- 从 场景 上,满足 全局一致性 的需求。
- 从 应用上,保持与 AT 模式一致的无侵入。
- 从 机制 上,适应分布式微服务架构的特点。
整体思路:
与 AT 模式相同的:以应用程序中 本地事务 的粒度,构建到 XA 模式的 分支事务。
通过数据源代理,在应用程序本地事务范围外,在框架层面包装 XA 协议的交互机制,把 XA 编程模型 透明化。
把 XA 的 2PC 拆开,在分支事务 执行阶段 的末尾就进行 XA prepare,把 XA 协议完美融合到 Seata 的事务框架,减少一轮 RPC 交互。
整体运行机制
XA 模式 运行在 Seata 定义的事务框架内:
- 执行阶段(E xecute):
-
- XA start/XA end/XA prepare + SQL + 注册分支
- 完成阶段(F inish):
-
- XA commit/XA rollback
数据源代理
XA 模式需要 XAConnection。
获取 XAConnection 两种方式:
-
方式一:要求开发者配置 XADataSource
-
方式二:根据开发者的普通 DataSource 来创建
第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。
第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。
我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。
类比 AT 模式的数据源代理机制,如下:
但是,第二种方法有局限:无法保证兼容的正确性。
实际上,这种方法是在做数据库驱动程序要做的事情。不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,我们只能保证在充分测试过的驱动程序上是正确的,开发者使用的驱动程序版本差异很可能造成机制的失效。
这点在 Oracle 上体现非常明显。参见 Druid issue:https://github.com/alibaba/druid/issues/3707
综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。
类比 AT 模式的数据源代理机制,如下:
分支注册
XA start 需要 Xid 参数。
这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。
目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前。
将来一个可能的优化方向:
把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。
这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。
小结
这里只通过几个重要的核心设计,说明 XA 模式的基本工作机制。
此外,还有包括 连接保持 、异常处理 等重要方面,有兴趣可以从项目代码中进一步了解。
以后会陆续写出来和大家交流。
演进规划
XA 模式总体的演进规划如下:
第 1 步(已经完成):首个版本(1.2.0),把 XA 模式原型机制跑通。确保只增加,不修改,不给其他模式引入的新问题。
第 2 步(计划 5 月完成):与 AT 模式必要的融合、重构。
第 3 步(计划 7 月完成):完善异常处理机制,进行上生产所必需的打磨。
第 4 步(计划 8 月完成):性能优化。
第 5 步(计划 2020 年内完成):结合 Seata 项目正在进行的面向云原生的 Transaction Mesh 设计,打造云原生能力。
XA 模式的使用
从编程模型上,XA 模式与 AT 模式保持完全一致。
可以参考 Seata 官网的样例:seata-xa
样例场景是 Seata 经典的,涉及库存、订单、账户 3 个微服务的商品订购业务。
在样例中,上层编程模型与 AT 模式完全相同。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换。
在当前的技术发展阶段,不存一个分布式事务处理机制可以完美满足所有场景的需求。
一致性、可靠性、易用性、性能等诸多方面的系统设计约束,需要用不同的事务处理机制去满足。
Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的 标准化 平台。
基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。
XA 模式的加入,补齐了 Seata 在 全局一致性 场景下的缺口,形成 AT、TCC、Saga、XA 四大 事务模式 的版图,基本可以满足所有场景的分布式事务处理诉求。
当然 XA 模式和 Seata 项目本身都还不尽完美,有很多需要改进和完善的地方。非常欢迎大家参与到项目的建设中,共同打造一个标准化的分布式事务平台。
说说Seata的实现原理?
在应用中Seata整体事务逻辑基于两阶段提价的模型,核心概念包含三个角色:
- TC:事务协调者,即独立运行的seata-server,用于接收事务注册,提交和回滚。
- TM:事务发起者。用来告诉TC全局事务的开始,提价,回滚。
- RM:事务资源,每一个RM都会作为一个分支事务注册在TC。
AT(Auto Transaction)模式
第一阶段
过程:
- TM 向TC申请开启一个全局事务,全局事务创建并生成一个全局唯一的XID。
- XID 在微服务调用链路的上下文中传播。
假设运行:update product set name="GTS" where name="TXC"; // id=1
1、解析SQL:得到SQL的类型(update),表(product),条件(where name = 'TXC')等相关的信息。
2、查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。select * from produc where name='TXC'
3、执行业务SQL:更新这条记录的name为'GTS'
4、查询后镜像:根据前镜像的结果,通过主键定位数据。select * from produc where name = 'TXC'
5、插入回滚日志:把前后镜像数据以及业务SQL相关的信息组成一个回滚日志记录,插入到UNDO_LOG表中。提交前,RM向TC注册分支:申请product表中,主键值等于1的记录的全局锁。
6、TM向TC发起针对XID的全局提交或回滚决议。将本地事务提交的结果上报给TC。TM向TC发起针对XID的全局提交或回滚决议。将本地事务提交的结果上报给TC
二阶段-提交:
TC调用XID下管辖的全部分支事务完成提交或回滚请求。
1、收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC。
2、异步任务阶段的分支:提交请求,将异步和批量地删除相应UNDO LOG记录。
二阶段-回滚:
TC调度XID下管辖的全部分支事务完成提交或回滚请求。
1、收到TC的分支回滚请求,开启一个本地事务,执行如下操作。
2、通过XID和Branch ID查找到相应的UNDO LOG记录。
3、数据校验:拿UNDO LOG中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改(出现脏读),转人工处理(Seata因为无法感知这个脏写如何发生,此时只能打印日志和触发异常通知,告知用户需要人工介入)
4、人工没有脏写就简单了:根据UNDO LOG中的前镜像和业务SQL的相关信息生成并执行回滚的语句:
5、提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。
分布式事务方案对比:
在学习各种分布式事务的解决方案后,我们了解到各种方案的优缺点:
2PC最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其阻塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长(long-running transactions)的分布式服务中。
如果拿TCC 事务的处理流程与2PC 两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用曾面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突,提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try,confirm,cancel三个操作。此外,其实现难度也比较大,需要按照网络状态,系统故障等不同的失败原因实现不同的回滚策略。典型的使用场景:满减,登陆送优惠券等。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解藕。典型的使用场景:注册送积分,登陆送优惠券等。
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接受通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都不会影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知,支付结果通知等。
2PC | TCC | 可靠消息 | 最大努力通知 | |
---|---|---|---|---|
一致性 | 强一致性 | 最终一致 | 最终一致 | 最终一致 |
吞吐量 | 低 | 中 | 高 | 高 |
实现复杂度 | 易 | 难 | 中 | 易 |
**总结:**在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿分布式事务与单机事务ACID做对比。
无论是数据库层的XA,还是应用层TCC,可靠消息,最大努力通知等方案,都没有完美解决分布式事务问题,它们不过是各自在性能,一致性,可用性等方面做取舍,寻求某些场景偏好下的权衡。
最大努力通知与可靠消息一致性有什么不同?
1、解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
2、两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。
最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
3、技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
方案对比:
属性 | 2PC | TCC | Saga | 本地消息表 | 事务消息 | 最大努力通知 |
---|---|---|---|---|---|---|
事务一致性 | 强 | 弱 | 弱 | 弱 | 弱 | 弱 |
复杂性 | 中 | 高 | 中 | 低 | 低 | 低 |
业务侵入性 | 小 | 大 | 小 | 中 | 中 | 中 |
使用局限性 | 大 | 大 | 中 | 小 | 中 | 中 |
性能 | 低 | 中 | 高 | 高 | 高 | 高 |
维护成本 | 低 | 高 | 中 | 低 | 中 | 中 |