一 本地事务
1 事务
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)
2 事务的特性
原子性:指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。转账前和转账后的总金额不变。
隔离性:事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性:指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
3 事务的隔离级别(多个会话单次会话事务重叠问题)
数据库通过设置事务的隔离级别防止以上情况的发生:
3.1 READ UNCOMMITTED: 赃读、不可重复读、虚读都有可能发生。
如上图所示开启两个会话,模拟两个线程同时对数据库的bankacount的表进行操作,首先我们改变数据库的隔离级别为READ UNCOMMITTED。此时在两个线程都开启事务,也就相当与两个人同时在柜台服务,都可以查看到A,B两个账户分别有1000元,此时B用户向A用户转账100元,如右图更新语句。然后找A用户确认,A用户查询一看,确是1100元。已经确认了。但是由于B用户还没有提交事务,此时B用户执行回滚操作,并且提交了。A用户再查询的时候,发现变成了1000元,这就是脏读引起的错误。也就是我们说的读未提交。
3.2 READ COMMITTED: 避免赃读。不可重复读、虚读都有可能发生。(oracle默认的)
如上图所示,为了避免脏读的发生,我们把数据库的事务隔离级别再修改高一个层次,叫做读提交,然而同样的操作,当B用户执行update更新以后,A用户再去查询的时候,发现金额并没有增加,避免了上例脏读的发生,可惜当B提交以后,A又看到了B已提交的数据,一会儿1100,一会儿1200,这样就造成了A账户在自己操作的时候,不断的被干扰,造成A用户的误操作,也就是我们说的读提交,产生了A的数据不可重复读的错误。
3.3 REPEATABLE READ:避免赃读、不可重复读。虚读有可能发生。(mysql默认)
当B账户更新,并提交以后A账户查询数据是不变了,保证了A操作不受外界打扰,可以读到重复的数据,这种隔离级别避免了脏读,不可重复读,但是却避免不了幻读,一般可以认为是insert语句,也就是A账号虽然看不到对A,B两个账户数据的更新操作,但是却可以看到两个数据的insert,delete操作,那么问题来了,刚刚的insert操作的数据,A账户并没有看到,在B账户insert之后,并没有看到insert的记录,那么mysql是不是已经在这个级别就解决了幻读的问题呢?还是REPEATABLE READ的理论有问题?
如上图所示,A账户原本查询到了两条数据,但是对这两条数据进行更新的时候,却把B账户另外一条新插入的数据给更新了,再查询的时候竟然出现了3条更新了的数据。那么原来理论是正确的,实际上类似于这个隔离级别还是没有解决幻读的问题。
那么mysql为什么B插入的数据A查不到了,给人的感觉是解决了幻读的问题呢?
这里有两个概念要说一下
一 当前读。
select ... shera mode
select ..... for update
insert/delete/update
二 快照读.
select....
如上所示执行不同的sql语句在可重复读级别下是不一样的.执行当前读的语句那么操作的是数据库的实时数据.执行快照读的语句那么操作的是历史查询快照.这就是在可重读隔离级别下的多版本并发控制.也叫做mvcc,那么什么是mvcc呢?
MVCC
要说幻读,首先要了解MVCC,MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。mvcc只在可重复读隔离级别下才会产生.那么mvcc是怎么工作的呢
如上所示读数据的规则在黄色框框中写了,如上所示开启了5个事务.事务0开启以后插入一条数据已经提交了.这个时候就看事务2查不查询数据库.如果事务2查询了数据库.那么事务2的快照就是在事务4提交数据之前生成的.事务4再提交数据那么事务2就看不到事务4提交的数据了.如果事务2在事务0提交数据以后没有查询数据,然后事务4又插入了数据那么事务2是可以查询到事务4的数据的.宗上所述得出结论.
在读提交隔离级别下: 每次读都会生成读视图,这个时候去读是可以实时读到事务的变更数据的
在可重复读级别下: 只有第一次进行快照读的时候会生成读视图,之后的所有的读操作都会用的第一次生成的读视图.
以上就是mvcc的工作原理.也就是为什么更新的时候更新到了数据,在查询的时候查不到的原因.假设是第一次查询那是可以查到更新的数据的,因为还没有生成读视图.后面的读的话会查询第一个读的数据所以这就是查不到的原因.但是更新能更新到是因为走的当前读,没有读快照所以能读到.
那么为什么出现了幻读?实际上就是在一个事务中,即使用了快照读,又使用了当前读,也就是说读的时候使用了快照读,更新的时候使用了当前读,insert,update,delete等操作了数据库做了当前读.就会导致读的时候没读到,更新的时候被更新了.这个时候再查🈶️会生成快照.
间隙锁
间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。
我们还是以user举例,假设现在user表有几条记录
e data-draft-node=“block” data-draft-type=“table” data-size=“normal” data-row-style=“normal”>
当我们执行:
begin; select * from user where age=20 for update; begin; insert into user(age) values(10); #成功 insert into user(age) values(11); #失败 insert into user(age) values(20); #失败 insert into user(age) values(21); #失败 insert into user(age) values(30); #失败
只有10可以插入成功,那么因为表的间隙mysql自动帮我们生成了区间(左开右闭)
(negative infinity,10],(10,20],(20,30],(30,positive infinity)
由于20存在记录,所以(10,20],(20,30]区间都被锁定了无法插入、删除。
如果查询21呢?就会根据21定位到(20,30)的区间(都是开区间)。
需要注意的是唯一索引是不会有间隙索引的。
3.4 SERIALIZABLE: 避免赃读、不可重复读、虚读。
级别越高,性能越低,数据越安全
如上图所示,为来更进一步的提高并发操作的事务的安全性,我们还可以设置为串行的读,如上图所示B账户在没有提交更新事务的时候,A账户开启来一个事务,然而此时A账户执行查询操作却要等待B用户提交,如图当B用户提交完更新操作以后,A用户查询的数据才能自动出来,这样的操作虽然很安全,但是却影响来性能不能做到并发操作。
小结
实际上很多的项目中是不会使用到上面的两种方法的,串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的。
Mysql如何实现事务管理呢?
3.5 Redo log/ undo log/ binlog
3.5.1 undo log
Undo Log 是为了实现事务的原子性(事物里的操作要么都完成,要么都不完成),在MySQL数据库InnoDB存储引擎中,还用Undo Log来实现多版本并发控制(简称:MVCC)。
(1)undo的原理
undo 顾名思义,就是没有做,没发生的意思。undo log 就是没有发生事情(原本事情是什么)的一些日志 我们刚刚已经说了,在准备更新一条语句的时候,该条语句已经被加载到 Buffer pool 中了,实际上这里还有这样的操作,就是在将该条语句加载到 Buffer Pool 中的时候同时会往 undo 日志文件中插入一条日志,也就是将 id=1 的这条记录的原来的值记录下来。
这样做的目的是什么?
Innodb 存储引擎的最大特点就是支持事务,如果本次更新失败,也就是事务提交失败,那么该事务中的所有的操作都必须回滚到执行前的样子,也就是说当事务失败的时候,也不会对原始数据有影响,看图说话
这里说句额外话,其实 MySQL 也是一个系统,就好比我们平时开发的 java 的功能系统一样,MySQL 使用的是自己相应的语言开发出来的一套系统而已,它根据自己需要的功能去设计对应的功能,它即然能做到哪些事情,那么必然是设计者们当初这么定义或者是根据实际的场景变更演化而来的。所以大家放平心态,把 MySQL 当作一个系统去了解熟悉他。
到这一步,我们的执行的 SQL 语句已经被加载到 Buffer Pool 中了,然后开始更新这条语句,更新的操作实际是在Buffer Pool中执行的,那问题来了,按照我们平时开发的一套理论缓冲池中的数据和数据库中的数据不一致时候,我们就认为缓存中的数据是脏数据,那此时 Buffer Pool 中的数据岂不是成了脏数据?没错,目前这条数据就是脏数据,Buffer Pool 中的记录是小强 数据库中的记录是旺财 ,这种情况 MySQL是怎么处理的呢,继续往下看
(2)用Undo Log实现原子性和持久化的事务的简化过程
假设有A、B两个数据,值分别为1,2。
A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录B=2到undo log.
E.修改B=4.
F.将undo log写到磁盘。
G.将数据写到磁盘。
H.事务提交
这里有一个隐含的前提条件:‘数据都是先读到内存中,然后修改内存中的数据,最后将数据写回磁盘’。
之所以能同时保证原子性和持久化,是因为以下特点:
A. 更新数据前记录Undo log(undo log存放在共享表空间里)。
B. 为了保证持久性,必须将数据在事务提交前写到磁盘。只要事务成功提交,数据必然已经持久化。
C. Undo log必须先于数据持久化到磁盘。如果在G,H之间系统崩溃,undo log是完整的,可以用来回滚事务。
D. 如果在A-F之间系统崩溃,因为数据没有持久化到磁盘。所以磁盘上的数据还是保持在事务开始前的状态。
缺陷:每个事务提交前将数据和Undo Log写入磁盘,这样会导致大量的磁盘IO,因此性能很低。
3.5.2 Redo log
除了从磁盘中加载文件和将操作前的记录保存到 undo 日志文件中,其他的操作是在内存中完成的,内存中的数据的特点就是:断电丢失。如果此时 MySQL 所在的服务器宕机了,那么 Buffer Pool 中的数据会全部丢失的。这个时候 redo 日志文件就需要来大显神通了
画外音:redo 日志文件是 InnoDB 特有的,他是存储引擎级别的,不是 MySQL 级别的
redo 记录的是数据修改之后的值,不管事务是否提交都会记录下来,例如,此时将要做的是update students set stuName='小强' where id=1; 那么这条操作就会被记录到 redo log buffer 中,啥?怎么又出来一个 redo log buffer ,很简单,MySQL 为了提高效率,所以将这些操作都先放在内存中去完成,然后会在某个时机将其持久化到磁盘中。
截至目前,我们应该都熟悉了 MySQL 的执行器调用存储引擎是怎么将一条 SQL 加载到缓冲池和记录哪些日志的,流程如下:
- 准备更新一条 SQL 语句
- MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载到缓冲池(BufferPool)中
- 在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo 日志文件中
- innodb 会在 Buffer Pool 中执行更新操作
- 更新后的数据会记录在 redo log buffer 中
上面说的步骤都是在正常情况下的操作,但是程序的设计和优化并不仅是为了这些正常情况而去做的,也是为了那些临界区和极端情况下出现的问题去优化设计的
这个时候如果服务器宕机了,那么缓存中的数据还是丢失了。真烦,竟然数据总是丢失,那能不能不要放在内存中,直接保存到磁盘呢?很显然不行,因为在上面也已经介绍了,在内存中的操作目的是为了提高效率。
此时,如果 MySQL 真的宕机了,那么没关系的,因为 MySQL 会认为本次事务是失败的,所以数据依旧是更新前的样子,并不会有任何的影响。
好了,语句也更新好了那么需要将更新的值提交啊,也就是需要提交本次的事务了,因为只要事务成功提交了,才会将最后的变更保存到数据库,在提交事务前仍然会具有相关的其他操作
将 redo Log Buffer 中的数据持久化到磁盘中,就是将 redo log buffer 中的数据写入到 redo log 磁盘文件中,一般情况下,redo log Buffer 数据写入磁盘的策略是立即刷入磁盘(具体策略情况在下面小总结出会详细介绍),上图
如果 redo log Buffer 刷入磁盘后,数据库服务器宕机了,那我们更新的数据怎么办?此时数据是在内存中,数据岂不是丢失了?不,这次数据就不会丢失了,因为 redo log buffer 中的数据已经被写入到磁盘了,已经被持久化了,就算数据库宕机了,在下次重启的时候 MySQL 也会将 redo 日志文件内容恢复到 Buffer Pool 中(这边我的理解是和 Redis 的持久化机制是差不多的,在 Redis 启动的时候会检查 rdb 或者是 aof 或者是两者都检查,根据持久化的文件来将数据恢复到内存中)
到此为止,从执行器开始调用存储引擎接口做了哪些事情呢?
- 准备更新一条 SQL 语句
- MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载到缓冲池(BufferPool)中
- 在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo 日志文件中
- innodb 会在 Buffer Pool 中执行更新操作
- 更新后的数据会记录在 redo log buffer 中
- MySQL 提交事务的时候,会将 redo log buffer 中的数据写入到 redo 日志文件中 刷磁盘可以通过 innodb_flush_log_at_trx_commit 参数来设置
- 值为 0 表示不刷入磁盘
- 值为 1 表示立即刷入磁盘
- 值为 2 表示先刷到 os cache
- myslq 重启的时候会将 redo 日志恢复到缓冲池中
3.5.3 Undo/redo
假设有A、B两个数据,值分别为1,2.
A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录A=3到redo log.
E.记录B=2到undo log.
F.修改B=4.
G.记录B=4到redo log.
H.将redo log写入磁盘。
I.事务提交
Undo + Redo事务的特点:
A. 为了保证持久性,必须在事务提交前将Redo Log持久化。
B. 数据不需要在事务提交前写入磁盘,而是缓存在内存中。
C. Redo Log 保证事务的持久性。
D. Undo Log 保证事务的原子性。
E. 有一个隐含的特点,数据必须要晚于redo log写入持久存储。
Undo + Redo的设计主要考虑的是:提升IO性能。虽说通过缓存数据,减少了写数据的IO,但是却引入了新的IO,即写Redo Log的IO。如果Redo Log的IO性能不好,就不能起到提高性能的目的。*
为了保证Redo Log能够有比较好的IO性能,InnoDB 的 Redo Log的设计有以下几个特点:
A. 尽量保持Redo Log存储在一段连续的空间上。因此在系统第一次启动时就会将日志文件的空间完全分配(也即是ib_logfile*文件,初始化实例时就分配好空间了)。以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。
B. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时(如事务提交),将许多日志一起写入磁盘.
C. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,以减少日志占用的空间。例如,Redo Log中的记录内容可能是这样的:
记录1: <trx1, insert …>
记录2: <trx2, update …>
记录3: <trx1, delete …>
记录4: <trx3, update …>
记录5: <trx2, insert …>
D. 因为C的原因,当一个事务将Redo Log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。
E. Redo Log上只进行顺序追加的操作,当一个事务需要回滚时,它的Redo Log记录也不会从Redo Log中删除掉。
截止到目前位置,MySQL 的执行器调用存储引擎的接口去执行【执行计划】提供的 SQL 的时候 InnoDB 做了哪些事情也就基本差不多了,但是这还没完。下面还需要介绍下 MySQL 级别的日志文件 bin log
3.5.4 binlog
上面介绍到的redo log是 InnoDB 存储引擎特有的日志文件,而bin log属于是 MySQL 级别的日志。redo log记录的东西是偏向于物理性质的,如:“对什么数据,做了什么修改”。bin log是偏向于逻辑性质的,类似于:“对 students 表中的 id 为 1 的记录做了更新操作” 两者的主要特点总结如下:
性质 | redo Log | bin Log |
---|---|---|
文件大小 | redo log 的大小是固定的(配置中也可以设置,一般默认的就足够了) | bin log 可通过配置参数max_bin log_size设置每个bin log文件的大小(但是一般不建议修改)。 |
实现方式 | redo log是InnoDB引擎层实现的(也就是说是 Innodb 存储引擎独有的) | bin log是 MySQL 层实现的,所有引擎都可以使用 bin log日志 |
记录方式 | redo log 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志。 | bin log 通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上 |
使用场景 | redo log适用于崩溃恢复(crash-safe)(这一点其实非常类似与 Redis 的持久化特征) | bin log 适用于主从复制和数据恢复 |
bin log文件是如何刷入磁盘的?
bin log 的刷盘是有相关的策略的,策略可以通过sync_bin log来修改,默认为 0,表示先写入 os cache,也就是说在提交事务的时候,数据不会直接到磁盘中,这样如果宕机bin log数据仍然会丢失。所以建议将sync_bin log设置为 1 表示直接将数据写入到磁盘文件中。
刷入 bin log 有以下几种模式
- STATMENT
基于 SQL 语句的复制(statement-based replication, SBR),每一条会修改数据的 SQL 语句会记录到 bin log 中
【优点】:不需要记录每一行的变化,减少了 bin log 日志量,节约了 IO , 从而提高了性能
【缺点】:在某些情况下会导致主从数据不一致,比如执行sysdate()、sleep()等
- ROW
基于行的复制(row-based replication, RBR),不记录每条SQL语句的上下文信息,仅需记录哪条数据被修改了
【优点】:不会出现某些特定情况下的存储过程、或 function、或 trigger 的调用和触发无法被正确复制的问题
【缺点】:会产生大量的日志,尤其是 alter table 的时候会让日志暴涨
- MIXED
基于 STATMENT 和 ROW 两种模式的混合复制( mixed-based replication, MBR ),一般的复制使用 STATEMENT 模式保存 bin log ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 bin log
那既然bin log也是日志文件,那它是在什么记录数据的呢?
其实 MySQL 在提交事务的时候,不仅仅会将 redo log buffer 中的数据写入到redo log 文件中,同时也会将本次修改的数据记录到 bin log文件中,同时会将本次修改的bin log文件名和修改的内容在bin log中的位置记录到redo log中,最后还会在redo log最后写入 commit 标记,这样就表示本次事务被成功的提交了。
如果在数据被写入到bin log文件的时候,刚写完,数据库宕机了,数据会丢失吗?
首先可以确定的是,只要redo log最后没有 commit 标记,说明本次的事务一定是失败的。但是数据是没有丢失了,因为已经被记录到redo log的磁盘文件中了。在 MySQL 重启的时候,就会将 redo log 中的数据恢复(加载)到Buffer Pool中。
好了,到目前为止,一个更新操作我们基本介绍得差不多,但是你有没有感觉少了哪件事情还没有做?是不是你也发现这个时候被更新记录仅仅是在内存中执行的,哪怕是宕机又恢复了也仅仅是将更新后的记录加载到Buffer Pool中,这个时候 MySQL 数据库中的这条记录依旧是旧值,也就是说内存中的数据在我们看来依旧是脏数据,那这个时候怎么办呢?
其实 MySQL 会有一个后台线程,它会在某个时机将我们Buffer Pool中的脏数据刷到 MySQL 数据库中,这样就将内存和数据库的数据保持统一了。
到此,关于Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及关系就基本差不多了。
我们再回顾下
- Buffer Pool 是 MySQL 的一个非常重要的组件,因为针对数据库的增删改操作都是在 Buffer Pool 中完成的
- Undo log 记录的是数据操作前的样子
- redo log 记录的是数据被操作后的样子(redo log 是 Innodb 存储引擎特有)
- bin log 记录的是整个操作记录(这个对于主从复制具有非常重要的意义)
从准备更新一条数据到事务的提交的流程描述
- 首先执行器根据 MySQL 的执行计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中
- 在数据被缓存到缓存池的同时,会写入 undo log 日志文件
- 更新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中
- 完成以后就可以提交事务,在提交的同时会做以下三件事
- 将redo log buffer中的数据刷入到 redo log 文件中
- 将本次操作记录写入到 bin log文件中
- 将 bin log 文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记
至此表示整个更新事务已经完成
二 分布式事物
1 什么是分布式事务
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
如果订单系统添加订单成功了,库存系统减库存失败了,怎么回滚订单系统保证数据的一致性? 如果换成消息中间件,一定能保证数据一致性吗?
2 分布式理论(CPA理论)
CAP由Eric Brewer在2000年PODC会议上提出[1][2],是Eric Brewer在Inktomi[3]期间研发搜索引擎、分布式web缓存时得出的关于数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)的猜想:
(C) 数据一致性(consistency):也就是业务代码与数据库操作的原子性
(A) 可用性(availability):所有读写请求在一定时间内得到响应,可终止、不会一直等待
(P) 分区容错性(partition-tolerance):多个可以进行负载均衡的节点
一般只能同时满足两个,比如CA不能满足P原因是,保证了事务的完整性,又要保证及时响应,那么这样的系统就做不到多个分区,多个服务。因为服务越多调用链越长,导致延迟。但是我们现在的系统都是微服务架构的,也就是说我们必须要保证P多个节点。那么我们就只能保证AP多节点,一定时间返回,可用。当然就不能完全保证一致性了。
3 分布式理论(Base理论)
BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。
3.1 基本可用
指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务,等。总结支持分区失败。
3.2 软状态
软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。总结允许短时间内不同步。
3.3 最终一致性
系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
Base理论为柔性事务
4 分布式事务方法论
4.1 解决方案与场景
可靠消息服务 (跨平台,跨系统,有最终一致性)
最大努力通知 (跨系统,有最终一致性)
分布式事务框架(TCC,LCN) (跨服务,有强一致性要求高)
MQ简单通知 (跨平台,跨服务,跨系统,有弱一致性要求)
4.2 保证最终一致性
4.2.1 幂等性
同一个业务的同一笔交易重复提交,对于同一笔业务由于网络问题,重复提交时保证这n次操作的结果是一致的
4.2.2 补偿机制
允许不实时的一致性,在保证本地事务一致性的基础上,对未处理或发送到另一个服务的消息可以采取补发,或者定时发送。
4.2.3 可查询服务
允许业务系统之间的消息传输失败,在服务的主动方提供业务订单的查询接口,业务的被动方可以定时对业务数据进行查询,核对业务数据的正确性。
5 分布式事务解决方案
5.1 可靠消息服务(最终一致性方案)
如上图所示,是我们正常的跨系统,跨服务的消息通信流程。生产者通过1发送消息到消息中间件,消息中间件持久化消息以后,消费者监听消息队列获取消息,为防消息中间件2消息的不断重发。我们消费者方做第三次消息的确认。这样就是我们日常的消息通信流程。这样的系统面临什么样的事务问题
1 生产方自身业务执行失败,消息发送出去了,造成数据不一致,不符合C理论。
2 生产方业务执行成功了,但是由于1网络异常,造成消息丢失,数据不一致,不符合C理论。
3 生产者方发送消息1处网络正常,消息中间件挂了,消息丢失,数据不一致,不符合C理论。
4 如果2或3处异常,不断的进行重发,虽然可以控制幂等性问题,但是系统开销太大。
以上三个地方就是基于消息中间件来调用分布式系统造成的消息不一致性问题的存在之处。那么这个时候我们就思考用什么样的方式来保证数据一致性呢?
那么这个时候我们就会想到这样变通一下流程。
1我们通过1给消息中间件发一个预处理的消息
2消息中间件通过6 对消息进行持久化,持久化完成以后通过4通知生产者。
3 生产者收到消息以后执行本地业务代码,如果没有收到消息,证明预处理消息发送失败网络不正常或者消息中间件没有在线,为保证数据一致性,就不执行本地业务。
4 生产者收到消息中间件给回来的预处理消息持久化返回结果之后,执行生产者业务代码,做8同步数据到数据库
5 如果8成功则通过确认发送消息,消息中间件此时就可以通过2进行消息的投递
6 如果8失败以后,那么就发送5给消息中间件进行7删除消息。这个过程就是我们能想到的变通方案。当然也会有异常。
这样的系统面临什么样的事务问题
1 正常流程没问题了,那么异常流程怎么处理。
2 如果消息中间件能够收到消息并持久化了,那么我们怎么保证它能够准确投递。
3 有没有这样的消息中间件支持准确投递与持久化通知。
其实针对如上所示的系统,任何一个流程都有可能出现问题,
如上表格所示是整个变通以后的异常流程存在的地方。
经过如上分析所示我们可以看出数据不一致主要发生在如上图所示画框的地方,好的那么对与解决这个问题。我们可以假定消息中间件,支持对预处理持久化以后的消息进行查询服务,这样的话就可以通过查询消息中间件中预处里的消息,进行定时的重做了。这样就可以保证最终一致性方案了。
问题 消息中间件支持查询服务?
最终结论,常规的消息中间件无法满足我们的事务一致性的要求,也不支持消息的定向投递。改造消息中间件成本太大,不值得。
5.2 本地消息服务(最中一致性)
如上图所示我们把分布式的事务进行拆解成两个独立的事务。把消息的确认与持久话独立出来到生产者方。
1 生产者通过1完成业务,并且把消息持久话到本地数据库。
2 定时任务调度系统通过3查询数据库中的预处理消息,通过4发送到MQ。
3 MQ通过5发送到消费系统,消费系统通过7完成MQ的消息确认
4 消费系统,调用生产者系统6接口通知生产者通过2删除处理完成的消息。
如上图所示的本地消息服务系统就可以处理网络问题的异常流程。因为有定时任务系统不断的检查本地库中预处理的消息,所以总可以进行消息的重发与重复确认。只要处理好幂等性问题就可以了。这个方案可以。但是这个方案不利于消息系统的复用。消息补偿等的操作。接下来我们就看看独立的消息服务。
5.3 独立消息服务(最终一致性)
如上图所示是独立消息服务子系统,整个实现流程
1 生产者通过1发送预处理消息到,消息服务系统,对预处里消息进行9存储
2 消息服务子系统通过2通知生产者系统做业务处理
3 生产者系统通过3调用消息服务系统确认并发送消息
4 经过4,5MQ进行消息的发送到消费者.消费者消费以后,ack 6;
5 消费者消费完以后,调用消息服务子系统对已确认的消息进行删除,分别表示7 8;
这个过程中会产生异常,但是可以通过定时任务调度系统检查消息服务子系统中的预处理的消息,对生产者的业务进行重做,进行消息的确认发送。也可以通过任务调度子系统定时的检测消息服务子系统中的确认了的消息进行重新发送。直到消息被删除为止。当然我们也可以对4,5发送的次数做记录。标记消息为死亡消息,进行手动补偿重发。
上图所示的分布式事务处理方案,相对比较独立,解耦了大部分的服务。有了独立的消息服务系统。这样就可以通过客户端界面化程序对消息进行监控。对预处理。已确认未删除的消息。进行手动的补偿,或者批量确认重发。保证事务的最终一致性。
5.4 最大努力通知(弱一致性解决方案)
如上图所示,我们通过打造一个通知服务与MQ对接
1 通过MQ对接通知服务,并确认消息收到
2 通知服务通过制定一定时间间隔的消息发送机制 通过3去通知商户
3 商户确认被通知到以后通过4给与反馈,通知服务自动修改通知消息的状态
4 如果达到5次通知还没得到反馈,这个时候通知服务就不再通知了
5 在通知服务方,提供可以查询的接口。供给商户查询校对。
这个方案是弱一致性分布式事务解决方案,这方案不能保证消息的发送成功。提供了消息查询服务的接口。供给消息的校对。相当于为事务的最终一致性。预留了缺口。
案例 支付宝支付流程 最大努力通知
6 XA (SEATA)
有这样一个场景,需要 update 多个数据源的数据。 假如数据源 A,有三天 update 语句分别为 a,b,c,数据源 B 的 update 语句为 d,e。
@Transactional(transationManager = "transactionManagerA",rollback = Exception.class)
public void updateA(){
a();
b();
updateB();
c();
}
@Transactional(transationManager = "transactionManagerB",rollback = Exception.class)
public void updateB(){
d();
e();
}
这种场景下e或者d 异常,a,b,c会回滚吗? a,b,c 异常 那么updateB()会回滚吗?如果不会,那么我们怎么解决这个问题呢 ?
6.1 什么是XA
XA协议是”两阶段提交”,是基于X/OPEN(DPT)的一个分布式事务模型。 模型成员有三种:事务的发起者(如下图的APP),事务的协调管理者(TM),资源管理者(就是数据库,RM)。
1、两阶段意思是:
a、阶段一为准备(prepare)阶段。即所有的RM参与者准备执行事务并锁住需要的资源(其实这时、RM已经在快照上执行了语句,但没有提交)。参与者,向transaction manager报告已准备就绪。
b、第二阶段是执行阶段,协调者TM根据所有参与者RM的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚;
优点
2PC/XA 提供了一套完整的分布式事务的解决方案,遵循事务严格的 ACID 特性。
缺点
a.主要业务都在协调服务上处理,比如“下单代码、积分变动代码”的实现,协调服务需要链接积 分和订单两个数据库(RM)(微服务中,每个数据库种类不一定一样)。一般来说某个系统内部 如果出现跨多个库的这么一个操作,是不合规的。现在微服务,一个大的系统分成几十个甚至几百个服务。一般来说,我们的规定和规范,是要求每个服务只能操作自己对应的一个数据库。
b.并且不是所有的数据库都支持XA协议。
c.性能很低。
下面看一个XA代码的例子
public class DemoApplication {
public static void main(String[] args) throws XAException {
XAResource rm1 = null;
XAResource rm2 = null;
Xid xid1 = null;
Xid xid2 = null;
try {
//注册驱动
String url_mobile_gold="jdbc:mysql://127.0.0.1:3306/xa_db1";
String account_mobile_gold="root";
String pass_mobile_gold="";
//创建连接
Connection connection_mobile_gold= DriverManager.getConnection(url_mobile_gold,account_mobile_gold,pass_mobile_gold);
XAConnection xaConn1 = new MysqlXAConnection(
(com.mysql.jdbc.Connection) connection_mobile_gold,
true);
rm1 = xaConn1.getXAResource();
//注册驱动
String url_mobile_task="jdbc:mysql://127.0.0.1:3306/xa_db2";
String account_mobile_task="root";
String pass_mobile_task="";
Connection connection_mobile_task= DriverManager.getConnection(url_mobile_task,account_mobile_task,pass_mobile_task);
//创建连接
XAConnection xaConn2 = new MysqlXAConnection(
(com.mysql.jdbc.Connection) connection_mobile_task,
true);
rm2 = xaConn2.getXAResource();
byte[] gtrid = "g12345".getBytes();
byte[] bqual1 = "b00001".getBytes();
byte[] bqual2 = "b00002".getBytes();
int formate=1;
xid1=new MysqlXid(gtrid,bqual1,formate);
xid2=new MysqlXid(gtrid,bqual2,formate);
String sql1="insert into `xa_table`(`name`) values ('bc')";
String sql2="insert into `xa_table`(`name`) values ('cd')";
//xa连接资源二
rm2.start(xid2,XAResource.TMNOFLAGS);
PreparedStatement preparedStatement2 = connection_mobile_task.prepareStatement(sql1);
preparedStatement2.execute();
rm2.end(xid2,XAResource.TMSUCCESS);
//xa连接资源一
rm1.start(xid1,XAResource.TMNOFLAGS);
PreparedStatement preparedStatement1 = connection_mobile_gold.prepareStatement(sql2);
preparedStatement1.execute();
rm1.end(xid1,XAResource.TMSUCCESS);
int prepare1 = rm1.prepare(xid1);
int prepare2 = rm2.prepare(xid2);
if(prepare1==XAResource.XA_OK&&prepare2==XAResource.XA_OK){
rm1.commit(xid1,false);
rm2.commit(xid2,false);
}else{
connection_mobile_gold.close();
connection_mobile_task.close();
throw new RuntimeException();
}
}catch (Exception e){
rm2.rollback(xid2);
rm1.rollback(xid1);
}
}
}
下图是上面代码的运行结果,可以看到它进行了很强的一致性操作。也就是我们说的事务的强一致性。
缺点
同步阻塞,分两阶段提交,对全局事务资源锁的释放取决于所有资源管理器处理事务的总耗时,并发情况下效率较低,尤其是在对某些公共数据进行频繁修改操作(排它锁独占资源,等到事务被提交才会被释放),如何解决?下面我们将对XA进行深入的讨论。
6.2 Mycat弱XA
首先事务内的SQL在各自的分片上执行并返回状态码,若某个分片上的返回码为ERROR,则Mycat认为事务失败,应用端只能回滚(rollback)事务,Mycat收到回滚指令后,依次回滚事务中涉及到的所有分片;若事务中的所有SQL的执行都返回成功(OK)的返回码,则应用程序提交事务的时候,Mycat会同时向事务中涉及到的节点发送提交事务的指令。
set autocommit=0 //应用程序指令
update person set name=‘xxxx’ where age >18
commit
如果person表跨分片(dn1,dn2,dn3),则上述SQL将触发如下的执行逻辑
for( dn1,dn2,dn3){
set autocommit=0;
update person set name='xxxx' where age >18;
xa end xaid;xa prepare xaid;
}
if(allOK){
for(dn1,dn2,dn3){
commit; }
}
这里称之为弱XA,是因为第二阶段Commit的时候,若某个节点出错了,也无法等节点恢复以后去做Recover操作重新commit(只能回滚,而不会等其恢复后再次提交。),但考虑到所有的节点都执行成功,但Commit指令失败的概率很小,因此这种弱XA事务也已经满足大多数应用的需求,而且性能接近普通事务。
// SetHandler.java
public static void handle(String stmt, ServerConnection c, int offset) {
int rs = ServerParseSet.parse(stmt, offset);
switch (rs & 0xff) {
// ... 省略代码
case XA_FLAG_ON: {
if (c.isAutocommit()) {
c.writeErrMessage(ErrorCode.ERR_WRONG_USED,
"set xa cmd on can't used in autocommit connection ");
return;
}
c.getSession2().setXATXEnabled(true);
c.write(c.writeToBuffer(OkPacket.OK, c.allocate()));
break;
}
case XA_FLAG_OFF: {
c.writeErrMessage(ErrorCode.ERR_WRONG_USED,
"set xa cmd off not for external use ");
return;
}
// ... 省略代码
}
}
// NonBlockingSession.java
public void setXATXEnabled(boolean xaTXEnabled) {
if (xaTXEnabled) {
if (this.xaTXID == null) {
xaTXID = genXATXID(); /获得 XA 事务编号
}
} else {
this.xaTXID = null;
}
}
private String genXATXID() {
return MycatServer.getInstance().getXATXIDGLOBAL();
}
// MycatServer.java
public String getXATXIDGLOBAL() {
return "'" + getUUID() + "'";
}
public static String getUUID() {
String s = UUID.randomUUID().toString();
return s.substring(0, 8) + s.substring(9, 13) + s.substring(14, 18) + s.substring(19, 23) + s.substring(24);
}
当 MyCAT 接收到 set xa = on 命令时,开启 XA 事务,并生成 XA 事务编号。XA 事务编号生成算法为 UUID。 向某个数据节点第一次发起 更新的SQL 时,会在 SQL 前面附加 XA START 'xaTranId',并设置该数据节点连接事务状态为 TxState.TX_STARTED_STATE
// MySQLConnection.java
private void synAndDoExecute(String xaTxID, RouteResultsetNode rrn,int clientCharSetIndex, int clientTxIsoLation,boolean clientAutoCommit) {
String xaCmd = null;
boolean conAutoComit = this.autocommit;
String conSchema = this.schema;
// never executed modify sql,so auto commit
boolean expectAutocommit = !modifiedSQLExecuted || isFromSlaveDB() || clientAutoCommit;
if (expectAutocommit == false && xaTxID != null && xaStatus == TxState.TX_INITIALIZE_STATE) {
xaCmd = "XA START " + xaTxID + ';';
this.xaStatus = TxState.TX_STARTED_STATE;
}
// .... 省略代码
StringBuilder sb = new StringBuilder();
// .... 省略代码
if (xaCmd != null) {
sb.append(xaCmd);
}
// and our query sql to multi command at last
sb.append(rrn.getStatement() + ";");
// syn and execute others
this.sendQueryCmd(sb.toString());
}
SET names utf8;SET autocommit=0;XA START '1f2da7353e8846e5833b8d8dd041cfb1','db2';insert into t_user(id, username, password) VALUES (3400, 'b7c5ec1f-11cc-4599-851c-06ad617fec42', 'd2694679-f6a2-4623-a339-48d4a868be90');
单节点事务 or 多节点事务
COMMIT 执行时,MyCAT 会判断 XA 事务里,涉及到的数据库节点数量。
如果节点数量为 1,单节点事务,使用 CommitNodeHandler 处理。
如果节点数量 > 1,多节点事务,使用 MultiNodeCoordinator 处理。
CommitNodeHandler 相比 MultiNodeCoordinator 来说,只有一个数据节点,不需要进行多节点协调,逻辑会相对简单,有兴趣的同学可以另外看。我们主要分析 MultiNodeCoordinator。
// CoordinatorLogEntry :协调者日志
public class CoordinatorLogEntry implements Serializable {
/**
* XA 事务编号
*/
public final String id;
/**
* 参与者日志数组
*/
public final ParticipantLogEntry[] participants;
}
// ParticipantLogEntry :参与者日志
public class ParticipantLogEntry implements Serializable {
/**
* XA 事务编号
*/
public String coordinatorId;
/**
* 数据库 uri
*/
public String uri;
/**
* 过期描述
*/
public long expires;
/**
* XA 事务状态
*/
public int txState;
/**
* 参与者名字
*/
public String resourceName;
}
[{"id":"'e827b3fe666c4d968961350d19adda31'","participants":[{"uri":"127.0.0.1","state":"3","expires":0,"resourceName":"db3"},{"uri":"127.0.0.1","state":"3","expires":0,"resourceName":"db1"}]}
{"id":"'f00b61fa17cb4ec5b8264a6d82f847d0'","participants":[{"uri":"127.0.0.1","state":"3","expires":0,"resourceName":"db2"},{"uri":"127.0.0.1","state":"3","expires":0,"resourceName":"db1"}]}
MultiNodeCoordinator 重点
6.2.1 发起 PREPARE
public void executeBatchNodeCmd(SQLCtrlCommand cmdHandler) {
//省略部分代码.....
for (RouteResultsetNode rrn : session.getTargetKeys()) {
if (rrn == null) {
continue;
}
final BackendConnection conn = session.getTarget(rrn);
if (conn != null) {
conn.setResponseHandler(this);
//process the XA_END XA_PREPARE Command
MySQLConnection mysqlCon = (MySQLConnection) conn;
String xaTxId = null;
if (session.getXaTXID() != null) {
xaTxId = session.getXaTXID() + ",'" + mysqlCon.getSchema() + "'";
}
if (mysqlCon.getXaStatus() == TxState.TX_STARTED_STATE) { // XA 事务
//recovery Log
participantLogEntry[started] = new ParticipantLogEntry(xaTxId, conn.getHost(), 0, conn.getSchema(), ((MySQLConnection) conn).getXaStatus());
String[] cmds = new String[]{"XA END " + xaTxId, // XA END 命令
"XA PREPARE " + xaTxId}; // XA PREPARE 命令
mysqlCon.execBatchCmd(cmds);
} else { // 非 XA 事务
// recovery Log
participantLogEntry[started] = new ParticipantLogEntry(xaTxId, conn.getHost(), 0, conn.getSchema(), ((MySQLConnection) conn).getXaStatus());
cmdHandler.sendCommand(session, conn);
} ++started; }}
// xa recovery log
if (session.getXaTXID() != null) {
CoordinatorLogEntry coordinatorLogEntry = new CoordinatorLogEntry(session.getXaTXID(), false, participantLogEntry);
inMemoryRepository.put(session.getXaTXID(), coordinatorLogEntry);
fileRepository.writeCheckpoint(inMemoryRepository.getAllCoordinatorLogEntries());
}
if (started < nodeCount) { // TODO 疑问:如何触发
runningCount.set(started);
LOGGER.warn("some connection failed to execute " + (nodeCount - started));
/**
* assumption: only caused by front-end connection close. <br/>
* Otherwise, packet must be returned to front-end
*/
failed.set(true);
}
}
向各数据节点发送 XA END + XA PREPARE 指令。
XA END '4cbb18214d0b47adbdb0658598666677','db3';XA PREPARE'4cbb18214d0b47adbdb0658598666677','db3';
6.2.2 发起 COMMIT
@Override
public void okResponse(byte[] ok, BackendConnection conn) {
// process the XA Transatcion 2pc commit
if (conn instanceof MySQLConnection) {
MySQLConnection mysqlCon = (MySQLConnection) conn;
switch (mysqlCon.getXaStatus()) {
case TxState.TX_STARTED_STATE:
//if there have many SQL execute wait the okResponse,will come to here one by one
//should be wait all nodes ready ,then send xa commit to all nodes.
if (mysqlCon.batchCmdFinished()) {
String xaTxId = session.getXaTXID();
String cmd = "XA COMMIT " + xaTxId + ",'" + mysqlCon.getSchema() + "'";
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Start execute the cmd :" + cmd + ",current host:" + mysqlCon.getHost() + ":" + mysqlCon.getPort());
}
CoordinatorLogEntry coordinatorLogEntry = inMemoryRepository.get(xaTxId);
for (int i = 0; i < coordinatorLogEntry.participants.length; i++) {
LOGGER.debug("[In Memory CoordinatorLogEntry]" + coordinatorLogEntry.participants[i]);
if (coordinatorLogEntry.participants[i].resourceName.equals(conn.getSchema())) {
coordinatorLogEntry.participants[i].txState = TxState.TX_PREPARED_STATE; }
}
inMemoryRepository.put(xaTxId, coordinatorLogEntry);
fileRepository.writeCheckpoint(inMemoryRepository.getAllCoordinatorLogEntries());
// send commit
mysqlCon.setXaStatus(TxState.TX_PREPARED_STATE);
mysqlCon.execCmd(cmd); }
return;
case TxState.TX_PREPARED_STATE: {
// recovery log
String xaTxId = session.getXaTXID();
CoordinatorLogEntry coordinatorLogEntry = inMemoryRepository.get(xaTxId);
for (int i = 0; i < coordinatorLogEntry.participants.length; i++) {
if (coordinatorLogEntry.participants[i].resourceName.equals(conn.getSchema())) {
coordinatorLogEntry.participants[i].txState = TxState.TX_COMMITED_STATE;
}
}
inMemoryRepository.put(xaTxId, coordinatorLogEntry);
fileRepository.writeCheckpoint(inMemoryRepository.getAllCoordinatorLogEntries());
// XA reset status now
mysqlCon.setXaStatus(TxState.TX_INITIALIZE_STATE);
break;
}
default:
}
// 释放连接
if (this.cmdHandler.relaseConOnOK()) {
session.releaseConnection(conn);
} else {
session.releaseConnectionIfSafe(conn, LOGGER.isDebugEnabled(), false);
}
// 是否所有节点都完成commit,如果是,则返回Client 成功
if (this.finished()) {
cmdHandler.okResponse(session, ok);
if (cmdHandler.isAutoClearSessionCons()) {
session.clearResources(false);
}
/* 1. 事务提交后,xa 事务结束 */
if (session.getXaTXID() != null) {
session.setXATXEnabled(false);
}
/* 2. preAcStates 为true,事务结束后,需要设置为true。preAcStates 为ac上一个状态 */
if (session.getSource().isPreAcStates()) {
session.getSource().setAutocommit(true);
}
}
}
mysqlCon.batchCmdFinished() 每个数据节点,第一次返回的是 XA END 成功,第二次返回的是 XA PREPARE。在 XA PREPARE 成功后,记录该数据节点的参与者日志状态为 TxState.TX_PREPARED_STATE。之后,向该数据节点发起 XA COMMIT 命令。
XA COMMIT 返回成功后,记录该数据节点的事务参与者日志状态为 TxState.TX_COMMITED_STATE。当所有数据节点(参与者)都执行完成 XA COMMIT 返回,即 this.finished() == true,返回 MySQL Client XA 事务提交成功。
6.2.3 启动回滚
MyCAT 启动时,会回滚处于TxState.TX_PREPARED_STATE的 ParticipantLogEntry 对应的数据节点的 XA 事务。
// MycatServer.java
private void performXARecoveryLog() {
// fetch the recovery log
CoordinatorLogEntry[] coordinatorLogEntries = getCoordinatorLogEntries();
for (int i = 0; i < coordinatorLogEntries.length; i++) {
CoordinatorLogEntry coordinatorLogEntry = coordinatorLogEntries[i];
boolean needRollback = false;
for (int j = 0; j < coordinatorLogEntry.participants.length; j++) {
ParticipantLogEntry participantLogEntry = coordinatorLogEntry.participants[j];
if (participantLogEntry.txState == TxState.TX_PREPARED_STATE) {
needRollback = true;
break;
}
}
if (needRollback) {
for (int j = 0; j < coordinatorLogEntry.participants.length; j++) {
ParticipantLogEntry participantLogEntry = coordinatorLogEntry.participants[j];
//XA rollback
String xacmd = "XA ROLLBACK " + coordinatorLogEntry.id + ';';
OneRawSQLQueryResultHandler resultHandler = new OneRawSQLQueryResultHandler(new String[0], new XARollbackCallback());
outloop:
for (SchemaConfig schema : MycatServer.getInstance().getConfig().getSchemas().values()) {
for (TableConfig table : schema.getTables().values()) {
for (String dataNode : table.getDataNodes()) {
PhysicalDBNode dn = MycatServer.getInstance().getConfig().getDataNodes().get(dataNode);
if (dn.getDbPool().getSource().getConfig().getIp().equals(participantLogEntry.uri)
&& dn.getDatabase().equals(participantLogEntry.resourceName)) {
//XA STATE ROLLBACK
participantLogEntry.txState = TxState.TX_ROLLBACKED_STATE;
SQLJob sqlJob = new SQLJob(xacmd, dn.getDatabase(), resultHandler, dn.getDbPool().getSource());
sqlJob.run();
break outloop;
}}}}}}}
// init into in memory cached
for (int i = 0; i < coordinatorLogEntries.length; i++) {
MultiNodeCoordinator.inMemoryRepository.put(coordinatorLogEntries[i].id, coordinatorLogEntries[i]);
}
// discard the recovery log
MultiNodeCoordinator.fileRepository.writeCheckpoint(MultiNodeCoordinator.inMemoryRepository.getAllCoordinatorLogEntries());
}
XA 事务定义,需要等待所有参与者全部 XA PREPARE 成功完成后发起 XA COMMIT。目前 MyCAT 是某个数据节点 XA PREPARE 完成后立即进行 XA COMMIT。比如说:第一个数据节点提交了 XA END;XA PREPARE 时,第二个数据节在进行 XA END;XA PREAPRE; 前挂了,第一个节点依然会 XA COMMIT 成功,然而这个时候作为TM我们要考虑的是,第二个节点恢复以后,如何做XA commit Mycat则是直接回滚了,所以我们说Mycat是弱XA,区别于atomiks的强XA
那么多数据库有没有标准去实现呢?
6.3 JTA 规范事务模型
Java事务API(JTA:Java Transaction API)和它的同胞Java事务服务(JTS:Java Transaction Service),为J2EE平台提
供了分布式事务服务(distributed transaction)的能力。 某种程度上,可以认为JTA规范是XA规范的Java版,其把XA规范中规定的DTP模型交互接口抽象成Java接口中的方法,并规定每个方法要实现什么样的功能。
在DTP模型中,规定了模型的五个组成元素:应用程序(Application)、资源管理器(Resource Manager)、事务管理器(Transaction Manager)、通信资源管理器(Communication Resource Manager)、 通信协议(Communication Protocol)。
而在JTA规范中,模型中又多了一个元素Application Server,如下所示:
6.3.1 事务管理器(transaction manager):
处于图中最为核心的位置,其他的事务参与者都是与事务管理器进行交互。事务了管理器提供事务声明,事务资源管理,同步,事务上下文传播等功能。JTA规范定义了事务管理器与其他事务参与者交互的接口,而JTS规范定义了事务管理器的实现要求,因此我们看到事务管理器底层是基于JTS的。
6.3.2 应用服务器(application server):
顾名思义,是应用程序运行的容器。JTA规范规定,事务管理器的功能应该由application server提供,如上图中的EJB Server。一些常见的其他web容器,如:jboss、weblogic、websphere等,都可以作为application server,这些web容器都实现了JTA规范。特别需要注意的是,并不是所有的web容器都实现了JTA规范,如tomcat并没有实现JTA规范,因此并不能提供事务管理器的功能。
6.3.3 应用程序(application):
简单来说,就是我们自己编写的应用,部署到了实现了JTA规范的application server中,之后我们就可以我们JTA规范中定义的UserTransaction类来声明一个分布式事务。通常情况下,application server为了简化开发者的工作量,并不一定要求开发者使用UserTransaction来声明一个事务,开发者可以在需要使用分布式事务的方法上添加一个注解,就像spring的声明式事务一样,来声明一个分布式事务。
特别需要注意的是,JTA规范规定事务管理器的功能由application server提供。但是如果我们的应用不是一个web应用,而是一个本地应用,不需要被部署到application server中,无法使用application server提供的事务管理器功能。又或者我们使用的web容器并没有事务管理器的功能,(重点)如tomcat。对于这些情况,我们可以直接使用一些第三方的事务管理器类库,如JOTM和Atomikos。将事务管理器直接整合进应用中,不再依赖于application server。
6.3.4 资源管理器(resource manager):
理论上任何可以存储数据的软件,都可以认为是资源管理器RM。最典型的RM就是关系型数据库了,如mysql,另外一种比较常见的资源管理器是消息中间件,如ActiveMQ、RabbitMQ等, 这些都是真正的资源管理器。
事实上,将资源管理器(resource manager)称为资源适配器(resource adapter)似乎更为合适。因为在java程序中,我们都是通过client来于RM进行交互的,例如:我们通过mysql-connector-java-x.x.x.jar驱动包,获取Conn、执行sql,与mysql服务端进行通信;通过ActiveMQ、RabbitMQ等的客户端,来发送消息等。
正常情况下,一个数据库驱动供应商只需要实现JDBC规范即可,一个消息中间件供应商只需要实现JMS规范即可。 而引入了分布式事务的概念后,DB、MQ等在DTP模型中的作用都是RM,二者是等价的,需要由TM统一进行协调。
为此,JTA规范定义了一个XAResource接口,其定义RM必须要提供给TM调用的一些方法。之后,不管这个RM是DB,还是MQ,TM并不关心,因为其操作的是XAResource接口。而其他规范(如JDBC、JMS)的实现者,同时也对此接口进行实现。如MysqlXAConnection,就实现了XAResource接口。
6.3.5 通信资源管理器(Communication Resource Manager):
这个是DTP模型中就已经存在的概念,对于需要跨应用的分布式事务,事务管理器彼此之间需要通信,这是就是通过CRM这个组件来完成的。JTA规范中,规定CRM需要实现JTS规范定义的接口。
6.3.6 JTA 工作流程
1、application 运行在application server中
2、application 需要访问3个资源管理器(RM)上资源:1个MQ资源和2个DB资源。
3、由于这些资源服务器是独立部署的,如果需要同时进行更新数据的话并保证一致性的话,则需要使用到分布式事务,需要有一个事务管理器来统一协调。
4、Application Server提供了事务管理器的功能,tomcat没有,所以引入了第三方到事物管理器 Atomikos,应用自己管理
5、作为资源管理器的DB和MQ的客户端驱动包(java-connect-mysql.jar),都实现了XAResource接口,以供事务管理器调用。
6.3.7 JTA规范--接口定义
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
javax.transaction.Status:事务状态,这个接口主要是定义一些表示事务状态的常量,此接口无需实现
javax.transaction.Synchronization:同步
javax.transaction.Transaction:事务
javax.transaction.TransactionManager:事务管理器
javax.transaction.UserTransaction:用于声明一个分布式事务
javax.transaction.TransactionSynchronizationRegistry:事务同步注册
javax.transaction.xa.XAResource:定义RM提供给TM操作的接口
javax.transaction.xa.Xid:事务id
TM供应商:
实现UserTransaction、TransactionManager、Transaction、TransactionSynchronizationRegistry、Synchronization、Xid接口,通过与XAResource接口交互来实现分布式事务。此外,TM厂商如果要支持跨应用的分布式事务,那么还要实现JTS规范定义的接口。
常见的TM提供者包括我们前面提到的application server,包括:jboss、ejb server、weblogic等,以及一些以第三方类库形式提供事务管理器功能的jotm、Atomikos。
RM供应商:
XAResource接口需要由资源管理器者来实现,XAResource接口中定义了一些方法,这些方法将会被TM进行调用,如:
start方法:开启事务分支
end方法:结束事务分支
prepare方法:准备提交
commit方法:提交
rollback方法:回滚
recover方法:列出所有处于PREPARED状态的事务分支
一些RM提供者,可能也会提供自己的Xid接口的实现。
此外,不同的资源管理器有一些各自的特定接口要实现:
如JDBC2.0规范定义支持分布式事务的jdbc driver需要实现:javax.sql.XAConnection、javax.sql.XADataSource接口
JMS1.0规范规定支持分布式事务的JMS厂商,需要实现javax.jms.XAConnection、javax.jms.XASession接口
注意:作为DTP模型中Application开发者的我们,并不需要去实现任何JTA规范中定义的接口,只需要使用TM提供的UserTransaction实现,来声明、提交、回滚一个分布式事务即可。
TM厂商干了什么? 包括第三方事务提供者?
需要注意的是,在分布式事务中,当我们需要提交或者回滚一个事务时,不应该再使用Connection接口提供的commit和rollback方法。而是应该使用UserTransaction接口的commit接口和rollback接口替代。
另外,在本案例中,我们并没有说明UserTransaction是如何构建的,这是由事务管理器(TM)实现者提供的,而目前我们还没有接触过任何事务管理器。
说白了TM的厂商就干一个事情
javax.transaction.TransactionManager:事务管理器
javax.transaction.UserTransaction:用于声明一个分布式事务
用自己的方式实现了上面两个JTA接口.
6.4 shareding-jdbc 强XA
6.4.1 Atomikos
引入如上所示的jar包
public class TransactionManager {
public static void main(String[] args) throws javax.transaction.SystemException {
AtomikosDataSourceBean ds1 = createAtomikosDataSourceBean("db_user");
AtomikosDataSourceBean ds2 = createAtomikosDataSourceBean("db_account");
Connection conn1 = null;
Connection conn2 = null;
PreparedStatement ps1 = null;
PreparedStatement ps2 = null;
UserTransaction userTransaction = new UserTransactionImp();
try {
// 开启事务
userTransaction.begin();
// 执行db1上的sql
conn1 = ds1.getConnection();
ps1 = conn1.prepareStatement("INSERT into user(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
ps1.setString(1, "tianshouzhi");
ps1.executeUpdate();
ResultSet generatedKeys = ps1.getGeneratedKeys();
int userId = -1;
while (generatedKeys.next()) {
userId = generatedKeys.getInt(1);// 获得自动生成的userId
}
// 模拟异常 ,直接进入catch代码块,2个都不会提交
// int i=1/0;
// 执行db2上的sql
conn2 = ds2.getConnection();
ps2 = conn2.prepareStatement("INSERT into account(user_id,money) VALUES (?,?)");
ps2.setInt(1, userId);
ps2.setDouble(2, 10000000);
ps2.executeUpdate();
// 两阶段提交
userTransaction.commit();
} catch (Exception e) {
try {
e.printStackTrace();
userTransaction.rollback();
} catch (SystemException e1) {
e1.printStackTrace();
}
} finally {
try {
ps1.close();
ps2.close();
conn1.close();
conn2.close();
ds1.close();
ds2.close();
} catch (Exception ignore) {
}
}
}
}
private static AtomikosDataSourceBean createAtomikosDataSourceBean(String dbName) {
// 连接池基本属性
Properties p = new Properties();
p.setProperty("url", "jdbc:mysql://localhost:3306/" + dbName);
p.setProperty("user", "root");
p.setProperty("password", "");
// 使用AtomikosDataSourceBean封装com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
//atomikos要求为每个AtomikosDataSourceBean名称,为了方便记忆,这里设置为和dbName相同
ds.setUniqueResourceName(dbName);
ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
ds.setXaProperties(p);
return ds;
}
}
如上代码所示,就可以实现强一致性的处理了。那么第三方的TM底层是怎么实现的呢,包括厂商的weblogic等等的其实就是类似第三方TM的实现。好了接下来我们看个类图。
这就是整个第三方事务管理器的一个底层架构。实际上最终事务的实现是Paricipant实现的,两个子类 一个是分布式的事务实现。一个是非分布式事务的实现。
可以看到如上图所示的XA的实现,莫过于在XAResourceTransaction.class类中,组合了XAResource,然后调用厂商的RM
获取厂商的RM来实现底层的一个事务管理。这就是我们说的第三方的分布式事务的实现。那么结合Spring我们如何把它用起来呢?接下来我们看代码
业务场景:
2个DB:
1. test库中有user用户表
2. test2库中有user_msg用户备注表
当插入一条user记录时,同时插入一条user_msg。如果出现异常,2个库中的数据都能回滚。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
@Configuration
@MapperScan(basePackages = {MasterRepositoryConfig.MASTER_PACKAGE}, sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterRepositoryConfig {
static final String MASTER_PACKAGE = "study.repository.master";
private static final String MAPPER_LOCATIONS = "classpath*:mybatis/mapper/master/**/*.xml";
@ConfigurationProperties(prefix = "study.datasource.master")
@Bean(name = "masterDataSource")
@Primary
public DataSource masterDataSource() {
// 连接池基本属性
Properties p = new Properties();
p.setProperty("url", "jdbc:mysql://localhost:3306/" + "test");
p.setProperty("user", "root");
p.setProperty("password", "12345");
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName("masterDataSource");
ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
ds.setXaProperties(p);
ds.setPoolSize(5);
return ds;
}
@Bean(name = "masterSqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(dataSource);
//指定基包
fb.setTypeAliasesPackage(MASTER_PACKAGE);
//指定xml文件位置
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATIONS));
return fb.getObject();
}
/**
* 基于sqlSession的操作模板类
*
* @param sqlSessionFactory
* @return
*/
@Bean(name = "masterSqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate;
}
}
@Configuration
@MapperScan(basePackages = {SlaveRepositoryConfig.MASTER_PACKAGE}, sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveRepositoryConfig {
static final String MASTER_PACKAGE = "study.repository.slave";
private static final String MAPPER_LOCATIONS = "classpath*:mybatis/mapper/slave/**/*.xml";
/**
* 从数据源:test2:user_msg表
*
* @return
*/
@ConfigurationProperties(prefix = "study.datasource.slave")
@Bean(name = "slaveDataSource")
public DataSource slaveDataSource() {
// 连接池基本属性
Properties p = new Properties();
p.setProperty("url", "jdbc:mysql://localhost:3306/" + "test2");
p.setProperty("user", "root");
p.setProperty("password", "12345");
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName("slaveDataSource");
ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
ds.setXaProperties(p);
ds.setPoolSize(5);
return ds;
}
/**
* 会话工厂
*
* @return
* @throws Exception
*/
@Bean(name = "slaveSqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(dataSource);
//指定基包
fb.setTypeAliasesPackage(MASTER_PACKAGE);
//指定xml文件位置
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATIONS));
return fb.getObject();
}
/**
* 基于sqlSession的操作模板类
*
* @param sqlSessionFactory
* @return
* @throws Exception
*/
@Bean(name = "slaveSqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate;
}
}
@Configuration
public class JtaTransactionManagerConfig {
@Bean(name = "atomikosTransactionManager")
public JtaTransactionManager regTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
}
@Slf4j
@Service
public class UserServiceImpl implements UserService{
@Resource
private UserRepository userRepository;
//@Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void addUser(int id, String name) {
log.info("[addUser] begin!!!");
User user = new User();
user.setId(id);
user.setName(name);
userRepository.insert(user);
log.info("[addUser] end!!! ");
//创造一个异常,看回滚情况
//throw new RuntimeException();
}
}
@Slf4j
@Service
public class UserMsgServiceImpl implements UserMsgService {
@Resource
private UserService userService;
@Resource
private UserMsgRepository userMsgRepository;
/**
* 新增带备注的用户:申明式分布式事务(atomikos)
*
* @param id
* @param name
* @param msg
*/
@Transactional(transactionManager = "atomikosTransactionManager", rollbackFor = Exception.class)
@Override
public void addUserMsg(int id, String name, String msg) {
log.info("[addUserMsg] begin!!!");
// 1.插入用户
userService.addUser(id, name);
UserMsg userMsg = new UserMsg();
userMsg.setUserId(id);
userMsg.setMsg(msg);
// 2.插入用户备注
userMsgRepository.insert(userMsg);
log.info("[addUserMsg] end!!! ");
//创造一个异常,看回滚情况
//throw new RuntimeException();
}
}
如上所示我们就通过Springboot+atomikos实现了分布式的事务处理。只是使用了Spring的一个简单的@Transaction注解就实现了数据库多库层面的分布式事务了。那么原理是什么呢让我来研究一下Spring的事务架构
JtaTransactionManager实现了InitializingBean接口的afterPropertiesSet()方法
就是检查JTA接口有没有对应的实现,如果我们能给到一个JTA接口的实现,那么spring就可以正常的通过注解或者申明来实现分布式的事务了。
总得给spring的接口JtaTransanctionManager 搞个实现吧,那就搞一个,然后在需要的两个对象放进去。这就是Spingboot集成.第三方分布式事务组件的过程。可以实现夸库的事务。
6.4.2 shareding+atomikos
XAShardingTransactionManager 为Apache ShardingSphere 的分布式事务的XA实现类。 它主要负责对多数据源进行管理和适配,并且将相应事务的开启、提交和回滚操作委托给具体的 XA 事务管理器。
7 柔性跨服务事务
7.1 TCC (SEATA)
如图所示:
- 一个完整的业务活动由一个主业务服务与若干从业务服务组成。
- 主业务服务负责发起并完成整个业务活动。
- 从业务服务提供TCC型业务操作。
- 业务活动管理器控制业务活动的一致性,它登记业务活动中的操作,并在业务活动提交时进行confirm操作,在业务活动取消时进行cancel操作
TCC和2PC很像,不过TCC的事务控制都是业务代码层面的,而2PC则是资源层面的。
Tom需要给Tracy转10元,当使用TCC解决这种事务时,应该如何去做呢?
TCC解决分布式事物的思路是,一个大事务拆解成多个小事务。把分布式事务,切分成本地事务。其实这里预留资源就是给这次发起的事务一个锁定状态
Try 在事务发起方生成唯一的事务流水号,锁定tom的账户,并且生成一条-10的流水号分支事务,锁定tracy的账号,,并生成一条流水号分支的+10的记录,这里锁定账号不是真的锁定,可以是-10或者加了10。但是是另,加一个字段表示冻结的金额。
Confirm阶段,如果try阶段两个节点try成功就可以Confirm把分支事务表中的记录,加进去,并解除锁定。如果其中一条失败,就执行cancel操作
Cancel阶段 执行事先写好的回滚逻辑,不管Try阶段,或者confirm阶段出异常都回滚。
7.1.1 整体流程图
从流程图上可以看到,TCC依赖于一条事务处理记录,在开始TCC事务前标记创建此记录,然后在TCC的每个环节持续更新此记录的状态,这样就可以知道事务执行到那个环节了,当一次执行失败,进行重试时同样根据此数据来确定当前阶段,并判断应该执行什么操作。
因为存在失败重试的逻辑,所以cancel、commit方法必须实现幂等。其实在分布式开发中,凡是涉及到写操作的地方都应该实现幂等。
7.2.1 TCC优缺点
目前解决分布式事务的方案中,最稳定可靠的方案有:TCC、2PC/3PC、最终一致性。这三种方案各有优劣,有自己的适用场景。下面我们简单讨论一下TCC主要的优缺点。
7.2.1.1 TCC的主要优点有
因为Try阶段检查并预留了资源,所以confirm阶段一般都可以执行成功。
资源锁定都是在业务代码中完成,不会block住DB,可以做到对db性能无影响。
TCC的实时性较高,所有的DB写操作都集中在confirm中,写操作的结果实时返回(失败时因为定时程序执行时间的关系,略有延迟)。
7.2.1.2 TCC的主要缺点有
从源码分析中可以看到,因为事务状态管理,将产生多次DB操作,这将损耗一定的性能,并使得整个TCC事务时间拉长。
事务涉及方越多,Try、Confirm、Cancel中的代码就越复杂,可复用性就越底(这一点主要是相对最终一致性方案而言的)。另外涉及方越多,这几个阶段的处理时间越长,失败的可能性也越高。
7.2 AT (SEATA)
Seata 管理的分布式事务的典型生命周期:
1.TM 要求 TC 开始一个全新的全局事务。TC 生成一个代表该全局事务的 XID。
2.XID 贯穿于微服务的整个调用链。
3.作为该 XID 对应到的 TC 下的全局事务的一部分,RM 注册本地事务。
4.TM 要求 TC 提交或回滚 XID 对应的全局事务。
5.TC 驱动 XID 对应的全局事务下的所有分支事务完成提交或回滚。
Seata AT 事务模型包含TM (事务管理器),RM (资源管理器) 和 TC (事务协调器)。 TC 是一个独立部署的服务,TM 和 RM 以 jar 包的方式同业务应用一同部署,它们同 TC 建立长连接,在整个事务生命周期内,保持远程通信。 TM 是全局事务的发起方,负责全局事务的开启,提交和回滚。 RM 是全局事务的参与者,负责分支事务的执行结果上报,并且通过 TC 的协调进行分支事务的提交和回滚。
7.3 saga模式
7.3 总结(AT、TCC、Saga、XA)模式分析
- AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
- TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
- Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统, Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供TCC 要求的接口,也可以使用 Saga 模式。
- XA模式是分布式强一致性的解决方案,但性能低而使用较少。