事务是指逻辑上的一组操作,或单个逻辑单元执行的一系列操作, 同属于一个事务的操作会当作一个整体提交给系统,操作要么全部执行成功, 要么全部执行失败.
A (Atomic原子性): 构成事务的所有操作要么全部执行成功, 要么全部执行失败.
C (Consistency一致性): 事务执行前后, 数据始终处于一致的状态.
I (Isolation隔离性): 并发执行的事务之间互不干扰.
D(Durability持久性): 事务提交完成后, 事务对数据的更改操作会持久化到数据库中,且不会回滚.
扁平事务: 通常由begin或start transaction开始, commit或rollback结束.
带保存点的扁平事务: 通过在事务内部的某个位置设置保存点(save point),达到将当前事务回滚到此保存点的目的.
设置事务保存点: savepoint 保存点名称
回滚到保存点: rollback to 保存点名称
删除保存点: release savepoint 保存点名称
链式事务: 自动将当前事务的上下文隐式地传递给下一个事务, 一个事务地提交操作和下一个事务地开始操作具备原子性, 上一个事务的处理结果对下一个事务是可见的, 事务之间像链条一样传递下去.
注意: 链式事务在提交的时候会释放要提交的事务的所有锁和保存点, 链式事务的回滚操作只能回滚到当前所在事务的保存点, 不能回滚到已提交事务的保存点.
嵌套事务: 多个事务处于嵌套状态, 共同完成一项任务的处理.嵌套事务最外层有一个顶层事务, 顶层事务控制在所有的内部子事务, 内部子事务提交完成后, 整体事务不会提交, 只有顶层事务提交完成后, 整体事务才算提交完成.
回滚嵌套事务内部的子事务时, 会将事务回滚到外部顶层事务的开始位置.
嵌套事务的提交是从内部的子事务向外依次进行的, 直到最外层的顶层事务提交完成.
回滚嵌套事务最外层的顶层事务时, 会回滚嵌套事务内的所有事务,包括已提交的内部子事务.
分布式事务: 事务的参与者,事务所在的服务器,涉及的资源服务器以及事务管理器位于不同分布式系统的不同服务或数据库节点上.
更新丢失(脏写): 对同一行数据, 一个事务对该行数据的更新操作覆盖了其他事务对该行数据的更新操作, 本质是写操作的冲突, 解决方式是让每个事务按照按照一定的顺序依次进行写操作.
脏读: 一个事务读取到另一个事务未提交的数据. 本质是读写操作的冲突, 解决方法先写后读.
不可重复读: 同一个事务使用相同的查询语句, 在不同时刻读取到的结果不一致(其他事务也都数据进行更改了).本质是读写操作的冲突, 解决方法先读后写.
幻读: 一个事务两次读取一个范围的数据记录, 读取的结果不同(其他事务插入了一条数据).本质上是读写操作的冲突, 解决方法先读后写.
不可重复读和幻读的不同:
读未提交(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
不同事务隔离级别对并发事务的问题的解决程度对比:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
悲观锁: 整个数据处理的过程中会将相应的数据锁定, 读取数据和修改数据都需要加锁.
乐观锁: 通过数据版本机制实现的, 为数据增加一个版本标识, 更新数据时, 会将版本号的字段加1, 提交数据时, 如果提交的数据版本号大于数据表中当前要修改的数据的版本号,则对数据进行修改,否则不进行修改.
读锁: 又称为共享锁或S锁(Shared Lock), 针对同一份数据可以加多个读锁而互不影响.但此时不能为数据增加写锁.
写锁: 又称为排他锁或X锁(Exclusive Lock), 如果当前写锁未释放, 会阻塞其他的写锁或读锁.一旦加了写锁, 则不能再增加写锁和读锁.
表锁: 又称表级锁, 在整个数据表上对数据进行加锁和释放锁, 开销比较小, 加锁速度快, 一般不会出现死锁, 锁定的粒度比较大, 发生锁冲突的概率最高, 并发度最低.
两张表级锁模式: 表共享锁(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 ]
页面锁: 也称页级锁, 在页面级别对数据进行加锁和释放锁, 开销介于表锁和行锁之间, 可能会出现死锁, 锁定的粒度大小介于表锁和行锁之间, 并发度一般.
有数据表 account 如下:
id(主键) | name | balance(非唯一索引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, 当我们使用索引进行范围查询, 命中了记录的情况下, 就是使用了临键锁, 相当于记录锁+间隙锁.
两种退化的情况:
左开右闭区间, 目的是为了解决幻读的问题.
select * from account where id > 2 and id < 9;
上面的sql命中了id = 3的记录,也包含了记录不存在的区间, 所以他锁住(3, 15] 区间, 在这区间, 别的事务插入不了数据, 所以部分解决了幻读问题.
只有4个必要条件都满足时, 才会发生死锁.
查看死锁的日志信息: show engine innodb status\G
在MVCC机制中, 每个连接到数据的读操作, 在某个瞬间看到的都是数据库中数据的一个快照, 而写操作的事务提交之前, 读操作是看不到这些数据的变化的.
本质上, MVCC机制保存了数据库中数据在某个时间点上的数据快照, 同一个读操作的事务, 按照相同的条件查询, 无论查询多少次结果都一样.不同的事务在同一时刻看到的同一张表的数据可能不同.
在InnoDB引擎中, MVCC机制通过每行数据表记录后的两个隐藏的列来实现的, 一个列用来保存行的创建版本号, 另一个列用来保存行的过期版本号.每当有一个新的事务执行时, 版本号会自动递增, 事务开始时刻的版本号作为事务的版本号, 用于和查询到的每行记录的版本号作对比.
InnoDB引擎会根据下面两个条件检查每行记录:
只有符合上面两个条件的数据行, 才会被返回作为查询的结果.
InnoDB引擎会将新插入的每一行记录的当前系统版本号作为行版本号.删除版本号未定义.
InnoDB引擎会插入一条新记录, 并保存当前系统版本号作为新记录的版本号, 同时保存当前系统版本号作为原记录的行删除标识.原记录会被复制到Undo Log中.
InnoDB引擎会保存删除的每一行记录当前的系统版本号作为行删除标识.
事务的隔离性是由锁和MVCC机制实现的, 原子性和持久性是由Redo Log实现的, 一致性是由Undo Log实现的.
Redo Log也被称作重做日志, InnoDB引擎产生的, 用于保证事务的原子性和持久性.主要记录的是物理日志, 对磁盘上的数据进行的修改操作,用来恢复提交后的物理数据页, 不过只能恢复到最后一次提交的位置.
Redo Log通常包含两个部分: 一部分是内存中的日志缓存, 称作 Redo Log Buffer.另一部分是放在磁盘上的重做日志文件, 称作 Redo Log File.
Redo Log在MySQL发生故障时, 尽力避免内存中的脏页数据写入数据表的IBD文件, 在重启MySQL服务时, 可以根据Redo Log恢复事务已经提交,但是还未写入IBD文件的数据.
InnoDB引擎通过提交事务时强制执行写日志操作机制实现事务的持久化,默认每次将Redo Log Buffer中的日志写入日志文件时,都调用一次操作系统的fsync()操作. 写入磁盘的Redo Log文件时, 需要经过内核空间的OS Buffer, 这是因为在打开日志文件时, 没有使用O_DIRECT标志, 而O_DIRECT标志可以不经过内核空间的OS Buffer直接向磁盘写数据.
刷盘规则:
innodb_flush_log_at_trx_commit的取值有 0, 1, 2, 默认为1.
Redo Log文件内容是以顺序循环的方式写入的, 一个文件写满时会写入另一个文件, 最后一个文件写满时, 会向第一个文件写数据, 并且是覆盖写.
两个指针Write Pos和Checkpoint: Write Pos随着不断向数据表中写数据, 会向后移动, 移动到最后一个文件的最后一个位置时, 会回到第一个文件的开始位置. Checkpiont开始在第一个文件的开始位置, 当Write Pos写满所有文件时(Write Pos回到第一个文件的开始位置), 这时Checkpoint和Write Pos相遇, 表明数据已经写满, 此时擦除一些记录, Checkpoint移动到擦除的记录的最后位置, Write Pos则可以继续向数据表中写数据.
LSN: 日志的逻辑序列号, 占据8字节的空间, 并且值是单调递增. 从LSN中可获取 Redo Log写入数据的总量, 检查点位置, 数据页版本相关的信息.
LSN除了存在于Redo Log中, 还存在于数据页中, fil_page_ls参数记录当前页最终的LSN值.如果数据页的LSN值小于Redo Log中的LSN值, 则表示丢失了一部分数据, 可通过Redo Log的记录来恢复数据.
show engine innodb status \G
可查看LSN值:
show variables like '%innodb_log%'
查看Redo 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服务的情况下修改.
事务提交之前, 向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.
show variables like '%undo%'
查看Undo Log的相关信息:
BinLog是记录所有MySQL数据库表结构变更已经表数据变更的二进制日志, 不会记录select和show这类查询操作的日志, BinLog以事件的形式记录,并包含执行语句所消耗的时间.
使用场景:
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写入.
MySQL提供了组提交(group commit)的功能, 调用一次fsync()函数能够将多个事务的日志刷新到磁盘的日志文件中, 大大提升了日志刷盘的效率.
提交事务会在InnoDB存储引擎的上层将事务按照一定的顺序放入一个队列, 队列中的第一个事务为leader, 其他事务为follower, 先写BinLog, 再写事务日志. BinLog和事务日志都通过组提交功能进行写入.BinLog是通过二进制日志组提交(Binary Log Group Commit), 分为Flush, Sync和Commit阶段:
Flush阶段不是写完立刻进入Sync阶段, 而是等待一定时间,积累几个事务的BinLog再以前进入Sync阶段, 这个等待事件由binlog_max_flush_queue_time决定, 默认为0.
一组事务正在执行Commit阶段的操作的同时, 其他新产生的事务可以执行Flush阶段的操作, Commit阶段的事务和Flush阶段的事务不会相互阻塞, 组提交功能持续生效.
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在事务的执行过程中, 会记录相应的SQL语句的Undo Log和Redo Log, 然后在内存中更新数据并形成数据脏页, 接下来Redo Log会根据一定的规则触发刷盘操作, Undo Log和数据脏页通过检查点机制刷盘, 提交事务时, 会将当前事务相关的Redo Log刷盘, 只有当前事务相关的所有Redo Log刷盘成功, 事务才算提交成功.
如果事务在提交之前, MySQL崩溃或宕机, 此时会先使用Redo Log恢复数据, 然后使用Undo Log回滚数据. 如果在事务提交之后, Undo Log和数据脏页还未刷盘之前, MySQL崩溃或宕机, 此时使用Redo Log恢复数据.
MySQL中只有InnoDB引擎支持XA分布式事务.
XA事务本质上是一种基于两阶段提交的分布式事务(多个数据库事务共同完成一个原子性的事务操作).使用XA事务时, InnoDB引擎的事务隔离级别需要设置为串行化.
XA事务由一个事务管理器(Transaction Manager), 一个或多个资源管理器(Resource Manager)和一个应用程序(Application Program)组成.
XA事务分为Prepare阶段和Commit阶段:
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]]
从本质上讲Spring事务是对数据库事务的进一步封装, 如果数据库不支持事务, Spring也无法实现事务操作.
JDBC通过事务方式操作数据库的步骤:
Class.forName("com.mysql.jdbc.Driver")
Connection conn = DriverManager.getConnect(url, "name", "password")
conn.setAutoCommit(true/false)
PreparedStatement ps = conn.prepareStatement(sql); ps.executeUpdate(); ps.executeQuery();
conn.commit(); conn.rollback();
ps.close(); conn.close();
开启事务, 提交事务, 回滚事务的操作全部交由Spring框架自动完成.在配置文件或者项目的启动类中配置Spring事务相关的注解驱动, 在相关的类或者方法上标识@Transactional注解, 则可开启并使用Spring的事务管理功能.
Spring框架在启动的时候会创建相关的bean实例对象, 并且会扫描标注有相关注解的类和方法, 为这些方法生成代理对象, 如果扫描到标注有@Transactional注解的类或者方法, 会根据@Transactional注解的相关参数进行配置注入, 在代理对象中会处理相应的事务, 对事务进行管理.这些操作都是Spring框架通过AOP代理自动完成的.
管理的事务可分为逻辑事务和物理事务两大类:
Spring支持两种事务声明方式:
事务超时: 对性能要求较高的应用, 要求事务执行的时间尽可能短, 可以给事务设置超时时间, 一般以秒为单位, 如果超时时间过长, 影响系统的并发性与整体性能.因为检测事务超时的任务是在事务开始时启动的, 所以事务超时机制对于程序在执行过程中会创建新事务的传播行为才有意义, 比如Spring中事务传播类型: REQUIRED, REQUIRES_NEW, NESTED三种.
事务回滚: 使用Spring管理事务, 可以指定在方法抛出异常时, 哪些异常能够回滚事务,哪些异常不回滚.默认情况下, 方法抛出RuntimeException时回滚事务, 也可手动指定回滚事务的异常类型: @Transactional(rollbackFor = Exception.class)
这里的rollbackFor属性可以指定Throwable异常类及其子类.
PlatformTransactionManager, TransactionDefinition 和TransactionStatus 三大接口.
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;
}
主要定义了与事务相关的方法, 表示事务属性的常量等信息.
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;
}
}
public interface TransactionExecution {
// 是否是新事务
boolean isNewTransaction();
// 设置为只回滚
void setRollbackOnly();
// 是否为只回滚
boolean isRollbackOnly();
// 判断当前事务是否已完成
boolean isCompleted();
}
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
// 是否有保存点
boolean hasSavepoint();
// 将事务涉及到的数据刷新到磁盘
void flush();
}
类别 | 事务传播机制 |
---|---|
支持当前事务 | 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;
}
}
Spring事务传播类型 | 使用场景 |
---|---|
REQUIRED | Spring中默认的,适用于大部分场景 |
NOT_SUPPORTED | 适用于发送提示信息,站内信,短信,邮件等.这类场景要求不影响系统的主体业务逻辑,即使操作失败也不应该对主体逻辑产生影响,不能使主体逻辑的事务回滚. |
REQUIRES_NEW | 适用于不受外层方法事务影响的场景,比如记录日志,不管主体业务逻辑是否已经完成,日志都要记录下来,不能因为主体业务逻辑异常事务回滚导致日志操作回滚 |
15项架构原则: N+1设计, 回滚设计, 禁用设计, 监控设计, 设计多活数据中心, 使用成熟的技术, 异步设计, 无状态系统, 水平扩展而非垂直升级, 设计时至少有两步前瞻性, 非核心则购买, 使用商品化硬件, 小构建小发布和快试错, 隔离故障, 自动化.
系统架构演进:
单体应用架构
优点: 架构简单, 项目开发和维护成本低; 所有项目模块部署在一起,对于小型项目来说方便维护;
缺点: 所有模块耦合在一起,对于大型项目,不易开发和维护; 项目各模块过于耦合,一旦有模块出现问题, 整个项目将不可用; 无法针对某个具体模块来提升性能; 无法对项目进行水平扩展.
垂直应用架构: 将原来的项目应用拆分为互不相干的几个应用,提升系统的整体性能.
优点: 对系统进行拆分,可根据不同系统的访问情况,有针对性地进行优化; 能够实现应用的水平扩展; 各系统分担整体访问流量,解决了并发问题; 子系统发生故障, 不影响其他子系统的运行情况, 提高了整体的容错率;
缺点: 拆分后的各系统之间相互独立, 无法进行相互调用; 各系统难免存在业务重叠, 会存在重复开发的业务,后期维护比较困难;
分布式架构: 在分布式架构中, 会将系统整体拆分为服务层和表现层, 服务层封装了具体的业务逻辑供表现层调用, 表现层则处理与页面的交互操作.
优点: 将重复的业务代码抽象出来, 形成公共的访问服务, 提高了代码的复用性; 可以有针对性地对系统和服务进行性能优化, 提升整体地访问性能;
缺点: 系统之间的调用关系变得复杂; 系统之间的依赖关系变得复杂; 系统维护成本高;
SOA(面向服务)架构: 增加统一的调度中心对集群进行实时管理
缺点: 各服务之间存在依赖关系, 如果某个服务出现崩溃, 可能会造成服务雪崩; 服务之间的依赖与调用关系复杂, 增加了测试和运维的成本.
微服务架构: 大的项目拆分为一个个小的可独立部署的微服务, 每个微服务都有自己的数据库
优点: 服务彻底拆分, 各服务独立打包, 独立部署和独立升级; 每个微服务负责的业务比较清晰,利于后期扩展和维护;微服务之间可以采用REST和RPC协议进行通信;
缺点: 开发成本较高; 涉及各服务的容错性问题; 涉及数据的一致性问题; 涉及分布式事务问题;
跨JVM进程: 各个微服务部署在不同的JVM进程中, 因此会产生因JVM进程导致的分布式事务问题.
跨数据库实例: 由于数据分布在不同的数据库实例中, 产生了分布式事务问题.
多服务访问单数据库: 多个微服务访问同一个数据库, 本质上是通过不同的数据库会话来操作数据库, 会产生分布式事务问题.
数据多副本场景: 服务器或系统发生故障时,可能会导致一部分副本写入成功, 一部分失败, 造成各个副本之间数据不一致.
调用超时场景: 包含同步调用超时和异步调用超时, 导致数据不一致.
缓存与数据库不一致场景: 缓存中的数据不能及时更新,会导致缓存和数据库中的数据不一致.
多个缓存节点数据不一致场景: 比如redis集群中由于网络异常等原因引起的脑裂问题, 会导致多个缓存节点数据不一致.
解决方案: ACID特性; CAP理论; Base理论; DTP模型; 2PC(两阶段提交)模型; 3PC(三阶段提交)模型; TCC模型; 可靠消息最终一致性模型; 最大努力通知模型等;
CAP是一致性(Consistency), 可用性(Availability)和分区容忍性(Partition Tolerance)首字母的缩写.
往往会将一份数据复制多份存储. 一致性是指用户对数据的操作, 要么所有的数据副本都执行成功, 要么都失败; 一致性要求对所有数据节点的数据副本修改是原子操作, 所有数据节点的数据副本都是最新的, 从任意节点读取的数据都是最新的状态.
存在如下特点:
可用性是指客户端访问数据时, 能够快速得到响应.系统处于可用性状态时, 每个存储节点的数据可能会不一致,并不要求应用程序向数据库写入数据时能够立刻读取到最新的数据.处于可用性状态的系统, 任何事务的操作都可以得到响应的结果,不会存在超时或者响应错误的情况.
存在如下特点:
分区容忍性是指存储系统部署并运行在多个不同的节点上, 并且节点处于不同的网络中, 形成网络分区, 不可避免出现网络问题, 导致节点之间的通信出现失败的情况, 但是此时的系统仍能对外提供服务.
存在如下特点:
AP: 牺牲一致性, 追求系统的可用性和分区容忍性.达到最终一致性, 允许多个节点数据在一定的时间内存在差异,一段时间达到数据一致的状态.
CP: 牺牲可用性, 追求系统的一致性和分区容忍性.对数据的一致性要求较高,追求强一致性.
CA: 放弃分区容忍性, 追求系统的一致性和可用性.此时系统已经不再是一个标准的分布式系统.
Base理论中的Base是基本可以(Basically Available), 软状态(Soft State)和最终一致性(Eventually Consistent)的缩写. Base理论允许部分数据不可用,但是会保证核心功能可用, 允许数据在一定时间内的不一致,但是经过一段时间, 数据最终是一致性的. 符合Base理论的事务成为柔性事务.
基本可用: 分布式系统出现故障时, 允许其损失系统的部分可用性, 但要保证系统基本可用.
软状态: 允许系统中存在中间状态,中间状态不会影响系统的可用性,只是允许系统各个节点之间的数据同步存在延迟.
最终一致性: 系统中各个节点的数据副本经过一段时间的同步, 最终能达到一致的状态,不要求各个节点的数据保持实时一致.
强一致性分布式事务要求在任意时刻查询参与全局事务的各节点的数据都是一致的.
典型方案: DTP模型(全局事务模型), 2PC模型(二阶段提交), 3PC(三阶段提交).
适用场景: 用于对数据一致性要求比较高,在任意时刻都要查询到最新写入数据的场景.
优点: 数据一致性比较高; 在任意时刻都能够查询到最新写入的数据;
缺点: 存在性能问题; 实现复杂; 牺牲了可用性; 不适合高并发场景;
事务: 一个事务就是一个完整的工作单元, 具备ACID特性.
全局事务: 由事务管理器管理的事务, 能够一次性操作多个资源管理器.
分支事务: 由事务管理器管理的全局事务中, 每个资源管理器中独立执行的事务.
控制线程: 执行全局事务的线程, 线程用来关联应用程序, 事务管理器和资源管理器三者之间的关系(事务上下文环境).
三个核心组件: AP(应用程序), RM(资源管理器), TM(事务管理器)
步骤:
Prepare阶段: 事务管理器向每个参与全局事务的管理器发送Prepare消息,资源管理器要么返回失败,要么在本地执行相应的事务,将事务写入本地的Redo Log和Undo Log文件, 此时事务并没有提交.
Commit阶段: 如果事务管理器收到了参与全局事务的资源管理器返回的失败消息,则直接给Prepare阶段执行成功的资源管理器发送回滚消息, 否则向每个资源管理器发送Commit消息.相应的资源管理器根据事务管理器发送过来的消息指令,进行事务回滚或提交操作, 并释放事务处理过程中的锁资源.
存在的问题:
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消息并执行事务回滚操作的资源管理器的数据不一致.
最终一致性分布式事务不要求参与事务的各节点数据时刻保持一致,允许存在中间状态,只要一段时间后,能够达到数据的最终一致状态即可.
典型方案: TCC解决方案, 可靠消息最终一致性解决方案, 最大努力通知型解决方案.
优点: 性能比较高; 具备可用性; 适合高并发场景;
缺点: 数据存在短暂的不一致; 对事务一致性要求特别高的场景不太适用;
需要服务操作具有可标识性,具有全局的唯一标识,要有完整的操作时间信息,如果出现了错误,通过可查询的接口来获取其他服务处理的情况.
幂等性指对于同一个方法来说,只要参数相同,无论执行多少次都与第一次执行时产生的影响相同.
业务服务对外提供操作业务数据的接口,并且需要在接口的实现中保证对数据处理的幂等性.
在分布式环境中, 难免会出现数据不一致的情况, 为了保证数据的最终一致性,系统会进行重试,如果重试的接口不幂等,即使重试成功也无法保证数据一致.
实现幂等性的方式: 一是通过业务操作本身实现幂等性; 二是通过系统缓存所有的请求与处理结果,当再次检测到相同的请求时,直接返回之前缓存的处理结果.
包括三个阶段: Try阶段(尝试业务执行), Confirm阶段(确定业务执行)和Cancel阶段(取消业务执行).
Try阶段:
Confirm阶段:
Cancel阶段:
在分布式系统中,如果某些数据处于不正常的状态,需要通过某种方式进行业务补偿,使数据能够达到最终一致性.业务服务对外提供操作数据的接口时,也需要对外提供补偿业务的接口,当其他服务调用业务服务操作数据接口出现异常时,能够通过补偿接口进行业务补偿操作.
执行业务操作时,完成业务操作并返回结果,操作阶段对外部都是可见的.
进行业务补偿时,能够补偿或者抵消正向业务操作的结果,并且业务补偿操作需满足幂等性.
适用于具有强隔离性,严格一致性要求的业务场景,也适用于执行时间较短的业务.
需实现的服务模式: 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阶段.
事务的发起方执行完本地事务之后,发出一条消息,事务的参与方(消息消费者)一定能够接收到这条消息并处理成功.主要适用于消息数据能够独立存储,降低系统之间耦合度,且业务对数据一致性的时间敏感度高的场景.
需要实现的服务模式: 可查询操作和幂等操作.
事务发起方将消息发送给可靠消息服务,消息服务可基于本地数据表实现, 也可基于消息中间件实现.事务参与方从可靠消息服务中接收消息.事务发起方, 可靠消息服务, 事务参与方都是通过网络通信,由于网络的不稳定性,需要引入消息确认服务和消息恢复服务.
消息确认服务会定期检测事务发起方业务的执行状态和消息库中的数据,如果发现不一致,消息确认服务会同步事务发起方的业务数据,保证数据一致性,确保事务发起方业务完成本地事务消息一定发送成功.
消息恢复服务会定期检测事务参与方业务的执行状态和消息库中的数据,如果不一致,消息恢复服务会恢复消息库中消息的状态,回滚消息状态为事务发起方发送消息成功,但未被事务参与方消费的状态.
基于本地消息表实现
优点: 实现了消息的可靠性,减少对消息中间件的依赖.
缺点: 绑定了具体的业务场景,耦合性太高,不可公用和扩展; 消息数据和业务数据在同一个数据库,占用了业务系统的资源; 消息数据可能会收到数据库并发性的影响;
基于消息中间件实现
优点: 消息数据能够独立存储,与具体的业务数据库解耦; 消息的并发性和吞吐量优于本地消息表;
缺点: 发送一次消息需要完成两次网络交互,一次是消息的发送,另一次是消息的提交或回滚; 需要实现消息的回查接口,增加了开发成本.
事务发送方本地事务与消息发送的原子性问题: 要求事务发起方的本地事务与消息发送的操作具有原子性.通过消息确认服务解决.
事务参与方接收消息的可靠性问题: 由于服务器宕机,网络故障,导致事务参与方不能正常接收消息,或者接收消息后处理事务的过程中发生异常,无法将结果正常回传到消息库中.通过消息恢复服务解决.
事务参与方接收消息的幂等性问题: 可靠消息服务会多次向事务参与方发生消息,进行重试,如果事务参与方的方法不具备幂等性,会造成消息重复消费的问题.通过事务参与方的方法实现需具有幂等性解决.
适用于最终一致性时间敏感度低的场景,并且事务被动方的处理结果不会影响主动方的处理结果.分布式事务跨域多个不同的系统,尤其是不同企业之间的系统时.
需实现的服务模式: 可查询参数和幂等操作.
实现最大努力通知型方案需实现如下功能:
优点: 实现跨企业的数据一致性; 业务被动方的处理结果不会影响业务主动方的处理结果; 能够快速接入其他业务系统,达到业务数据的一致性;
缺点: 只适用于时间敏感度低的场景; 业务主动方的消息可能丢失,造成业务被动方收不到消息; 需要业务主动方提供查询消息的接口,增加了开发成本;
消息重复通知问题: 业务主动方按照一定的阶梯型通知规则通知业务被动方,存在消息重复通知问题.通过业务被动方接收消息通知的方法具有幂等性解决
消息通知丢失的问题: 业务主动方尽最大努力还没通知到业务被动方,或者业务被动方需再次获取消息,但消息通知已丢失.通过业务主动方提供查询消息的接口解决.
DTP模型主要定义了应用程序, 资源管理器, 事务管理器三个核心组件.
二阶段提交:
JTA规范是XA规范的Java版, 涉及到的接口有:
事务管理器提供者需实现除XAResource的接口,通过与XAResource接口交互来实现分布式事务.
资源管理器提供者需实现XAResource接口.
目前只有InnoDB存储引擎支持,在DTP模型中, MySQL属于资源管理器.
(花括号是必填项,方括号是可填项)
XA {START | BEGIN} xid [JOIN | RESUME]
: 开始XA事务,如果是START则不支持[JOIN | RESUME].XA END xid [SUSPEND [FOR MIGRATE]]
: 结束XA事务,如果开始事务是START则不支持[SUSPEND [FOR MIGRATE]]XA PREPARE xid
: 准备提交XA事务XA COMMIT xid [ONE PHASE]
: 提交XA事务XA ROLLBACK xid
: 回滚XA事务XA RECOVER [CONVERT XID]
: 列出所有处于Prepare阶段的XA事务注意:
各个事务分支的ACID特性保证了全局事务的ACID特性,可重复读隔离级别不足以保证分布式事务的一致性,需将事务隔离级别设置为串行化,但执行效率最低.
一旦协调者事务管理器发生故障,参与者资源管理器会一直阻塞下去.
在Commit阶段,当协调者向参与者发送commit请求后,发生了局部网络故障,或者协调者发生了故障,会导致一部分参与者接收到commit请求,其他参与者未接收到,造成数据不一致.
XA数据不一致问题:
单点故障问题:
在应用层将一个完整的事务操作分为Try Confirm Cancel三个阶段:
一个完整的TCC分布式事务包含: 主业务服务, 从业务服务,TCC管理器.
主业务服务是TCC分布式事务的发起方.
从业务服务提供TCC业务操作,必须实现TCC分布式事务Try, Confirm和Cancel阶段的三个接口,供主业务服务调用.
TCC管理器管理和控制整个事务活动,包括记录全局事务和分支事务的状态,在Try阶段执行成功时,自动调用每个分支事务的Confirm阶段的操作,分支事务执行失败时,则自动调用Cancel阶段的操作.
在电商业务中的支付订单场景,如果订单支付成功,则需修改订单状态,扣减库存(库存服务),增加积分(积分服务),创建出库单(仓储服务)等.
Try阶段执行成功,TCC分布式事务会执行Confirm阶段的业务逻辑.
如果Try阶段的业务执行失败,或者某个服务出现异常,TCC分布式事务框架会自动调用Cancel阶段的方法.
AOP切面: 通过AOP切面拦截具体的业务逻辑,在AOP切面中执行事务日志的记录,远程调用等逻辑.
反射技术: TCC分布式事务中Confirm阶段的方法和Cancel阶段的方法是通过反射技术调用的.
持久化技术: 由于网络的不稳定性,所有参与事务的服务都存在数据的持久化操作,保证数据的最终一致性.
序列化技术: 分布式环境中数据的持久化和在网络中的传输,都需要序列化技术的支持.
定时任务: 分布式环境,网络的不稳定,会出现方法调用失败的情况,需要利用定时任务重试.
动态代理: 通过动态代理的方式支持多种远程调用框架.
多配置源技术: 真正的业务场景中,会存在不同的配置存储技术.
事务发起方(消息发送者)执行本地事务成功后发出一条消息,事务参与方(消息消费者)接收到事务发起方发生过来的消息,并成功执行本地事务.
通过本地事务,将业务数据和消息数据分别保存到本地数据库的业务数据表和本地消息表中,然后通过定时任务读取本地消息表中的数据,将消息发送到消息中间件,等待消息消费者成功接收到消息后,再将本地消息表中的消息删除.
存放消息的本地消息表和存放数据的业务数据表位于同一个数据库中,这种设计能够保证使用本地事务达到消息和业务数据的一致性,并且引入消息中间件实现多个分支事务的最终一致性.
流程如下:
优点:
缺点:
独立消息服务在本地消息表的基础上,将消息服务独立出来,并将消息数据从本地消息表独立成单独消息数据库,引入消息确认服务和消息恢复服务.
具体流程如下:
优点:
缺点:
RocketMQ的事务消息主要为了让Producer端的本地事务与消息发送逻辑形成一个完整的原子操作.Producer端和Broker端具有双向通信能力,使得Broker端具备事务协调者的功能.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);
}
}
要保证消息发送的一致性,就要实现消息的发送和确认机制.整体流程:
实现消息接收的一致性需解决如下问题:
具体处理流程:
需要保证事务发起方的可靠性,事务发起方部署多份形成集群模式.
需要保证消息生产和发送的可靠性,引入回调确认机制: 消息服务通过回调获取事务执行状态和消息数据,确保消息一定被消息服务成功接收;消息服务收到消息后,会返回一个确认消息.如果事务发起方一定时间内没有收到确认消息,就会触发消息重试机制.消息服务也需实现幂等.消息重试需要实现响应时长判断和重试次数限制.
确保消息能够进行持久化,使用消息存储的多副本机制.
需要确保事务参与方成功消费消息,事务参与方部署多份形成集群模式.还要实现事务参与方的重试机制和幂等机制.
方案适用于如下特点的场景:
业务主动方主要分为业务处理服务,消息中间件,消息消费服务,消息通知服务.流程如下:
如果消息中间件宕机,业务处理服务会多次重试将消息发送给消息中间件,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.
如果消息消费服务宕机,消息中间件会多次重试将消息重新投递,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.
如果消息通知服务宕机,消息消费服务会多次重试调用消息通知服务的接口,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.
如果业务被动方宕机,消息通知服务会按照一定的规则向业务被动方阶梯式消息通知,直到达到最大次数.如果仍然失败,则业务被动方可通过查询接口恢复丢失的业务消息.