原创

深入理解分布式事务


深入理解分布式事务

1.事务的基本概念

事务是指逻辑上的一组操作,或单个逻辑单元执行的一系列操作, 同属于一个事务的操作会当作一个整体提交给系统,操作要么全部执行成功, 要么全部执行失败.

事务的特性

A (Atomic原子性): 构成事务的所有操作要么全部执行成功, 要么全部执行失败.

C (Consistency一致性): 事务执行前后, 数据始终处于一致的状态.

I (Isolation隔离性): 并发执行的事务之间互不干扰.

D(Durability持久性): 事务提交完成后, 事务对数据的更改操作会持久化到数据库中,且不会回滚.

事务的类型

扁平事务: 通常由begin或start transaction开始, commit或rollback结束.

带保存点的扁平事务: 通过在事务内部的某个位置设置保存点(save point),达到将当前事务回滚到此保存点的目的.

设置事务保存点: savepoint 保存点名称

回滚到保存点: rollback to 保存点名称

删除保存点: release savepoint 保存点名称

链式事务: 自动将当前事务的上下文隐式地传递给下一个事务, 一个事务地提交操作和下一个事务地开始操作具备原子性, 上一个事务的处理结果对下一个事务是可见的, 事务之间像链条一样传递下去.

注意: 链式事务在提交的时候会释放要提交的事务的所有锁和保存点, 链式事务的回滚操作只能回滚到当前所在事务的保存点, 不能回滚到已提交事务的保存点.

嵌套事务: 多个事务处于嵌套状态, 共同完成一项任务的处理.嵌套事务最外层有一个顶层事务, 顶层事务控制在所有的内部子事务, 内部子事务提交完成后, 整体事务不会提交, 只有顶层事务提交完成后, 整体事务才算提交完成.

回滚嵌套事务内部的子事务时, 会将事务回滚到外部顶层事务的开始位置.

嵌套事务的提交是从内部的子事务向外依次进行的, 直到最外层的顶层事务提交完成.

回滚嵌套事务最外层的顶层事务时, 会回滚嵌套事务内的所有事务,包括已提交的内部子事务.

分布式事务: 事务的参与者,事务所在的服务器,涉及的资源服务器以及事务管理器位于不同分布式系统的不同服务或数据库节点上.

MySQL事务基础

并发事务带来的问题

更新丢失(脏写): 对同一行数据, 一个事务对该行数据的更新操作覆盖了其他事务对该行数据的更新操作, 本质是写操作的冲突, 解决方式是让每个事务按照按照一定的顺序依次进行写操作.

脏读: 一个事务读取到另一个事务未提交的数据. 本质是读写操作的冲突, 解决方法先写后读.

不可重复读: 同一个事务使用相同的查询语句, 在不同时刻读取到的结果不一致(其他事务也都数据进行更改了).本质是读写操作的冲突, 解决方法先读后写.

幻读: 一个事务两次读取一个范围的数据记录, 读取的结果不同(其他事务插入了一条数据).本质上是读写操作的冲突, 解决方法先读后写.

不可重复读和幻读的不同:

  1. 不可重复读的重点在于更新和删除操作, 而幻读的重点在于插入操作.
  2. 使用锁机制实现事务隔离级别时, 在可重复读隔离级别中, sql语句第一次读取到数据后会将相应的数据加锁, 使其他事务无法修改和删除这些数据, 但这种方法无法对新插入的数据加锁,另一个事务还可以进行插入操作,导致其他事务多了一条之前没有的数据.
  3. 幻读无法通过行级锁来避免, 需要使用串行化的事务隔离级别, 会极大降低数据库的并发能力.
  4. 本质上, 不可重复读和幻读最大区别在于如何通过锁机制解决问题.

MySQL事务隔离级别

读未提交(Read Uncommitted)

读已提交(Read Committed)

可重复读(Repeatable Read) InnoDB默认级别

串行化(Serializable)

可在my.cnf或my.ini的mysqld节点上配置:

transaction-isolation = [READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE READ | SERIALIZABLE]

也可使用set transaction命令改变单个或所有新链接的事务隔离级别:

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL [READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE READ | SERIALIZABLE]

不带 SESSION 或 GLOBAL关键字设置隔离级别, 指的是为下一个(还未开始的)事务设置隔离级别

使用GLOBAL关键字是对全局设置事务隔离级别, 对所有新产生的数据库连接生效

使用SESSION关键字是对当前的数据库链接设置事务隔离级别, 只对当前链接的后续事务生效

任何客户端能自由改变当前会话的事务隔离级别, 可在事务中间改变, 也可改变下一个事务的隔离级别

查询事务隔离级别

SELECT @@global.tx_isolation

SELECT @@session.tx_isolation

SELECT @@tx_isolation

不同事务隔离级别对并发事务的问题的解决程度对比:

事务隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能可能
串行化不可能不可能不可能

MySQL中锁的分类

1.悲观锁和乐观锁

悲观锁: 整个数据处理的过程中会将相应的数据锁定, 读取数据和修改数据都需要加锁.

乐观锁: 通过数据版本机制实现的, 为数据增加一个版本标识, 更新数据时, 会将版本号的字段加1, 提交数据时, 如果提交的数据版本号大于数据表中当前要修改的数据的版本号,则对数据进行修改,否则不进行修改.

2.读锁和写锁

读锁: 又称为共享锁或S锁(Shared Lock), 针对同一份数据可以加多个读锁而互不影响.但此时不能为数据增加写锁.

写锁: 又称为排他锁或X锁(Exclusive Lock), 如果当前写锁未释放, 会阻塞其他的写锁或读锁.一旦加了写锁, 则不能再增加写锁和读锁.

3.表锁,行锁和页面锁

表锁: 又称表级锁, 在整个数据表上对数据进行加锁和释放锁, 开销比较小, 加锁速度快, 一般不会出现死锁, 锁定的粒度比较大, 发生锁冲突的概率最高, 并发度最低.

两张表级锁模式: 表共享锁(Table Shared Lock), 表独占写锁(Table Write Lock)

增加表锁命令: lock table 表名1 [ read | write] , 表名2 [ read | write]

查看数据表上增加的锁命令: show open tables

删除表锁命令: unlock tables

行锁: 又称行级锁, 在数据行上对数据进行加锁和释放锁, 开销比较大, 加锁速度慢, 可能会出现死锁, 锁定的粒度最小, 发生锁冲突的概率最小, 并发度最高. 两种类型的行锁: 共享锁和排他锁.

行锁主要加在索引上, 如果对非索引的字段设置条件进行更新, 行锁可能会变成表锁.

行锁针对索引加锁, 不是针对记录加锁, 并且加锁的索引不能失效, 否则行锁可能会变成表锁.

lock in share mode 来指定共享锁, for update 来指定排他锁:

select * from account where id = 1 [ lock in share mode | for update ]

页面锁: 也称页级锁, 在页面级别对数据进行加锁和释放锁, 开销介于表锁和行锁之间, 可能会出现死锁, 锁定的粒度大小介于表锁和行锁之间, 并发度一般.

4.记录锁, 间隙锁和临键锁

有数据表 account 如下:

id(主键)namebalance(非唯一索引key)
1张三300
2李四350
3王五500
15赵六100
20田七360

记录锁: 为某行记录进行加锁和释放锁.

select * from account where id = 1 for update;

update account set balance = balance + 100 where id = 1;

上诉语句把 id为1的记录行锁定, id必须为唯一索引列或主键列.

间隙锁: MySQL使用范围查询时, 如果请求共享锁或排他锁, InnoDB会给符合条件的已有数据的索引项加锁, 如果键值在条件范围内, 而这个范围内并不存在记录, 则认为此时出现了间隙(GAP), InnoDB会对这个间隙加锁, 而这种加锁机制就是间隙锁(GAP Lock).

间隙锁是对两个值之间的间隙加锁, 间隙锁只有在可重复读事务隔离级别才会生效, 在某种程度下可以解决幻读的问题.

当我们使用索引无论是等值还是范围查询, 没有命中一条记录时候, 加的就是间隙锁, 左开右开的区间.

select * from account where id = 6;

select * from account were id > 4 and id < 7;

没有命中任何一条记录, 会锁住 (4, 7) 区间, 另一个事务插入id = 6则会阻塞.

临键锁: Next-Key Lock, 当我们使用索引进行范围查询, 命中了记录的情况下, 就是使用了临键锁, 相当于记录锁+间隙锁.

两种退化的情况:

  1. 唯一性索引, 等值查询匹配到一条记录的时候, 退化成记录锁.
  2. 没有匹配到任何记录的时候, 退化成间隙锁.

左开右闭区间, 目的是为了解决幻读的问题.

select * from account where id > 2 and id < 9; 上面的sql命中了id = 3的记录,也包含了记录不存在的区间, 所以他锁住(3, 15] 区间, 在这区间, 别的事务插入不了数据, 所以部分解决了幻读问题.

死锁的产生和预防

发生死锁的必要条件
  1. 互斥条件: 一段时间内, 计算机中的某个资源只能被一个进程占用, 如果其他进程请求该资源, 则只能等待.
  2. 不可剥夺条件: 某个进程获得的资源在使用完毕之前, 不能被其他进程强行夺走, 只能由获得资源的进程主动释放.
  3. 请求与保持条件: 进程已经获得至少一个资源, 又要请求其他资源, 但请求的资源被其他进程占用, 此时请求的进程就会被阻塞, 并且不会释放自己已获得的资源.
  4. 循环等待条件: 系统中的进程之间相互等待, 同时各自占用的资源又会被下一个进程所请求.

只有4个必要条件都满足时, 才会发生死锁.

处理死锁的方法
  1. 预防死锁: 破坏造成以上4个必要条件中的一个或多个, 防止死锁的发生.
  2. 避免死锁: 在系统资源的分配过程中, 使用某种策略或方法防止系统进入不安全状态.
  3. 检测死锁: 允许发生死锁, 能够检测死锁的发生, 并采取适当的措施清除死锁.
  4. 解除死锁: 当检测出死锁后, 采用适当的策略或方法将进程从死锁状态解脱出来.
避免死锁的方式

查看死锁的日志信息: show engine innodb status\G

  1. 尽量使用索引进行数据检索, 避免无效索引导致行锁升级为表锁.
  2. 合理设计索引, 尽量缩小锁的范围.
  3. 尽量减少查询条件的范围, 尽量避免间隙锁或缩小间隙锁的范围.
  4. 尽量控制事务的大小, 减少一次事务锁定的资源数量, 缩短锁定资源的时间.
  5. 如果sql语句涉及到加锁操作, 尽量放在整个事务的最后执行.
  6. 尽可能使用低级别的事务隔离机制.

InnoDB的MVCC原理

在MVCC机制中, 每个连接到数据的读操作, 在某个瞬间看到的都是数据库中数据的一个快照, 而写操作的事务提交之前, 读操作是看不到这些数据的变化的.

本质上, MVCC机制保存了数据库中数据在某个时间点上的数据快照, 同一个读操作的事务, 按照相同的条件查询, 无论查询多少次结果都一样.不同的事务在同一时刻看到的同一张表的数据可能不同.

在InnoDB引擎中, MVCC机制通过每行数据表记录后的两个隐藏的列来实现的, 一个列用来保存行的创建版本号, 另一个列用来保存行的过期版本号.每当有一个新的事务执行时, 版本号会自动递增, 事务开始时刻的版本号作为事务的版本号, 用于和查询到的每行记录的版本号作对比.

查询操作

InnoDB引擎会根据下面两个条件检查每行记录:

  1. 查询版本号小于等于当前事务版本号的数据行, 要么在事务开始前已存在, 要么事务本身插入或更新的数据行.
  2. 数据行删除的版本要么没有被定义, 要么大于当前事务的版本号, 可确保事务读取到的行, 在事务开始前没有被删除.

只有符合上面两个条件的数据行, 才会被返回作为查询的结果.

插入操作

InnoDB引擎会将新插入的每一行记录的当前系统版本号作为行版本号.删除版本号未定义.

更新操作

InnoDB引擎会插入一条新记录, 并保存当前系统版本号作为新记录的版本号, 同时保存当前系统版本号作为原记录的行删除标识.原记录会被复制到Undo Log中.

删除操作

InnoDB引擎会保存删除的每一行记录当前的系统版本号作为行删除标识.

2.MySQL事务的实现原理

事务的隔离性是由锁和MVCC机制实现的, 原子性和持久性是由Redo Log实现的, 一致性是由Undo Log实现的.

Redo Log

Redo Log也被称作重做日志, InnoDB引擎产生的, 用于保证事务的原子性和持久性.主要记录的是物理日志, 对磁盘上的数据进行的修改操作,用来恢复提交后的物理数据页, 不过只能恢复到最后一次提交的位置.

Redo Log通常包含两个部分: 一部分是内存中的日志缓存, 称作 Redo Log Buffer.另一部分是放在磁盘上的重做日志文件, 称作 Redo Log File.

Redo Log在MySQL发生故障时, 尽力避免内存中的脏页数据写入数据表的IBD文件, 在重启MySQL服务时, 可以根据Redo Log恢复事务已经提交,但是还未写入IBD文件的数据.

Redo Log刷盘规则

InnoDB引擎通过提交事务时强制执行写日志操作机制实现事务的持久化,默认每次将Redo Log Buffer中的日志写入日志文件时,都调用一次操作系统的fsync()操作. 写入磁盘的Redo Log文件时, 需要经过内核空间的OS Buffer, 这是因为在打开日志文件时, 没有使用O_DIRECT标志, 而O_DIRECT标志可以不经过内核空间的OS Buffer直接向磁盘写数据.

刷盘规则:

  1. 开启事务, 发生提交事务指令后是否刷新日志由变量 innodb_flush_log_at_trx_commit决定.
  2. 每秒刷新一次, 刷新日志的频率由变量 innodb_flush_log_at_timeout的值决定, 默认是1s.刷新日志的频率和是否执行了事务的commit操作无关.
  3. 当Log Buffer中已经使用的内存超过一半时, 也会触发刷盘操作.
  4. 当事务中存在检查点(checkpoint)时, 一定程度上代表了刷写到磁盘时所处的LSN (Log Sequence Number 日志的逻辑序列号) 的位置.

innodb_flush_log_at_trx_commit的取值有 0, 1, 2, 默认为1.

  1. 为0时, 每次提交事务, 不会将Log Buffer中的日志写入OS Buffer, 而是通过一个单独的线程, 每秒写入OS Buffer并调用fsync()函数写入磁盘的Redo Log文件.如果系统崩溃, 会丢失1s的数据.
  2. 为1时, 每次提交事务都会将Log Buffer中的日志写入OS Buffer, 并调用fsync()函数将日志数据写入磁盘的Redo Log文件.性能较差.
  3. 为2时, 每次提交事务都只是将数据写入OS Buffer, 之后每隔1s, 通过fsync()函数将OS Buffer中的日志数据同步写入磁盘的Redo Log文件中.

Redo Log写入机制

Redo Log文件内容是以顺序循环的方式写入的, 一个文件写满时会写入另一个文件, 最后一个文件写满时, 会向第一个文件写数据, 并且是覆盖写.

两个指针Write Pos和Checkpoint: Write Pos随着不断向数据表中写数据, 会向后移动, 移动到最后一个文件的最后一个位置时, 会回到第一个文件的开始位置. Checkpiont开始在第一个文件的开始位置, 当Write Pos写满所有文件时(Write Pos回到第一个文件的开始位置), 这时Checkpoint和Write Pos相遇, 表明数据已经写满, 此时擦除一些记录, Checkpoint移动到擦除的记录的最后位置, Write Pos则可以继续向数据表中写数据.

Redo Log的LSN机制

LSN: 日志的逻辑序列号, 占据8字节的空间, 并且值是单调递增. 从LSN中可获取 Redo Log写入数据的总量, 检查点位置, 数据页版本相关的信息.

LSN除了存在于Redo Log中, 还存在于数据页中, fil_page_ls参数记录当前页最终的LSN值.如果数据页的LSN值小于Redo Log中的LSN值, 则表示丢失了一部分数据, 可通过Redo Log的记录来恢复数据.

show engine innodb status \G 可查看LSN值:

  1. Log sequence number: 当前内存缓冲区中的Redo Log的LSN.
  2. Log flushed up to: 刷新到磁盘上的Redo Log文字中的LSN.
  3. Pages flushed up to: 已经刷新到磁盘数据页的LSN.
  4. Last checkpoint at: 上一次检查点所在位置的LSN.

show variables like '%innodb_log%'查看Redo Log 的相关参数.

  1. innodb_log_buffer_size: 表示log_buffer的大小. 默认8MB.
  2. innodb_log_file_size: 表示事务日志的大小,默认5MB.
  3. innodb_log_files_in_group: 事务日志组中的事务日志文件个数, 默认为2.
  4. innodb_log_group_home_dir: 事务日志组所在的目录, 默认 ./(当前MySQL数据所在的目录).

Undo Log

基本概念

Undo Log主要起到的作用: 回滚事务和多版本并发事务(MVCC).

MySQL启动事务前, 会先将修改的数据记录存储到Undo Log中, 如果事务回滚或MySQL崩溃, 可以利用Undo Log对未提交的事务进行回滚操作, 保证数据的一致性.

当事务提交时, 并不会立刻删除对应的Undo Log, 会放入待删除的列表中, 通过一个后台线程purge thread进行删除处理.

Undo Log记录的是逻辑日志, 会记录一条相反的操作语句, 用于回滚. 当select语句查询的数据被其他事务锁定时, 可以从Undo Log中分析出当前数据之前的版本,从而向客户端返回之前版本的数据.(MVCC)

MySQL事务执行过程中产生的Undo Log也需要持久化操作, Undo Log会产生Redo Log. Undo Log的完整性和可靠性需要Redo Log来保证, 数据崩溃时先做Redo Log恢复, 然后做Undo Log回滚.

存储方式

InnoDB对Undo Log的存储采用段的方式进行管理, 存在rollback segment的回滚段, 回滚段内部有1024个undo log segment段.

Undo Log默认存放在共享数据表空间中, 默认为ibdata1文件, 若开启了 innodb_file_per_table 参数,则Undo Log存放在每张数据表的.ibd文件中.

默认情况下InnoDB会将回滚段全部写入同一个文件中, 也可通过innodb_undo_tablespaces变量将回滚段平均分配到多个文件中, innodb_undo_tablespaces默认值为0,表示将rollback segment回滚段全部写入同一个文件中.innodb_undo_tablespaces变量只能在停止MySQL服务的情况下修改.

实现MVCC机制

事务提交之前, 向Undo Log保存事务当前的数据.Undo Log的回滚段中, undo logs分为 insert undo log和update undo log.

insert undo log: 事务对新插入记录产生的Undo Log, 只在事务回滚时需要, 事务提交后可立即丢弃.

update undo log: 事务对数据进行删除和更新操作时产生的Undo Log, 事务回滚时需要, 一致性读也需要, 不能随便删除, 只有当数据库所使用的快照不涉及该日志记录时, 对应的回滚日志才会被purge线程删除.

为实现MVCC机制, InnoDB引擎在数据库每行数据的后面添加了3个字段: 6字节的事务id(DB_TRX_ID), 7字节的回滚指针(DB_ROLL_PTR), 6字节的DB_ROW_ID.

  1. DB_TRX_ID: 用于标识最近一次对本行记录做修改的事务的标识符, 最后一次修改本行记录的事务id. delete操作在InnoDB引擎内部属于一次update操作, 更新行的一个特殊位, 将行标识为已删除, 并非真正删除.
  2. DB_ROLL_PTR: 指向上一个版本的行记录
  3. DB_ROW_ID: 随着新数据行的插入操作而单调递增的行id.

相关参数

show variables like '%undo%' 查看Undo Log的相关信息:

  1. innodb_max_undo_log_size: Undo Log空间的最大值, 默认1GB, 超过会触发truncate回收操作, 回收操作后, Undo Log空间缩小到10MB.
  2. innodb_undo_directory: Undo Log的存储目录, 默认 ./
  3. innodb_undo_log_encrypt: MySQL 8 新增的参数, 表示Undo Log是否加密, 默认OFF(不加密). ON(加密).
  4. innodb_undo_log_truncate: 是否开启在线回收Undo Log文件操作, 默认OFF(关闭), ON(开启)
  5. innodb_undo_tablespaces: 必须大于等于2, 回收一个Undo Log时保证另一个Undo Log可用.
  6. innodb_undo_logs: Undo Log的回滚段数量, 至少大于等于35, 默认为128.
  7. innodb_purge_rseg_truncate_frequency: 控制回收Undo Log的频率.

BinLog

基本概念

BinLog是记录所有MySQL数据库表结构变更已经表数据变更的二进制日志, 不会记录select和show这类查询操作的日志, BinLog以事件的形式记录,并包含执行语句所消耗的时间.

使用场景:

  1. 主从复制: 在主数据库上开启BinLog, 主数据库把BinLog发送至从数据库,从数据库获取BinLog后通过I/O线程将日志写到中继日志(Relay Log), 通过SQL线程及那个Relay Log的数据同步至从数据库,从而达到主从数据库的一致性.
  2. 数据恢复: 可使用mysqlbinlog等工具进行数据恢复.

记录模式

BinLog主要有三种记录模式: Row, Statement, Mixed.

Row: BinLog会记录每一行数据被修改的情况,非常清楚记录每一行数据的修改情况, 完全实现主从数据库的同步和数据的恢复, 缺点是如果主数据库发送批量操作, 会产生大量的二进制日志, 影响主从数据库的同步性能.

Statement: BinLog会记录每一天修改数据的SQL语句, 不记录数据的修改细节, 只记录数据表结构和数据变更的SQL语句, 产生的二进制日志数据量比较小, 减少磁盘I/O操作, 提示数据存储和恢复的效率. 缺点是可能会导致主从数据库的数据不一致, 比如使用了now()或last_insert_id()函数.

Mixed: 是Row模式和Statement模式的混用, MySQL会根据执行的SQL语句选中写入的记录模式.

用来表示修改操作的数据结构叫做日志事件(Log Event), 不同的修改操作对应不同的日志事件, 比较常用的日志事件包括: Query Event, Row Event, Xid Event, BinLog文件的内容是各种日志事件的集合.

写入机制

MySQL事务提交时, 会记录事务日志(Redo Log)和二进制日志(BinLog).Redo Log是InnoDB引擎特有的日志, BinLog是MySQL本身就有的上层日志, 先于事务日志Redo Log写入.

  1. 根据记录的模式和操作触发事件生成日志事件.
  2. 将事务执行过程中产生的日志事件写入相应的缓冲区(每个事务线程都有一个缓冲区).日志事件保存在数据结构binlog_cache_mngr中, 该数据结构中有两个缓冲区: stmt_cache(存放不支持事务的信息), trx_cache(存放支持事务的信息).
  3. 事务在提交阶段会将产生的日志事件写入磁盘的BinLog文件中, 因为不同的事务会以串行的方式将日志事件写入BinLog文件中,所以一个事务在BinLog文件中是连续完整的,中间不会插入其他事务的日志事件.

组提交机制

MySQL提供了组提交(group commit)的功能, 调用一次fsync()函数能够将多个事务的日志刷新到磁盘的日志文件中, 大大提升了日志刷盘的效率.

提交事务会在InnoDB存储引擎的上层将事务按照一定的顺序放入一个队列, 队列中的第一个事务为leader, 其他事务为follower, 先写BinLog, 再写事务日志. BinLog和事务日志都通过组提交功能进行写入.BinLog是通过二进制日志组提交(Binary Log Group Commit), 分为Flush, Sync和Commit阶段:

  1. Flush: 将每个事务的BinLog写入对应的内存缓冲区.
  2. Sync: 将内存缓冲区的BinLog写入磁盘的BinLog文件中, 如果队列中存在多个事务, 此时只执行一次可将多个事务的BinLog刷新到磁盘的BinLog文件中,成为BLGC操作.
  3. Commit: Leader事务根据事务的顺序调用存储引擎层的事务提交操作.

Flush阶段不是写完立刻进入Sync阶段, 而是等待一定时间,积累几个事务的BinLog再以前进入Sync阶段, 这个等待事件由binlog_max_flush_queue_time决定, 默认为0.

一组事务正在执行Commit阶段的操作的同时, 其他新产生的事务可以执行Flush阶段的操作, Commit阶段的事务和Flush阶段的事务不会相互阻塞, 组提交功能持续生效.

和Redo Log的区别

  1. BinLog是MySQL本身就有的, 只有InnoDB引擎才会输出Redo Log.
  2. BinLog是逻辑日志, 而Redo Log是物理日志.
  3. Redo Log具有幂等性, 而BinLog不具有.插入一条数据并删除, Redo Log前后的状态未发送变化, 而BinLog会记录插入和删除操作.
  4. BinLog开启事务时, 会将每次提交的事务一次性写入内存缓冲区, 如果未开启事务, 则每次成功执行插入,更新和删除语句时,就会将事务信息写入内存缓冲区.Redo Log在数据准备修改之前将数据写入缓冲区的Redo Log总, 然后在缓冲区修改数据, 提交事务时, 先将Redo Log写入缓冲区, 写入完成后再提交事务.
  5. BinLog只会在事务提交时, 一次性写入BinLog, 日志记录的方式与事务的提交顺序有关, 且一个事务的BinLog中间不会插入其他事务的BinLog.而Redo Log记录的是物理页的修改, 最后一个提交的事务记录会覆盖之前所有未提交的事务记录, 并且一个事务的Redo Log中间会插入其他事务的Redo Log.
  6. BinLog是追加写入, 写完一个再写下一个不会覆盖使用, 而Redo Log是循环写入, 日志空间的大小是固定的, 就覆盖使用.
  7. BinLog一般用于主从复制和数据恢复, 不具备崩溃自动恢复的能力, 而Redo Log是数据库故障后重启, 用于恢复事务已提交但为写入数据表的数据.

相关参数

show variables like '%log_bin%';

show variables like '%binlog%';

log_bin: 是否开启二进制日志

log_bin_index: 指定BinLog文件的路径和名称

binlog_do_db: 只记录指定数据库的BinLog文件

binlog_ignore_db: 不记录指定数据库的BinLog文件

max_binlog_size: BinLog的最大值,默认1GB

sync_binlog: 取值0事务提交后, MySQL将binlog_cache中的数据写入BinLog文件的同时, 不会执行fsync()函数刷盘. 取值大于0的数字N时, 在进行N次事务提交操作后, MySQL将执行一次fsync()函数, 将多个事务的BinLog刷新到磁盘中.

max_binlog_cache_size: BinLog占用的最大内存

binlog_cache_size: BinLog使用的内存大小

binlog_cache_use: BinLog缓存的事务数量

binlog_cache_disk_use: BinLog超过binlog_cache_size的值,且使用临时文件保存SQL语句的事务数量

MySQL默认不会开启BinLog, 如果需要开启, 修改my.cnf或my.ini配置文件, 在mysqld下增加:log_bin=mysql_bin_log, 并重启MySQL服务.

MySQL事务流程

事务执行流程

MySQL在事务的执行过程中, 会记录相应的SQL语句的Undo Log和Redo Log, 然后在内存中更新数据并形成数据脏页, 接下来Redo Log会根据一定的规则触发刷盘操作, Undo Log和数据脏页通过检查点机制刷盘, 提交事务时, 会将当前事务相关的Redo Log刷盘, 只有当前事务相关的所有Redo Log刷盘成功, 事务才算提交成功.

事务恢复流程

如果事务在提交之前, MySQL崩溃或宕机, 此时会先使用Redo Log恢复数据, 然后使用Undo Log回滚数据. 如果在事务提交之后, Undo Log和数据脏页还未刷盘之前, MySQL崩溃或宕机, 此时使用Redo Log恢复数据.

MySQL中的XA事务

XA事务基本原理

MySQL中只有InnoDB引擎支持XA分布式事务.

XA事务本质上是一种基于两阶段提交的分布式事务(多个数据库事务共同完成一个原子性的事务操作).使用XA事务时, InnoDB引擎的事务隔离级别需要设置为串行化.

XA事务由一个事务管理器(Transaction Manager), 一个或多个资源管理器(Resource Manager)和一个应用程序(Application Program)组成.

  1. 事务管理器: 主要对参与全局事务的各个分支事务进行协调, 并与资源管理器进行通信.
  2. 资源管理器: 主要提供对事务资源的访问能力,(单个数据库)
  3. 应用程序: 明确全局事务和各个分支事务, 指定全局事务中的操作.

XA事务分为Prepare阶段和Commit阶段:

  1. Prepare: 事务管理器向资源管理器发送准备指令, 资源管理器接收到指令后执行数据的修改操作并记录相关的日志信息, 然后向事务管理器返回可以提交或不可提交的结果信息.
  2. Commit: 事务管理器接收所有资源管理器返回的结果信息, 如果一个或多个资源管理器向事务管理器返回的结果信息为不可提交, 或超时, 则事务管理器向所有的资源管理器发送回滚指令.如果事务管理器收到的所有资源管理器返回的结果信息为可提交, 则事务管理器向所有的资源管理器发送提交事务的指令.

MySQL XA事务语法

show engines 查看存储引擎是否支持XA事务

XA [START | BEGIN] xid [JOIN | RESUME] 开启XA事务, XA START不支持[JOIN | RESUME], xid是一个唯一值, 表示事务分支标识符.

XA END xid [SUSPEND [FOR MIGRATE]] 结束XA事务, XA START不支持[SUSPEND [FOR MIGRATE]]

XA PREPARE xid准备提交XA事务

XA COMMIT xid [ONE PHASE] 提交XA事务, 如果使用了ONE PHASE则表示使用一阶段提交, 在两阶段提交协议中, 如果只有一个资源管理器参与操作, 则可优化为一阶段提交.

XA ROLLBACK xid 回滚XA事务

XA RECOVER [CONVERT XID] 列出所有处于准备阶段的XA事务

XA使用XID标识分布式事务, 主要由: xid: gtrid[, bqual [, formatID]]

  1. gtrid: 必须, 字符串, 表示全局事务标识符
  2. bqual: 可选, 字符串, 默认空串, 表示分支限定符
  3. formatID: 可选, 默认值为1, 用于标识 gtrid 和 bqual 值使用的格式

3.Spring事务的实现原理

Spring事务原理

JDBC直接操作事务

从本质上讲Spring事务是对数据库事务的进一步封装, 如果数据库不支持事务, Spring也无法实现事务操作.

JDBC通过事务方式操作数据库的步骤:

  1. 加载JDBC驱动: Class.forName("com.mysql.jdbc.Driver")
  2. 建立与数据库的连接: Connection conn = DriverManager.getConnect(url, "name", "password")
  3. 开启事务: conn.setAutoCommit(true/false)
  4. 执行数据库的CRUD操作: PreparedStatement ps = conn.prepareStatement(sql); ps.executeUpdate(); ps.executeQuery();
  5. 提交或回滚事务: conn.commit(); conn.rollback();
  6. 关闭连接: ps.close(); conn.close();

使用Spring管理事务

开启事务, 提交事务, 回滚事务的操作全部交由Spring框架自动完成.在配置文件或者项目的启动类中配置Spring事务相关的注解驱动, 在相关的类或者方法上标识@Transactional注解, 则可开启并使用Spring的事务管理功能.

Spring框架在启动的时候会创建相关的bean实例对象, 并且会扫描标注有相关注解的类和方法, 为这些方法生成代理对象, 如果扫描到标注有@Transactional注解的类或者方法, 会根据@Transactional注解的相关参数进行配置注入, 在代理对象中会处理相应的事务, 对事务进行管理.这些操作都是Spring框架通过AOP代理自动完成的.

Spring事务分类

管理的事务可分为逻辑事务和物理事务两大类:

  1. 逻辑事务: 通过Spring等框架管理的事务,建立在物理事务之上, 比物理事务更加抽象.
  2. 物理事务: 针对特定数据库的事务.

Spring支持两种事务声明方式:

  1. 编程式事务: 如果系统需要明确的事务, 并且需要细粒度的控制事务的边界, 建议使用编程式事务.
  2. 声明式事务: 如果系统对于事务的控制粒度较为粗糙, 建议使用声明式事务.

Spring事务机制

事务超时: 对性能要求较高的应用, 要求事务执行的时间尽可能短, 可以给事务设置超时时间, 一般以秒为单位, 如果超时时间过长, 影响系统的并发性与整体性能.因为检测事务超时的任务是在事务开始时启动的, 所以事务超时机制对于程序在执行过程中会创建新事务的传播行为才有意义, 比如Spring中事务传播类型: REQUIRED, REQUIRES_NEW, NESTED三种.

事务回滚: 使用Spring管理事务, 可以指定在方法抛出异常时, 哪些异常能够回滚事务,哪些异常不回滚.默认情况下, 方法抛出RuntimeException时回滚事务, 也可手动指定回滚事务的异常类型: @Transactional(rollbackFor = Exception.class)

这里的rollbackFor属性可以指定Throwable异常类及其子类.

Spring事务的三大接口

PlatformTransactionManager, TransactionDefinition 和TransactionStatus 三大接口.

PlatformTransactionManager接口

Spring并不直接管理事务,而提供了多种事务管理器,通过这些事务管理器,将事务管理的职责委托给了持久化框架的事务来实现(如Mybatis, JTA). 接口源码:

public interface PlatformTransactionManager extends TransactionManager {
    // 获取事务状态
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    // 提交事务
    void commit(TransactionStatus var1) throws TransactionException;
    // 回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}

TransactionDefinition接口

主要定义了与事务相关的方法, 表示事务属性的常量等信息.

public interface TransactionDefinition {
    // 支持当前事务,没有事务则创建事务
    int PROPAGATION_REQUIRED = 0;
    // 如果当前存在事务, 则加入该事务, 没有当前事务, 则以非事务方式继续运行
    int PROPAGATION_SUPPORTS = 1;
    // 如果当前存在事务, 则加入事务, 没有事务, 则抛出异常
    int PROPAGATION_MANDATORY = 2;
    // 创建一个新事务, 如果当前存在事务, 则把当前事务挂起
    int PROPAGATION_REQUIRES_NEW = 3;
    // 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起
    int PROPAGATION_NOT_SUPPORTED = 4;
    // 以非事务方式运行, 如果当前存在事务, 则抛出异常
    int PROPAGATION_NEVER = 5;
    // 如果当作正有一个事务在运行,则方法运行在一个嵌套事务中,被嵌套的事务可独立于当前的事务提交或回滚(需要事务的保存点),如果当前的事务不存在,后续事务行为同PROPAGATION_REQUIRED(创建一个新事务)
    int PROPAGATION_NESTED = 6;
    // 使用数据库默认的隔离级别
    int ISOLATION_DEFAULT = -1;
    // 事务隔离级别: 读未提交
    int ISOLATION_READ_UNCOMMITTED = 1;
    // 事务隔离级别: 读已提交, 阻止脏读, 可能产生幻读或不可重复读
    int ISOLATION_READ_COMMITTED = 2;
    // 事务隔离级别: 可重复读, 阻止脏读和不可重复读, 可能产生幻读
    int ISOLATION_REPEATABLE_READ = 4;
    // 事务隔离级别: 串行化, 阻止脏读,不可重复读和幻读
    int ISOLATION_SERIALIZABLE = 8;
    // 使用默认的超时时间
    int TIMEOUT_DEFAULT = -1;
    // 获取事务的传播行为
    default int getPropagationBehavior() {
        return 0;
    }
    // 获取事务的隔离级别
    default int getIsolationLevel() {
        return -1;
    }
    // 获取事务的超时时间
    default int getTimeout() {
        return -1;
    }
    // 返回当前是否为只读事务
    default boolean isReadOnly() {
        return false;
    }
    // 获取事务的名称
    @Nullable
    default String getName() {
        return null;
    }
    // 默认事务信息
    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

TransactionStatus接口

public interface TransactionExecution {
    // 是否是新事务
    boolean isNewTransaction();
    // 设置为只回滚
    void setRollbackOnly();
    // 是否为只回滚
    boolean isRollbackOnly();
    // 判断当前事务是否已完成
    boolean isCompleted();
}
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    // 是否有保存点
    boolean hasSavepoint();
    // 将事务涉及到的数据刷新到磁盘
    void flush();
}

Spring事务隔离级别

  1. ISOLATION_DEFAULT: Spring不做事务隔离级别的处理, 会直接使用数据库默认的事务隔离级别.
  2. ISOLATION_READ_UNCOMMITTED: 相当于MySQL中未提交读隔离级别
  3. ISOLATION_READ_COMMITTED: 相当于MySQL中已提交读隔离级别
  4. ISOLATION_REPEATABLE_READ: 相当于MySQL中可重复读隔离级别
  5. ISOLATION_SERIALIZABLE: 相当于MySQL中串行化隔离级别

Spring事务传播机制

类别事务传播机制
支持当前事务REQUIRED
SUPPORTS
MANDATORY
不支持当前事务REQUIRES_NEW
NOT_SUPPORTED
NEVER
嵌套事务NESTED

事务传播机制枚举Propagation:

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

事务传播类型说明

  1. REQUIRED: 如果当前没有事务,就创建一个事务,如果已经存在一个事务,则加入, 是Spring中的默认事务传播类型.
  2. REQUIRES_NEW: 如果当前存在事务,则把当前事务挂起,并重新创建事务执行,直到新的事务提交或者回滚,才会恢复原来的事务.这种事务传播类型具有隔离性, 原有事务和新创建的事务的提交回滚互不影响.新创建的事务和被挂起的事务没有任何关系.外部事务执行失败回滚, 不会回滚内部事务的执行结果.内部事务执行失败抛出异常, 被外部事务捕获到,外部事务可以不处理内部事务的回滚操作.
  3. SUPPORTS: 外部事务不存在时, 不会开启新的事务, 外部存在事务, 则加入外部事务.
  4. MANDATORY: 当前事务必须存在, 不存在则抛出异常.
  5. NOT_SUPPORTED: 如果当前操作存在事务,则把事务挂起,以非事务的方式运行.
  6. NEVER: 以非事务的方式执行,如果当前存在事务,则抛出异常.
  7. NESTED: 如果当前存在事务,则这个方法运行在一个嵌套事务中, 被嵌套的事务可以独立于被封装的事务进行提交或者回滚, 如果没有当前事务, 则按照REQUIRED事务传播类型执行.如果封装事务存在,并且外层事务抛出异常回滚, 那么内层事务必须回滚.如果内层事务异常进行回滚,则不会影响外部事务的提交和回滚.

事务传播类型使用场景

Spring事务传播类型使用场景
REQUIREDSpring中默认的,适用于大部分场景
NOT_SUPPORTED适用于发送提示信息,站内信,短信,邮件等.这类场景要求不影响系统的主体业务逻辑,即使操作失败也不应该对主体逻辑产生影响,不能使主体逻辑的事务回滚.
REQUIRES_NEW适用于不受外层方法事务影响的场景,比如记录日志,不管主体业务逻辑是否已经完成,日志都要记录下来,不能因为主体业务逻辑异常事务回滚导致日志操作回滚

Spring事务最佳实践场景

  1. 外部方法无事务注解,内部方法添加REQUIRED事务传播类型时,内部方法抛出异常.内部方法执行失败,不会影响外部方法的执行,外部方法执行成功.
  2. 外部方法添加REQUIRED事务传播类型,内部方法无事务注解时,内部方法抛出异常,会影响外部方法的执行,导致外部方法的事务回滚.
  3. 外部方法添加REQUIRED事务传播类型, 内部方法添加REQUIRED事务传播类型时,内部方法抛出异常,会影响外部方法的执行,导致外部方法的事务回滚.
  4. 外部方法添加REQUIRED事务传播类型, 内部方法添加NOT_SUPPORTED事务传播类型时,内部方法抛出异常,如果外部方法执行成功,事务会提交,如果外部方法执行失败,事务会回滚.
  5. 外部方法添加REQUIRED事务传播类型, 内部方法添加REQUIRES_NEW事务传播类型时,内部方法抛出异常时,内部方法和外部方法都会执行失败,事务回滚.
  6. 外部方法添加REQUIRED事务传播类型, 内部方法添加REQUIRES_NEW事务传播类型, 且把异常代码移到到外部方法的末尾, 内部方法抛出异常,事务回滚.内部方法执行成功,内部事务提交.外部事务如果异常则外部事务回滚.
  7. 外部方法添加REQUIRED事务传播类型, 内部方法添加REQUIRES_NEW事务传播类型, 且把异常代码移到到外部方法的末尾, 同时外部方法和内部方法在同一个类中, 内部方法抛出异常, 外部方法和内部方法都会执行失败, 事务回滚.

Spring事务失效的场景

  1. 数据库不支持事务
  2. 事务方法未被Spring管理: 事务方法所在的类没有加载到IOC容器中.
  3. 方法没有被public修饰
  4. 同一类中的方法调用: 如果一个类中的方法A和B,B方法有事务注解, 方法A没有,方法A调用方法B,则方法B的事务会失效
  5. 未配置事务管理器
  6. 方法的事务传播类型不支持事务
  7. 不正确的捕获异常
  8. 标注错误的异常类型: rollbackFor默认RuntimeException时回滚.

4.分布式事务的基本概念

分布式系统架构

15项架构原则: N+1设计, 回滚设计, 禁用设计, 监控设计, 设计多活数据中心, 使用成熟的技术, 异步设计, 无状态系统, 水平扩展而非垂直升级, 设计时至少有两步前瞻性, 非核心则购买, 使用商品化硬件, 小构建小发布和快试错, 隔离故障, 自动化.

系统架构演进:

  1. 单体应用架构

    优点: 架构简单, 项目开发和维护成本低; 所有项目模块部署在一起,对于小型项目来说方便维护;

    缺点: 所有模块耦合在一起,对于大型项目,不易开发和维护; 项目各模块过于耦合,一旦有模块出现问题, 整个项目将不可用; 无法针对某个具体模块来提升性能; 无法对项目进行水平扩展.

  2. 垂直应用架构: 将原来的项目应用拆分为互不相干的几个应用,提升系统的整体性能.

    优点: 对系统进行拆分,可根据不同系统的访问情况,有针对性地进行优化; 能够实现应用的水平扩展; 各系统分担整体访问流量,解决了并发问题; 子系统发生故障, 不影响其他子系统的运行情况, 提高了整体的容错率;

    缺点: 拆分后的各系统之间相互独立, 无法进行相互调用; 各系统难免存在业务重叠, 会存在重复开发的业务,后期维护比较困难;

  3. 分布式架构: 在分布式架构中, 会将系统整体拆分为服务层和表现层, 服务层封装了具体的业务逻辑供表现层调用, 表现层则处理与页面的交互操作.

    优点: 将重复的业务代码抽象出来, 形成公共的访问服务, 提高了代码的复用性; 可以有针对性地对系统和服务进行性能优化, 提升整体地访问性能;

    缺点: 系统之间的调用关系变得复杂; 系统之间的依赖关系变得复杂; 系统维护成本高;

  4. SOA(面向服务)架构: 增加统一的调度中心对集群进行实时管理

    缺点: 各服务之间存在依赖关系, 如果某个服务出现崩溃, 可能会造成服务雪崩; 服务之间的依赖与调用关系复杂, 增加了测试和运维的成本.

  5. 微服务架构: 大的项目拆分为一个个小的可独立部署的微服务, 每个微服务都有自己的数据库

    优点: 服务彻底拆分, 各服务独立打包, 独立部署和独立升级; 每个微服务负责的业务比较清晰,利于后期扩展和维护;微服务之间可以采用REST和RPC协议进行通信;

    缺点: 开发成本较高; 涉及各服务的容错性问题; 涉及数据的一致性问题; 涉及分布式事务问题;

分布式事务场景

跨JVM进程: 各个微服务部署在不同的JVM进程中, 因此会产生因JVM进程导致的分布式事务问题.

跨数据库实例: 由于数据分布在不同的数据库实例中, 产生了分布式事务问题.

多服务访问单数据库: 多个微服务访问同一个数据库, 本质上是通过不同的数据库会话来操作数据库, 会产生分布式事务问题.

数据一致性

数据多副本场景: 服务器或系统发生故障时,可能会导致一部分副本写入成功, 一部分失败, 造成各个副本之间数据不一致.

调用超时场景: 包含同步调用超时和异步调用超时, 导致数据不一致.

缓存与数据库不一致场景: 缓存中的数据不能及时更新,会导致缓存和数据库中的数据不一致.

多个缓存节点数据不一致场景: 比如redis集群中由于网络异常等原因引起的脑裂问题, 会导致多个缓存节点数据不一致.

解决方案: ACID特性; CAP理论; Base理论; DTP模型; 2PC(两阶段提交)模型; 3PC(三阶段提交)模型; TCC模型; 可靠消息最终一致性模型; 最大努力通知模型等;

5.分布式事务的理论知识

CAP理论

CAP是一致性(Consistency), 可用性(Availability)和分区容忍性(Partition Tolerance)首字母的缩写.

一致性

往往会将一份数据复制多份存储. 一致性是指用户对数据的操作, 要么所有的数据副本都执行成功, 要么都失败; 一致性要求对所有数据节点的数据副本修改是原子操作, 所有数据节点的数据副本都是最新的, 从任意节点读取的数据都是最新的状态.

存在如下特点:

  1. 存在数据同步的过程, 应用程序的写操作存在一定的延迟.
  2. 为了保证各个节点数据的一致性, 需要对相应的资源进行锁定, 待数据同步完成后再释放锁定的资源.
  3. 如果数据写入并同步成功,所有节点都会返回最新的数据; 如果数据写入或同步失败, 所有节点都不会存在最新写入的数据

可用性

可用性是指客户端访问数据时, 能够快速得到响应.系统处于可用性状态时, 每个存储节点的数据可能会不一致,并不要求应用程序向数据库写入数据时能够立刻读取到最新的数据.处于可用性状态的系统, 任何事务的操作都可以得到响应的结果,不会存在超时或者响应错误的情况.

存在如下特点:

  1. 所有的请求都会被响应
  2. 不会存在响应超时或者响应错误的情况
  3. 如果对不同的应用程序设置了超时响应时间, 一旦超过超时时间, 系统将不可用

分区容忍性

分区容忍性是指存储系统部署并运行在多个不同的节点上, 并且节点处于不同的网络中, 形成网络分区, 不可避免出现网络问题, 导致节点之间的通信出现失败的情况, 但是此时的系统仍能对外提供服务.

存在如下特点:

  1. 一个节点挂掉, 不影响其他节点对外提供服务
  2. 分区容忍性是分布式系统必须具备的基础能力

CAP的组合

AP: 牺牲一致性, 追求系统的可用性和分区容忍性.达到最终一致性, 允许多个节点数据在一定的时间内存在差异,一段时间达到数据一致的状态.

CP: 牺牲可用性, 追求系统的一致性和分区容忍性.对数据的一致性要求较高,追求强一致性.

CA: 放弃分区容忍性, 追求系统的一致性和可用性.此时系统已经不再是一个标准的分布式系统.

Base理论

Base理论中的Base是基本可以(Basically Available), 软状态(Soft State)和最终一致性(Eventually Consistent)的缩写. Base理论允许部分数据不可用,但是会保证核心功能可用, 允许数据在一定时间内的不一致,但是经过一段时间, 数据最终是一致性的. 符合Base理论的事务成为柔性事务.

基本可用: 分布式系统出现故障时, 允许其损失系统的部分可用性, 但要保证系统基本可用.

软状态: 允许系统中存在中间状态,中间状态不会影响系统的可用性,只是允许系统各个节点之间的数据同步存在延迟.

最终一致性: 系统中各个节点的数据副本经过一段时间的同步, 最终能达到一致的状态,不要求各个节点的数据保持实时一致.

6.强一致性分布式事务解决方案

强一致性分布式事务概述

强一致性分布式事务要求在任意时刻查询参与全局事务的各节点的数据都是一致的.

典型方案: DTP模型(全局事务模型), 2PC模型(二阶段提交), 3PC(三阶段提交).

适用场景: 用于对数据一致性要求比较高,在任意时刻都要查询到最新写入数据的场景.

优点: 数据一致性比较高; 在任意时刻都能够查询到最新写入的数据;

缺点: 存在性能问题; 实现复杂; 牺牲了可用性; 不适合高并发场景;

DTP模型

重要概念

事务: 一个事务就是一个完整的工作单元, 具备ACID特性.

全局事务: 由事务管理器管理的事务, 能够一次性操作多个资源管理器.

分支事务: 由事务管理器管理的全局事务中, 每个资源管理器中独立执行的事务.

控制线程: 执行全局事务的线程, 线程用来关联应用程序, 事务管理器和资源管理器三者之间的关系(事务上下文环境).

执行流程

三个核心组件: AP(应用程序), RM(资源管理器), TM(事务管理器)

步骤:

  1. AP向TM请求开启全局事务, 然后注册资源1, 2, 3...
  2. AP操作RM执行操作
  3. 执行操作完成后, AP向TM请求提交事务
  4. TM向所有的RM发生准备命令, 并进行提交RM本地的事务

2PC模型

Prepare阶段: 事务管理器向每个参与全局事务的管理器发送Prepare消息,资源管理器要么返回失败,要么在本地执行相应的事务,将事务写入本地的Redo Log和Undo Log文件, 此时事务并没有提交.

Commit阶段: 如果事务管理器收到了参与全局事务的资源管理器返回的失败消息,则直接给Prepare阶段执行成功的资源管理器发送回滚消息, 否则向每个资源管理器发送Commit消息.相应的资源管理器根据事务管理器发送过来的消息指令,进行事务回滚或提交操作, 并释放事务处理过程中的锁资源.

存在的问题:

  1. 同步阻塞问题: 事务的执行过程中,所有参数事务的阶段都会对其占用的公共资源加锁,导致其他访问公共资源的进程或者线程阻塞.
  2. 单独故障问题: 如果事务管理器发生故障, 资源管理器会一直阻塞.
  3. 数据不一致问题: 如果在Commit阶段,由于网络或者部分资源管理器发生故障,导致部分资源管理器没有接收到事务管理器发送过来的Commit消息, 会引起数据不一致的问题.
  4. 无法解决的问题: 如果在Commit阶段,事务管理器发出Commit消息后宕机,并且唯一接收到这条Commit消息的资源管理器也宕机了,则无法确认事务是否已经提交.

3PC模型

3PC模型把2PC模型中Prepare阶段一分为二,最终形成三个阶段: CanCommit阶段, PreCommit阶段和DoCommit或者DoRollback阶段.

事务执行成功流程

CanCommit阶段: 事务管理器向参与全局事务的资源管理器发送CanCommit消息,资源管理器收到CanCommit消息, 认为能够执行事务, 会向事务管理器响应Yes消息,进入预备状态.

PreCommit阶段: 事务管理器向参与全局事务的资源管理器发送PreCommit消息,资源管理器收到PreCommit消息后,执行事务操作,将Undo和Redo信息写入事务日志,并向事务管理器响应Ack状态,但此时不会提交事务.

DoCommit阶段: 事务管理器向参与全局事务的资源管理器发送DoCommit消息,资源管理器收到DoCommit消息后,正式提交事务,并释放执行事务期间占用的资源,同时向事务管理器响应事务已提交的状态,事务管理器收到资源管理器响应的事务已提交的状态,完成事务的提交.

事务执行失败流程

CanCommit阶段: 事务管理器向参与全局事务的资源管理器发送CanCommit消息,资源管理器收到CanCommit消息, 认为不能执行事务, 会向事务管理器响应No状态.

PreCommit阶段: 事务管理器向参与全局事务的资源管理器发送Abort消息,资源管理器收到Abort消息或者期间出现超时,都会中断事务的执行.

DoCommit阶段: 事务管理器向参与全局事务的资源管理器发送Rollback消息,资源管理器收到会利用Undo Log日志信息回滚事务,并释放执行事务期间占用的资源,同时向事务管理器响应事务已回滚的状态,事务管理器收到资源管理器响应的事务已回滚的状态,完成事务的回滚.

存在的问题

和2PC模型相比,3PC主要解决了单点故障问题,减少了事务执行过程中的阻塞现象.

如果资源管理器无法及时收到来自事务管理器发出的消息,那么资源管理器会执行提交事务的操作,而不是持有事务资源并处于阻塞状态,但这种机制会导致数据不一致的问题.

由于网络故障等原因,导致资源管理器没有及时收到事务管理器发出的Abort消息,则资源管理器会在一段时间后提交事务,导致和其他接收到Abort消息并执行事务回滚操作的资源管理器的数据不一致.

7.最终一致性分布式事务解决方案

最终一致性分布式事务概述

最终一致性分布式事务不要求参与事务的各节点数据时刻保持一致,允许存在中间状态,只要一段时间后,能够达到数据的最终一致状态即可.

典型方案: TCC解决方案, 可靠消息最终一致性解决方案, 最大努力通知型解决方案.

优点: 性能比较高; 具备可用性; 适合高并发场景;

缺点: 数据存在短暂的不一致; 对事务一致性要求特别高的场景不太适用;

服务模式

可查询操作

需要服务操作具有可标识性,具有全局的唯一标识,要有完整的操作时间信息,如果出现了错误,通过可查询的接口来获取其他服务处理的情况.

幂等操作

幂等性指对于同一个方法来说,只要参数相同,无论执行多少次都与第一次执行时产生的影响相同.

业务服务对外提供操作业务数据的接口,并且需要在接口的实现中保证对数据处理的幂等性.

在分布式环境中, 难免会出现数据不一致的情况, 为了保证数据的最终一致性,系统会进行重试,如果重试的接口不幂等,即使重试成功也无法保证数据一致.

实现幂等性的方式: 一是通过业务操作本身实现幂等性; 二是通过系统缓存所有的请求与处理结果,当再次检测到相同的请求时,直接返回之前缓存的处理结果.

TCC操作

包括三个阶段: Try阶段(尝试业务执行), Confirm阶段(确定业务执行)和Cancel阶段(取消业务执行).

Try阶段:

  1. 完成所有业务的一致性检查
  2. 预留必要的资源,并需要和其他操作隔离

Confirm阶段:

  1. 此阶段真正执行业务操作
  2. 此阶段不会做任何业务检查(Try阶段已经执行过)
  3. 只用Try阶段预留的业务资源进行操作
  4. 此阶段的操作需满足幂等性

Cancel阶段:

  1. 释放Try阶段预留的业务资源
  2. 此阶段的操作需满足幂等性

可补偿操作

在分布式系统中,如果某些数据处于不正常的状态,需要通过某种方式进行业务补偿,使数据能够达到最终一致性.业务服务对外提供操作数据的接口时,也需要对外提供补偿业务的接口,当其他服务调用业务服务操作数据接口出现异常时,能够通过补偿接口进行业务补偿操作.

执行业务操作时,完成业务操作并返回结果,操作阶段对外部都是可见的.

进行业务补偿时,能够补偿或者抵消正向业务操作的结果,并且业务补偿操作需满足幂等性.

TCC解决方案

适用于具有强隔离性,严格一致性要求的业务场景,也适用于执行时间较短的业务.

需实现的服务模式: TCC操作, 幂等操作(每个阶段), 可补偿操作, 查询操作.

执行流程

Try阶段不会执行任何业务逻辑,仅做业务的一致性检查和预留相应的资源,这些资源能够和其他操作保持隔离.

当Try阶级所有分支事务执行成功后开始执行Confirm阶段,通常认为Confirm阶段不会出错,如果出错,需要引入重试机制或人工处理,对出错的事务进行干预.

在业务执行异常或者出现错误的情况下,需要回滚事务的操作,执行分支事务的取消操作,并释放Try阶段预留的资源,通常认为Confirm阶段不会出错,如果出错,需要引入重试机制或人工处理,对出错的事务进行干预.

优缺点

优点: 在应用层实现具体逻辑,锁定资源的粒度小; Confirm阶段和Cancel阶段的操作具有幂等性,保证数据的一致性; 主业务和分支业务都可集群部署;

缺点: 代码耦合到具体业务中;每个参与分布式事务业务方法都要拆分成Try, Confirm和Cancel三个阶段的方法,提高了开发成本;

需要注意的问题

空回滚

出现问题的原因: 一个分支事务所在的服务器宕机或者网络异常, 导致此分支事务未执行Try阶段的方法,当服务器或网络恢复后, TCC事务执行回滚操作,会调用此分支事务的Cancel阶段的方法.(因为未执行Try阶段方法,此时不应执行Cancel阶段的方法).

解决方案: 主业务发起全局事务时,生成全局事务记录,并生成全局唯一的ID,叫全局事务ID.分支事务执行Try阶段方法时, 记录全局事务ID和本地事务ID, 插入分支事务记录.当事务回滚执行Cancel阶段时, 需根据全局事务ID查询是否存在Try阶段的记录,如果有,则执行正常的操作,否则为空回滚,不进行任何操作.

幂等问题

由于服务器宕机,网络故障等原因,TCC会加入超时重试机制,需要分支事务的执行,Confirm阶段和Cancel阶段具备幂等性.

解决方案: 在分支事务记录中增加事务的执行状态,每次执行时,都查询此事务的执行状态.

悬挂问题

对于已经空回滚的业务, 之前被阻塞的Try操作恢复,继续执行Try, 就永远不可能执行Confirm或Cancel,事务一直处于中间状态,预留了业务资源,无法继续向下处理.

解决方案: 在执行Try阶段方法时,判断分支事务记录中是否存在同一个全局事务ID下的Confirm阶级或Cancel阶段的事务记录,如果存在,则不再执行Try阶段.

可靠消息最终一致性解决方案

事务的发起方执行完本地事务之后,发出一条消息,事务的参与方(消息消费者)一定能够接收到这条消息并处理成功.主要适用于消息数据能够独立存储,降低系统之间耦合度,且业务对数据一致性的时间敏感度高的场景.

需要实现的服务模式: 可查询操作和幂等操作.

执行流程

事务发起方将消息发送给可靠消息服务,消息服务可基于本地数据表实现, 也可基于消息中间件实现.事务参与方从可靠消息服务中接收消息.事务发起方, 可靠消息服务, 事务参与方都是通过网络通信,由于网络的不稳定性,需要引入消息确认服务和消息恢复服务.

消息确认服务会定期检测事务发起方业务的执行状态和消息库中的数据,如果发现不一致,消息确认服务会同步事务发起方的业务数据,保证数据一致性,确保事务发起方业务完成本地事务消息一定发送成功.

消息恢复服务会定期检测事务参与方业务的执行状态和消息库中的数据,如果不一致,消息恢复服务会恢复消息库中消息的状态,回滚消息状态为事务发起方发送消息成功,但未被事务参与方消费的状态.

优缺点

基于本地消息表实现

优点: 实现了消息的可靠性,减少对消息中间件的依赖.

缺点: 绑定了具体的业务场景,耦合性太高,不可公用和扩展; 消息数据和业务数据在同一个数据库,占用了业务系统的资源; 消息数据可能会收到数据库并发性的影响;

基于消息中间件实现

优点: 消息数据能够独立存储,与具体的业务数据库解耦; 消息的并发性和吞吐量优于本地消息表;

缺点: 发送一次消息需要完成两次网络交互,一次是消息的发送,另一次是消息的提交或回滚; 需要实现消息的回查接口,增加了开发成本.

需要注意的问题

事务发送方本地事务与消息发送的原子性问题: 要求事务发起方的本地事务与消息发送的操作具有原子性.通过消息确认服务解决.

事务参与方接收消息的可靠性问题: 由于服务器宕机,网络故障,导致事务参与方不能正常接收消息,或者接收消息后处理事务的过程中发生异常,无法将结果正常回传到消息库中.通过消息恢复服务解决.

事务参与方接收消息的幂等性问题: 可靠消息服务会多次向事务参与方发生消息,进行重试,如果事务参与方的方法不具备幂等性,会造成消息重复消费的问题.通过事务参与方的方法实现需具有幂等性解决.

最大努力通知型解决方案

适用于最终一致性时间敏感度低的场景,并且事务被动方的处理结果不会影响主动方的处理结果.分布式事务跨域多个不同的系统,尤其是不同企业之间的系统时.

需实现的服务模式: 可查询参数和幂等操作.

实现最大努力通知型方案需实现如下功能:

  1. 业务主动方在完成业务处理后,会向业务被动方发生消息通知.发生消息通知允许消息丢失
  2. 业务主动方可以设置时间阶梯型通知规则,在消息通知失败后,可以按照规则再次通知,直到到达最大通知次数为止
  3. 业务主动方需提供查询接口供业务主动方按照需要查询,用于恢复丢失的消息.

方案优缺点

优点: 实现跨企业的数据一致性; 业务被动方的处理结果不会影响业务主动方的处理结果; 能够快速接入其他业务系统,达到业务数据的一致性;

缺点: 只适用于时间敏感度低的场景; 业务主动方的消息可能丢失,造成业务被动方收不到消息; 需要业务主动方提供查询消息的接口,增加了开发成本;

需要注意的问题

消息重复通知问题: 业务主动方按照一定的阶梯型通知规则通知业务被动方,存在消息重复通知问题.通过业务被动方接收消息通知的方法具有幂等性解决

消息通知丢失的问题: 业务主动方尽最大努力还没通知到业务被动方,或者业务被动方需再次获取消息,但消息通知已丢失.通过业务主动方提供查询消息的接口解决.

8.XA强一致性分布式事务原理

DTP模型和XA规范

DTP模型主要定义了应用程序, 资源管理器, 事务管理器三个核心组件.

  1. 应用程序用于定义事务的开始和结束,且在事务边界内对资源进行操作.
  2. 资源管理器称为事务参与者,如数据库,文件系统,并提供访问资源的方式.
  3. 事务管理器称为事务协调者,负责分配事务唯一标识,监控事务的进行进度,并负责事务的提交回滚等操作.

XA规范

  1. xa_start: 负责开启或恢复一个事务分支,并且管理XID到调用线程.
  2. xa_end: 负责取消当前线程与事务分支的关联.
  3. xa_prepare: 负责询问资源管理器是否准备好提交事务分支.
  4. xa_commit: 负责通知资源管理器提交事务分支.
  5. xa_rollback: 负责通知资源管理器回滚事务分支.
  6. xa_recover: 负责列出需要恢复的XA事务分支.

二阶段提交:

  • 一阶段: 执行XA_PREPARE语句,事务管理器通知各个资源管理器准备提交它们的事务分支,资源管理器收到通知后执行XA_PREPARE语句.
  • 二阶段: 执行XA_COMMIT/ROLLBACK语句,事务管理器根据各个资源管理器的XA_PREPARE语句执行结果,决定提交事务还是回滚事务.

JTA规范

JTA规范是XA规范的Java版, 涉及到的接口有:

  1. javax.transaction.TransactionManager: 事务管理器, 负责事务的begin, commit, rollback等命令.
  2. javax.transaction.UserTransaction: 用于声明一个分布式事务.
  3. javax.transaction.TransactionSynchronizationRegistry: 事务同步注册
  4. javax.transaction.xa.XAResource: 定义资源管理器提供给事务管理器操作的接口
  5. javax.transaction.xa.Xid: 事务XID接口

事务管理器提供者需实现除XAResource的接口,通过与XAResource接口交互来实现分布式事务.

资源管理器提供者需实现XAResource接口.

MySQL XA事务

目前只有InnoDB存储引擎支持,在DTP模型中, MySQL属于资源管理器.

语法

(花括号是必填项,方括号是可填项)

  1. XA {START | BEGIN} xid [JOIN | RESUME]: 开始XA事务,如果是START则不支持[JOIN | RESUME].
  2. XA END xid [SUSPEND [FOR MIGRATE]]: 结束XA事务,如果开始事务是START则不支持[SUSPEND [FOR MIGRATE]]
  3. XA PREPARE xid: 准备提交XA事务
  4. XA COMMIT xid [ONE PHASE]: 提交XA事务
  5. XA ROLLBACK xid: 回滚XA事务
  6. XA RECOVER [CONVERT XID]: 列出所有处于Prepare阶段的XA事务

执行流程

graph LR; A[开启XA事务]--XA START-->B(Active状态); B--XA END-->C(Idle状态); C--XA COMMIT-->D(Commited状态); C--XA PREPARE-->E(Prepared状态); E--XA COMMIT-->D; E-->F(FAILED状态); F-->G(Aborted状态);

注意:

  1. 在XA START和XA END之间执行的是业务SQL语句,无论是否支持成功,都应该执行XA END语句.
  2. 在IDLE状态下的事务可直接执行XA COMMIT,当只有一个资源管理器时,退化成一阶段提交.
  3. 只有状态为Failed的时候,才能执行XA ROLLBACK进行XA事务回滚.
  4. XA事务和非XA事务是互斥的.

XA规范的问题

同步阻塞

各个事务分支的ACID特性保证了全局事务的ACID特性,可重复读隔离级别不足以保证分布式事务的一致性,需将事务隔离级别设置为串行化,但执行效率最低.

单点故障

一旦协调者事务管理器发生故障,参与者资源管理器会一直阻塞下去.

数据不一致

在Commit阶段,当协调者向参与者发送commit请求后,发生了局部网络故障,或者协调者发生了故障,会导致一部分参与者接收到commit请求,其他参与者未接收到,造成数据不一致.

问题的解决方案

XA数据不一致问题:

  1. 日志存储: 记录XA事务在每个流程中的执行状态.
  2. 自定义事务恢复: 首先通过XA RECOVER命令从资源管理器中获取需要被恢复的事务记录,然后根据XID匹配应用程序中存储的日志,根据事务状态进行提交或回滚.

单点故障问题:

  1. 去中心化部署: 事务管理器嵌套在应用程序里,不再单独部署.
  2. 中心化部署: 事务管理器单独部署,然后与应用程序进行远程通信.

9.TCC分布式事务原理

TCC核心思想

应用层将一个完整的事务操作分为Try Confirm Cancel三个阶段:

  1. Try阶段: 完成所有的业务检查,确保数据的一致性; 预留必要的业务资源,确保数据的隔离性.
  2. Confirm阶段: 真正的执行业务;不做任何业务逻辑检查,直接将数据持久化到数据库;直接使用Try阶段预留的业务资源.
  3. Cancel阶段: 释放Try阶段预留的业务资源; 将数据库中的数据恢复到最初状态.

TCC实现原理

一个完整的TCC分布式事务包含: 主业务服务, 从业务服务,TCC管理器.

主业务服务是TCC分布式事务的发起方.

从业务服务提供TCC业务操作,必须实现TCC分布式事务Try, Confirm和Cancel阶段的三个接口,供主业务服务调用.

TCC管理器管理和控制整个事务活动,包括记录全局事务和分支事务的状态,在Try阶段执行成功时,自动调用每个分支事务的Confirm阶段的操作,分支事务执行失败时,则自动调用Cancel阶段的操作.

TCC核心流程

在电商业务中的支付订单场景,如果订单支付成功,则需修改订单状态,扣减库存(库存服务),增加积分(积分服务),创建出库单(仓储服务)等.

Try阶段

  1. 订单服务将订单数据库中订单的状态更新为支付中.
  2. 订单服务调用库存服务冻结部分库存,将冻结的库存数量(下单商品数量)单独写入商品库存表的预扣减字段中,同时将商品库存数量减去冻结的商品数量.
  3. 订单服务调用积分服务进行预增加积分的操作,在用户积分数据表中,将要增加的积分写入单独的预增加积分字段中,而不是直接增加用户的积分.
  4. 订单服务调用仓储服务生成出库单,将出库单的状态标记为未知,并不直接生成正常的出库单.

Confirm阶段

Try阶段执行成功,TCC分布式事务会执行Confirm阶段的业务逻辑.

  1. 订单服务将顶峰数据库中订单的状态更新为支付成功.
  2. TCC分布式事务框架调用库存服务中Confirm阶段的方法,真正的扣减库存,将预扣减字段中的库存数量减去本次下单提交的商品数量.
  3. TCC分布式事务框架调用积分服务中Confirm阶段的方法,真正的增加积分,将预增加积分字段中的积分数量减去本次支付产生的积分数量,并且在用户的积分账户中增加本次支付产生的积分数量.
  4. TCC分布式事务框架调用仓储服务中Confirm阶段的方法,将出库单状态更新为已创建.

Cancel阶段

如果Try阶段的业务执行失败,或者某个服务出现异常,TCC分布式事务框架会自动调用Cancel阶段的方法.

  1. 订单服务将订单数据库中订单的状态标记为已取消.
  2. TCC分布式事务框架调用库存服务Cancel阶段的方法进行事务回滚,将库存数据表中的预扣减库存字段中存储的商品数量减去本次下单提交的商品数量,并将库存数据表中的商品库存字段存储的商品库存数量增加本次下单提交的商品数量.
  3. TCC分布式事务框架调用积分服务Cancel阶段的方法进行事务回滚,将积分数据中的预增加积分字段中的积分数量减去本次支付产生的积分数量.
  4. TCC分布式事务框架调用仓储服务Cancel阶段的方法进行事务回滚,将出库单的状态标记为已取消.

TCC关键技术

AOP切面: 通过AOP切面拦截具体的业务逻辑,在AOP切面中执行事务日志的记录,远程调用等逻辑.

反射技术: TCC分布式事务中Confirm阶段的方法和Cancel阶段的方法是通过反射技术调用的.

持久化技术: 由于网络的不稳定性,所有参与事务的服务都存在数据的持久化操作,保证数据的最终一致性.

序列化技术: 分布式环境中数据的持久化和在网络中的传输,都需要序列化技术的支持.

定时任务: 分布式环境,网络的不稳定,会出现方法调用失败的情况,需要利用定时任务重试.

动态代理: 通过动态代理的方式支持多种远程调用框架.

多配置源技术: 真正的业务场景中,会存在不同的配置存储技术.

10.可靠消息最终一致性分布式事务原理

基本原理

事务发起方(消息发送者)执行本地事务成功后发出一条消息,事务参与方(消息消费者)接收到事务发起方发生过来的消息,并成功执行本地事务.

  1. 事务发起方一定能够将消息成功发生出去
  2. 事务参与方一定能够成功接收到消息

本地消息表

通过本地事务,将业务数据和消息数据分别保存到本地数据库的业务数据表和本地消息表中,然后通过定时任务读取本地消息表中的数据,将消息发送到消息中间件,等待消息消费者成功接收到消息后,再将本地消息表中的消息删除.

存放消息的本地消息表和存放数据的业务数据表位于同一个数据库中,这种设计能够保证使用本地事务达到消息和业务数据的一致性,并且引入消息中间件实现多个分支事务的最终一致性.

流程如下:

  1. 事务发起方向业务数据表成功写入数据后,会向本地消息表发送一条消息数据,因为写业务数据和写消息数据在同一个本地事务中,所以本地事务会保证这条消息数据一定能够正确写入本地消息表中.
  2. 使用专门的定时任务将本地消息表中的消息写入消息中间件,如果写入成功,则会将消息从本地消息表中删除,否则根据一定的规则进行重试.
  3. 如果消息根据一定的规则写入消息中间件仍然失败,可以将失败的消息数据存储到死信队列数据表中后续进行人工干预,以达到事务最终一致性的目的.
  4. 事务参与方会订阅消息中间件的消息,当接收到消息中间件的消息时,完成本地的业务逻辑.
  5. 事务参与方执行本地事务成功,则整个分布式事务执行成功,否则会根据一定的规则进行重试.如果仍然不能成功执行本地事务,则会给事务发起方发送一条事务执行失败的消息,通知事务发起方进行事务回滚.

优点:

  1. 使用消息中间件在多个服务之间传递消息数据,一定程度上避免了分布式事务的问题.
  2. 作为业界使用比较多,相对成熟的方案.

缺点:

  1. 无法保证各个服务节点的数据强一致性
  2. 某个时刻可能查不到提交的最新数据
  3. 消息表会耦合到业务库中,需要手动处理发送消息的逻辑,不利于消息数据的扩展.如果消息表中存储了大量的数据,会对操作业务数据的性能造成一定的影响.
  4. 消息发送失败时需要重试,事务参与方要保证消息的幂等.
  5. 如果消息重试仍然失败,则需要引入人工干预机制
  6. 消息服务与业务服务耦合,不利于消息服务的扩展和维护
  7. 消息服务不能共用,每次都需要单独开发消息服务逻辑,增加了开发成本

独立消息服务

独立消息服务在本地消息表的基础上,将消息服务独立出来,并将消息数据从本地消息表独立成单独消息数据库,引入消息确认服务和消息恢复服务.

具体流程如下:

  1. 事务发起方向可靠消息服务成功发送消息后,执行本地事务.
  2. 可靠消息服务接收到事务发起方发送的消息后,将消息存储到消息库中,将消息记录的状态标记为待发送,并不会马上向消息中间件发送消息.同时向事务发起方响应消息发送已就绪的状态.
  3. 当事务发起方的事务执行成功时,事务发起方会向可靠消息服务发送确认消息,否则发送取消消息.
  4. 当可靠消息服务接收到事务发起方发送的确认消息时,会将消息发送到消息中间件,并将消息库中保存的当前消息记录状态标记为已发送.如果接收到取消消息,则直接将消息库中的消息记录状态改为已取消.
  5. 消息中间件接收到可靠消息服务发来的消息时,会将消息投递给业务参与方,业务参与方接收到消息后,执行本地事务,并将执行结果作为确认消息发送到中间件.
  6. 消息中间件将确认结果投递到可靠消息服务,可靠消息服务接收到确认消息后,根据结果状态将消息库中的当前消息记录标记为已完成.
  7. 如果事务发起方向可靠消息服务发送消息失败,会触发消息重试机制.如果重试后仍然失败,则会由消息确认服务定时校对事务发起方的事务状态和消息数据库中当前消息的状态,发现状态不一致时,采用一定的校对规则进行校对.
  8. 如果可靠消息服务向消息中间件发送消息失败,会触发消息重试机制.如果重试后仍然失败,则会由消息恢复服务根据一定的规则定时恢复消息库中的消息数据.

优点:

  1. 消息服务能够独立部署,开发和维护.
  2. 消息服务和业务服务解耦,具有更好的扩展性和伸缩性.
  3. 消息表从本地数据库解耦出来,使用独立的数据库存储,具有更好的扩展性和伸缩性.
  4. 消息服务可被多个服务共用,降低了重复开发消息服务的成本.
  5. 消息数据的可靠性不依赖于消息中间件,弱化了对于消息中间件的依赖性.

缺点:

  1. 发送一次消息需要请求两次接口
  2. 事务发起方需要开发比较多的事务查询接口,一定程度上增加了开发成本.

RocketMQ事务消息

RocketMQ的事务消息主要为了让Producer端的本地事务与消息发送逻辑形成一个完整的原子操作.Producer端和Broker端具有双向通信能力,使得Broker端具备事务协调者的功能.RocketMQ提供的消息存储机制能够对消息进行持久化操作.

整体原理流程:

  1. 事务发起方向RocketMQ发送Half消息.
  2. RocketMQ向事务发起方响应Half消息发送成功.
  3. 事务发起方执行本地事务.
  4. 事务发起方向RocketMQ发送提交事务或者回滚事务的消息.
  5. 如果事务参与方未收到消息或者执行事务失败且RocketMQ未删除保存的消息数据,则RocketMQ会回查事务发起方的接口,查询事务状态,以此确认再次提交事务还是回滚事务.
  6. 事务发起方查询本地数据库,确认事务是否是执行成功的状态.
  7. 事务发起方根据查询到的事务状态,向RocketMQ发送提交事务或者回滚事务的消息.
  8. 如果7中,事务发起方向RocketMQ发送的是提交事务的消息,则RocketMQ会向事务参与方投递消息.
  9. 如果7中,事务发起方向RocketMQ发送的是回滚事务的消息,则RocketMQ不会向事务参与方投递消息,且会删除内部存储的消息数据.
  10. 如果RocketMQ向事务参与方投递的是执行本地事务的消息,则事务参与方会执行本地事务.
  11. 如果RocketMQ向事务参与方投递的是查询本地事务状态的消息,则事务参与方会查询本地数据库中事务的执行状态.

本地事务监听接口:

public interface RocketMQLocalTransactionListener {
    RocketMQLocalTransactionState executeLocalTransaction(Message message, Object obj);

    RocketMQLocalTransactionState checkLocalTransaction(Message message);
}

具体实现代码:

@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "tx_order_group")
public class OrderTxMessageListener implements RocketMQLocalTransactionListener {
    @Autowired
    private OrderService orderService;
    @Autowired
    private OrderMapper orderMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object obj) {
        try{
            log.info("订单微服务执行本地事务");
            TxMessage txMessage = this.getTxMessage(msg);
            //执行本地事务
            orderService.submitOrderAndSaveTxNo(txMessage);
            //提交事务
            log.info("订单微服务提交事务");
            return RocketMQLocalTransactionState.COMMIT;
        }catch (Exception e){
            e.printStackTrace();
            //异常回滚事务
            log.info("订单微服务回滚事务");
            return RocketMQLocalTransactionState.ROLLBACK;
        }

    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        log.info("订单微服务查询本地事务");
        TxMessage txMessage = this.getTxMessage(msg);
        Integer exists = orderMapper.isExistsTx(txMessage.getTxNo());
        if(exists != null){
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    private TxMessage getTxMessage(Message msg){
        String messageString = new String((byte[]) msg.getPayload());
        JSONObject jsonObject = JSONObject.parseObject(messageString);
        String txStr = jsonObject.getString("txMessage");
        return JSONObject.parseObject(txStr, TxMessage.class);
    }
}

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    RocketMQTemplate rocketMQTemplate;
    // 供事务监听器调用
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitOrderAndSaveTxNo(TxMessage txMessage) {
        Integer existsTx = orderMapper.isExistsTx(txMessage.getTxNo());
        if(existsTx != null){
            log.info("订单微服务已经执行过事务,商品id为:{},事务编号为:{}",txMessage.getProductId(), txMessage.getTxNo());
            return;
        }
        //生成订单
        Order order = new Order();
        order.setId(System.currentTimeMillis());
        order.setCreateTime(new Date());
        order.setOrderNo(String.valueOf(System.currentTimeMillis()));
        order.setPayCount(txMessage.getPayCount());
        order.setProductId(txMessage.getProductId());
        orderMapper.saveOrder(order);

        //添加事务日志
        orderMapper.saveTxLog(txMessage.getTxNo());
    }
    
    // 注意此方法是暴露给Controller使用的提交订单发放,在事务监听器内中调用了submitOrderAndSaveTxNo方法
    @Override
    public void submitOrder(Long productId, Integer payCount) {
        //生成全局分布式序列号
        String txNo = UUID.randomUUID().toString();
        TxMessage txMessage = new TxMessage(productId, payCount, txNo);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("txMessage", txMessage);
        Message<String> message = MessageBuilder.withPayload(jsonObject.toJSONString()).build();
        //发送一条事务消息
        rocketMQTemplate.sendMessageInTransaction("tx_order_group", "topic_txmsg", message, null);
    }
}

消息发送的一致性

要保证消息发送的一致性,就要实现消息的发送和确认机制.整体流程:

  1. 事务发起方向消息中间件发送待确认消息
  2. 消息中间件接收到事务发起方发送过来的消息,将消息存储到本地数据库,此时并不会向事务参与方投递消息.
  3. 消息中间件向事务发起方返回消息存储结果,如果存储成功,事务发起方会执行后续的业务逻辑,否则事务发起方不再执行后续的业务逻辑,必要时还会向上层抛出异常.
  4. 事务发起方完成业务处理后,把业务处理的结果发送给消息中间件.
  5. 消息中间件根据事务发起方发来的处理结果数据,如果业务处理结果为成功,则消息中间件会更新本地数据库中消息状态为待发送,否则将本地数据库的消息状态标记为已删除(或直接删除).
  6. 事务参与方会监听消息中间件,并接收状态为待发送的消息,当接收到消息后,执行业务逻辑,消息中间件将对应的消息记录变更为已发送.
  7. 事务参与方执行完业务操作,会向消息中间件发送确认消息,表示事务参与方接收到消息并完成业务逻辑,消息中间件会将消息从本地数据库中删除.
  8. 为了保证事务发起方的一定能够将消息发送出去,在事务发起方应用服务中需要暴露一个回调查询接口,消息服务在后台开启线程定时扫描消息服务中的待发送消息,回调事务发起方提供的回调查询接口,回查事务执行状态.如果事务状态为执行成功,且消息状态为待发送,则将消息投递出去,且标记为已发送;如果事务状态为执行失败,则会删除对应的消息,不再投递.
  9. 消息中间件会根据状态向事务发起方投递事务参与方的执行状态,事务发起方会根据状态执行对应的操作,比如事务回滚等.

消息接收的一致性

实现消息接收的一致性需解决如下问题:

  1. 限制消息中间件重复投递消息的最大次数
  2. 事务参与方接收消息的接口满足幂等性
  3. 实现事务参与方与消息中间件的确认机制
  4. 消息中间件中的消息多次重试失败时,放入死信队列,后续引入人工干预

具体处理流程:

  1. 消息中间件向事务参与方投递消息时,如果失败,则会按照一定规则重新投递.
  2. 如果重试次数达到最大,消息仍然无法投递出去,则将对应的消息存入死信队列,后续通过人工干预投递.
  3. 如果消息正确投递出去,则会将数据库中存储的对应消息记录状态更新为已发送.
  4. 事务参与方接收到消息中间件投递的消息,执行业务操作,并将操作结果发送给消息中间件.
  5. 消息中间件接收到事务参与方发送的操作结果消息后,根据操作结果更新数据库中的消息记录状态,还会根据操作结果执行是否向事务发起方投递消息的逻辑.

消息的可靠性

消息发送的可靠性

需要保证事务发起方的可靠性,事务发起方部署多份形成集群模式.

需要保证消息生产和发送的可靠性,引入回调确认机制: 消息服务通过回调获取事务执行状态和消息数据,确保消息一定被消息服务成功接收;消息服务收到消息后,会返回一个确认消息.如果事务发起方一定时间内没有收到确认消息,就会触发消息重试机制.消息服务也需实现幂等.消息重试需要实现响应时长判断和重试次数限制.

消息存储的可靠性

确保消息能够进行持久化,使用消息存储的多副本机制.

消息消费的可靠性

需要确保事务参与方成功消费消息,事务参与方部署多份形成集群模式.还要实现事务参与方的重试机制和幂等机制.

11.最大努力通知型分布式事务原理

适用场景

方案适用于如下特点的场景:

  1. 业务主动方完成业务逻辑操作后,向业务被动方发送消息,允许消息丢失.
  2. 业务主动方需要提供消息回查接口,供业务被对方回查调用,以恢复丢失的业务消息.
  3. 业务被动方未接收到业务主动方发送的消息,或者接收到消息执行业务逻辑失败,业务被动方查询业务主动方提供的消息回查接口,进行数据校对.
  4. 业务被动方接收到业务主动方发来的消息,执行完业务处理,需要向业务主动方返回已成功接收消息的状态,避免业务主动方触发消息重试机制.
  5. 业务主动方未及时收到被动方返回的确认消息时,会根据一定的延时策略向业务被动方重新发送消息.
  6. 业务被动方的业务处理结果不影响业务主动方的业务处理结果.

方案特点

  1. 使用到的服务模式有可查询操作, 幂等操作.
  2. 对最终一致性的时间敏感度低.
  3. 业务被动方的业务处理结果不影响业务主动方的业务处理结果.
  4. 多用于跨企业的系统,或者企业内部比较独立的系统之间实现事务一致性,不同系统之间实现事务一致性.
  5. 允许消息丢失.
  6. 业务主动方可以根据一定的策略设置阶梯型通知规则,通知失败后重复通知到最大次数为止.
  7. 业务主动方需提供查询接口给业务被动方按照需求进行校对查询,以便恢复可能丢失的业务信息.

基本原理

业务主动方主要分为业务处理服务,消息中间件,消息消费服务,消息通知服务.流程如下:

  1. 业务处理服务处理完业务逻辑,向消息中间件发送消息数据.
  2. 消息中间件接收到消息数据后,将消息保存到消息数据库中,并将消息记录的状态标记为待发送.
  3. 消息消费服务订阅消息中间件的消息数据,当接收到消息数据时,会向消息中间件发送确认消息.
  4. 消息中间件收到消息消费服务的确认消息,会将消息数据库中对应的消息记录状态标记未已发送.
  5. 消息消费服务调用消息通知服务的接口,将消息数据传递给消息通知服务.
  6. 消息通知服务向业务被动方发送通知消息,并将通知记录保存到通知记录库中.
  7. 如果业务被动方没有收到通知消息,或者收到通知消息后处理业务逻辑失败,或者需要再次获取通知消息,则会按照需求主动查询业务主动方提供的回调接口,以便恢复丢失的业务信息.
  8. 业务被动方接收消息通知服务的接口需要实现消息的幂等操作.

异常处理

消息中间件宕机

如果消息中间件宕机,业务处理服务会多次重试将消息发送给消息中间件,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.

消息消费服务宕机

如果消息消费服务宕机,消息中间件会多次重试将消息重新投递,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.

消息通知服务宕机

如果消息通知服务宕机,消息消费服务会多次重试调用消息通知服务的接口,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.

业务被动方宕机

如果业务被动方宕机,消息通知服务会按照一定的规则向业务被动方阶梯式消息通知,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.

博客项目
程序员内功
码出好代码
  • 作者:lzlg520
  • 发表时间:2022-12-07 01:10
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 公众号转载:请在文末添加作者公众号二维码