
事务的基本概念
ACID
事务类型
扁平事务
带保存点的扁平事务
链式事务
在带保存点的扁平事务的基础上,自动将当前事务的上下文隐式地传递给下一个事务。也就是说,一个事务的提交操作和下一个事务的开始操作具备原子性,上一个事务的处理结果对下一个事务是可见的,事务与事务之间就像链条一样传递下去。
嵌套事务
多个事务处于嵌套状态,共同完成一项任务的处理,真个任务具备完整性。
分布式事务
本地事务优缺点
优点
支持严格的 ACID
事务可靠
本地执行效率高
事务状态在数据库中维护,上层应用不需要感知
应用的编程模型比较简单,不涉及复杂网络通信
缺点
不具备分布式事务处理能力
一次事务过程中只能连接一个支持事务的数据库,既不能用于多个事务性数据库
MySQL 死锁
InnoDB 使用等待图(wait-for graph)的方式自动检测死锁,如果发现死锁,就会自动回滚一个事务。
如果发生死锁可以通过 show engine innodb status 来查看死锁的日志信息,或者通过配置 innodb_print_all_deadlocks 参数为 ON 将相关信息打到错误日志中
以下方式避免死锁
尽量让数据表中的数据检索都通过索引来完成,避免无效索引导致行锁升级为表锁
合理设计索引,尽量缩小锁的范围
尽量减少查询条件的范围,尽量避免间隙锁或缩小间隙锁的范围
尽量控制事务大小,减少一次事务锁定的资源数量,缩短锁定资源的时间
如果一条 SQL 语句涉及事务加锁操作,则尽量将其放在整个事务的最后执行
尽可能使用低级别的事务隔离机制
MySQL 事务的实现原理
Redo Log
保证事务的原子型和持久性。主要记录的是物理日志,也就是对磁盘上的数据进行的修改操作。
用来恢复提交后的物理数据页,不过只能恢复到最后一次提交的位置
通常包含两部分
日志缓冲(Redo Log Buffer);容易丢失
磁盘上的重要文件 Redo Log File;不容易丢失
原理
提交事务时将数据写入到 Redo Log Buffer,Buffer 中数据再根据一定规则写入 Redo Log 文件
重启时候通过 Redo Log 恢复
刷盘规则
示意图
开启事务,发出提交事务指令后是否刷新日志由变量 innodb_flush_log_at_trx_commit 决定。0:每次提交事务,不会将 Log Buffer 中的日志写入 OS Buffer 而是通过一个单独的线程,每秒写入 OS Buffer 并调用 fsync 函数写入 Redo Log,非实时。1(默认): 每次提交事务都会将 Log Buffer 中的日志写入 OS Buffer,并且会调用 fsync 函数将日志数据写入磁盘的 Redo Log 文件中。性能较差。2: 每次提交事务时,都只是将数据写入 OS Buffer,之后每隔 1s,通过 fsync 函数将 OS Buffer 中的日志数据同步写入磁盘的 Redo Log 文件中。
每秒刷新一次,刷新日志的频率由变量 innodb_flush_log_at_timeout 的值决定,默认是 1s。需要注意的是,刷新日志的频率和是否执行了 commit 操作无关
当 Log Buffer 中已经使用的内存超过一半时,也会触发刷盘操作
当事务中存在 checkpoint 时,在一定程度上代表了刷写到磁盘时日志所处的 LSN 的位置。其中,LSN (Log Sequence Number) 表示日志的逻辑序列号。
Redo Log 写入机制
文件内容是以顺序循环的方式写入的,写满一个开始下一个,最后一个满了之后循环到第一个,并且是覆盖写
LSN 机制
占用 8 字节存储空间,并且 LSN 的值是单调递增的。一般可以从 LSN 中获取如下信息
Redo Log 写入数据的总量
检查点位置
数据页版本相关信息
LSN 除了存在于 Redo Log 中,还存在于数据页中。每个数据页的头部,有一个 fil_page_lsn 参数记录着当前页最终 LSN 值。将数据页中的 LSN 值和 Redo Log 中的 LSN 值进行比较,如果数据页中的 LSN 值小于 Redo Log 中的 LSN 值,则表示丢失了一部分数据,此时,可以通过 Redo Log 的记录来回复数据,否则不需要恢复数据。
可以通过 show engine innodb status 查看LSN 的值
Log sequence number:当前内存缓冲区中的 Redo Log 的 LSN
Log flushed up to: 表示刷新到磁盘上的 Redo Log 文件中的 LSN
Pages flushed up to: 表示刷新到磁盘数据页上的 LSN
Last checkpoint at: 表示上一次检查点所在位置的 LSN
相关参数
show variables like ‘%innodb_log%’
innodb_log_buffer_size: 表示 log buffer 的大小,默认 8M
innodb_log_file_size: 表示事务日志的大小,默认 5M
innodb_log_files_group = 2:表示事务日志组中的事务日志文件个数,默认 2
innodb_log_group_home_dir=./:表示事务日志组所在的目录,当前目录表示 MySQL 数据所在的目录
Undo Log
实现事务的一致性
回滚事务
多版本并发事务(MVCC)
基本概念
启动事务前,将要修改的数据记录存储到 Undo Log 中,如果数据库的事务回滚或者崩溃,可以利用 Undo Log 对数据库中未提交的事务进行回滚
Undo Log 会在事务开始前产生,当事务提交时,并不会立即删除相应的 Undo Log。此时,InnoDB 存储引擎会将当前事务对应的 Undo Log 放入待删除的列表,接下来,通过一个后台线程 purge thread 进行删除处理。
Undo Log 记录的是逻辑日志
事务执行过程中产生的 Undo Log 也需要进行持久化操作,所以 Undo Log 也会产生 Redo Log。由于 Undo Log 的完整性和可靠性需要 Redo Log 来保证,印尘数据库崩溃时需要先做 Redo Log 数据恢复,然后做 Undo Log 回滚。
存储方式
采用段的方式进行管理,在 InnoDB 数据文件中存在一种叫做 rollback segment 的回滚段,这个回滚段内部有 1024 个 undo log segment 段。Undo Log 默认存放在共享数据表空间中,默认为 ibdata1 文件中。如果开启 innodb_file_per_table 参数,就会将 Undo Log 存放在每张数据表中的 .ibd 文件中。
实现 MMVC
insert undo log:事务对插入新记录产生的 Undo Log,只在事务回滚时需要,在事务提交后可以立即丢弃
update undo log:事务对记录进行删除和更新操作时产生的 Undo Log,不仅在事务回滚时需要,在一致性读时也需要,只有当数据库锁使用的快照不涉及该日志记录时,对应的回滚日志才会被 purge 线程删除
可重复读隔离级别下的 MMVC 实现
【SELECT】InnoDB 只会查找版本号小于或者等于当前事务版本号的数据行,这样可以保证事务读取的数据行要么之前就已经存在,要么是当前事务自身插入或者修改的记录。另外,行的删除版本号要么未定义,要么大于当前事务的版本号,这样可以保证事务读取的行在事务开始之前没有被删除。
【INSERT】将当前事务的版本号保存为当前行的创建版本号
【DELETE】将当前事务的版本号保存为删除的数据行的删除版本号,作为行删除标识
【UPDATE】将待修改的行复制为新的行,将当前事务的版本号保存为新数据行的创建版本号,同时保存当前事务的版本号为原来数据行的删除版本号
相关参数
show variable like “%undo%”
innodb_max_undo_log_size:Undo Log 空间最大值,超过该值(默认 1G)会触发 truncate 回收(收缩)操作。
innodb_undo_directory:Undo Log 的存储目录
innodb_undo_log_encrypt:8 新增参数,表示 Undo Log 是否加密,默认 OFF
innodb_undo_log_truncate:表示是否开启在线回收 Undo Log 文件操作,支持动态设置,ON / OFF,默认 OFF
innodb_undo_tablespaces:此参数必须大于或等于 2,即回收一个 Undo Log 时,要保证另一个 Undo Log 是可用的
innodb_undo_logs:表示 Undo Log 的回滚段数量,此参数的值至少大于或等于 35,默认 128
innodb_purge_rseg_truncate_frequency:回收 Undo Log 的频率。
BinLog
记录结构变更与数据变更的二进制操作
三种模式
Row:清晰记录每一行数据被修改的情况,能完全实现主从同步,缺点是日志量可能过大
Statement:记录 SQL。缺点是某些情况下比如 last_insert_id / now 等函数会导致复制数据不一致
Mixed:Row 与 Statement 混用,如果存在 Statement 无法复制的操作使用 Row 替代
写入机制
Commit 才会写入
组提交机制
调用一次 fsync 将多个事务的日志刷新到磁盘的日志文件中
5.6 的二进制日志组提交(Binary Log Group Commit,BLGC)
Flush 阶段:将每个事务的 BinLog 写入对应的内存缓冲区
Sync 阶段:将内存缓冲区中的 BinLog 写入磁盘文件,如果队列中存在多个事务,则此时只执行一次刷盘操作就可以将多个事务的 BinLog 刷新到磁盘文件中
Commit 阶段:leader 事务根据队列中事务的顺序调用存储引擎层事务的提交操作,由于 InnoDB 本身支持组提交功能,因此解决了 prepare_commit_mutex 锁导致的组提交功能失效的问题
与 Redo 日志差别
BinLog MySQL 本身就有,Redo 只有 InnoDB 有
BinLog 是逻辑日志,Redo Log 是物理日志,记录的是每个数据页的修改
Redo Log 具有幂等性,多次操作的前后状态是一致的,BinLog 不具有幂等性,记录的是所有影响数据库的操作,比如插入之后再删除,Redo Log 前后不会发送变化,而 BinLog 会记录插入操作和删除操作
BinLog 开启事务时,会将每次提交的事务一次性写入内存缓冲区,如果未开启事务,则每次成功执行插入、更新和删除语句时,就会将对应的事务信息写入内存缓冲区,而 Redo Log 是在数据准备修改之前将数据写入缓冲区的 Redo Log 中,然后在缓冲区中修改数据。而且在提交事务时,先将 Redo Log 写入缓冲区,写入完成后再提交事务。
BinLog 只会在事务提交时,一次性写入 BinLog,其日志的记录方式与事务的提交顺序有关,并且一个事务的 BinLog 中间不会插入其他事务的 BinLog。而 Redo Log 记录的是物理页的修改,最后一个提交的事务记录会覆盖之前所有未提交的事务记录,并且一个事务的 Redo Log 中间会插入其他事务的 Redo Log
BinLog 是追加写入,写完一个日志文件再写下一个日志文件,不会覆盖使用,而 Redo Log 是循环写入,日志空间的大小是固定的,会覆盖使用
BinLog 一般用于主从复制和数据恢复,不具备崩溃时自动恢复的能力,而 Redo Log 是在服务器发送故障后重启 MySQL,用于恢复事务已提交但未写入数据表的数据
BinLog 相关参数
show variables like “%log_bin%“show variables like “%binlog%”
log_bin:开启 binlog 与否
log_bin_index:指定二进制索引文件的路径和名称
binlog_do_db:只记录指定数据库的二进制日志
binlog_ignore_db:不记录指定数据库的二进制日志
max_binlog_size:最大值,默认 1GB
sync_binlog:影响 MySQL 的性能和数据完整性。取值 0: 将 binlog_cache 写入文件的同事,不会执行 fsync 刷盘取值大于 0 的数字 N:在进行 N 次事务提交操作后,执行一次 fsync 刷盘
max_binlog_cache_size:BinLog 占用最大内存
binlog_cache_size:BinLog 使用内存大小
binlog_cache_use:BinLog 缓存的事务数量
binlog_cache_disk_use:使用 BinLog 缓存但超过 binlog_cache_size 的值,并且使用临时文件来保存 SQL 语句中的事务数量
MySQL 事务的流程
执行流程
恢复流程
MySQL 中的 XA 事务
XA 事务支持不同数据库之间实现分布式事务
本质是一种基于二阶段提交的分布式事务,在使用 XA 分布式事务时,InnoDB 存储引擎的事务隔离级别需要设置为串行化
事务模型
事务模型【飞书的图不能缩进,有问题】
事务管理器:主要对参与全局事务的各个分支事务进行协调,并与资源管理器进行通信
资源管理器:主要提供对事务资源的访问能力。数据库即资源管理器
应用程序:明确全局事务和各个分支事务,指定全局事务中的各个操作
两阶段
Prepare:事务管理器向资源管理器发送准备指令,资源管理器接到之后,执行数据的修改操作并记录相关的日志信息,然后向事务管理器返回可以提交或者不可用提交的结果信息
Commit:事务管理器接受所有资源管理器返回的结果信息,如果某个返回不可提交或者超时,事务管理器想所有资源管理器发送回滚指令。如果都可以提交,则事务管理器向所有资源管理器发送提交事务指令。
分布式事务理论知识
CAP: 一致性(Consistency)可用性(Availablity)和分区容忍性(Partition Tolerance)三选二
Base 是 AP 中的扩展,牺牲强一致性来获取可用性。基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent) 缩写
强一致性分布式事务解决方案
DTP
主要定义了实现分布式事务的规范和 API
重要概念
事务:完整工作单元,满足 ACID
全局事务:由事务管理器管理的事务,能一次性操作多个资源管理器
分支事务:有事务管理器管理的全局事务中,每个资源管理器中独立执行的事务
控制线程:执行全局事务的线程,这个线程用来关联应用程序、事务管理器和资源管理器三者之间的关系,也就是表示全局事务和分支事务的关系,通常称为事务上下文环境。
示意图
2PC
问题
1. 同步阻塞问题:事务的执行过程中,所有参与事务的节点都会对其占用的公共资源加锁,导致其他访问公共资源的进程或线程阻塞
2. 单点故障问题:如果事务管理器发生故障,则资源管理器会一直阻塞
3. 数据不一致问题:如果在 Commit 阶段,由于网络或者部分资源管理器发生故障,导致部分资源管理器没有接收到事务管理器发送过来的 Commit 消息,会引起数据不一致的问题
4. 无法解决的问题:如果在 Commit 阶段,事务管理器发出 Commit 消息后宕机,并且唯一接收到这条 Commit 消息的资源管理器也宕机,则无法确认事务是否已经提交。
3PC
将 2PC 的 Prepare 一分为二。最终三个阶段 CanCommit、PreCommit 和 doCommit / doRollback
解决 2PC 的单点问题,并减少事务执行过程中产生的阻塞现象。如果资源管理器无法及时收到来自事务管理器发出的消息,那么资源管理器就会执行提交事务的操作,而不是一直持有事务的资源并处于阻塞状态,但是会导致数据不一致的问题。
最终一致性分布式事务解决方案
服务模式
最终一致性分布式事务解决方案存在 4 种典型的服务模式
可查询操作:服务的操作具有全局唯一标识
幂等操作
TCC 操作:Try -> Confirm / Cancel
可补充操作:如果某些数据处于不正常状态,需要通过某种方式进行业务补偿,使数据能够达到最终一致性
TCC
解决跨服务调用场景下的分布式事务问题
以电商下单减库存为例
Try阶段:提交订单并将订单的状态设置为待提交,调用库存服务预扣减库存。数据中商品库存数量减少同时预扣减库存增加。
Confirm 阶段:如果成功则执行,订单状态更改为已提交,库存数据表中预扣减库存字段 - 商品数量。实现真正减库存。
Cancel 阶段:如果执行失败。更改订单状态,库存表中商品库存字段的数据增加提交订单时传递的商品数量,预扣库存字段减去商品数量。
需要实现的服务操作:TCC 操作、幂等操作、可补充操作、可查询操作
优点
1. 在应用层实现具体逻辑,锁定资源粒度变小,不会锁定所有资源,提升了系统性能
2. Confirm 与 Cancel 具有幂等性,保证执行结束后数据一致
3. 由主业务发起整个事务,无论主事务还是分支事务所在的业务,都能部署未集群模式,从而解决了 XA 规范的单点故障问题
缺点
每个参与分布式事务的业务方法都要拆分成 Try、Confirm 和 Cancel 三个阶段,提高了开发成本
问题
空回滚问题
原因:一个分支事务所在的服务器宕机或者网络发生异常,此分支事务调用失败,此时并未执行分布式事务 Try 阶段方法。当服务器或网络恢复,TCC 执行回滚操作,会调用 Cancel,如果没有处理这类情况,就会出现空回滚问题。
解决方案:使用全局事务 ID,在执行 Try 阶段方法做记录,Cancel 幂等处理
幂等问题
原因:宕机或网络异常,方法出现超时可能会出现重试机制,需要保证重试是幂等的
全局事务记录表中增加事务执行状态,每次执行分支事务及 Confirm 和 Cancel 时都查询状态判断事务幂等性
悬挂问题
原因:预留业务资源后,无法继续往下处理。在 TCC 分布式事务中,通过 RPC 调用分支事务 Try 阶段的方法时,会先注册分支事务,再执行 RPC 调用。如果此时发生服务器宕机、应用崩溃或网络异常 RPC 超时。此时事务管理器会通知对应的资源管理器回滚事务,可能资源管理器回滚完事务后,RPC 请求达到了参与分支事务所在的业务方法,因为此时事务已经回滚,所以在 Try 阶段预留的资源就无法释放了。
方案:如果执行了 Confirm 或 Cancel 则 Try 方法就不能再执行
可靠消息
主要适用于消息数据能够独立存储,能够降低系统之间偶尔度,并且业务对数据一致性的时间敏感度高的场景。比如基于 RocketMQ Half 消息的实现。
执行流程
执行流程【飞书的图不能缩进有问题】
事务发起方将消息发送给可靠消息服务,这里的可靠消息可以基于本地数据表实现,也可以基于 MQ 实现。
事务参与方从可靠消息服务中接收消息,考虑网络原因需要引入消息确认服务和消息恢复服务
消息确认服务会定时检测事务发起方业务的执行状态和消息库中的数据,如果发现事务发起方业务的执行状态与消息库中的数据不一致,消息确认服务就会同步事务发起方的业务数据和消息库中的状态,保证数据一致性,确保事务发起方业务完成本地事务后消息一定会发送成功
消息恢复服务会定期检测事务参与方业务的执行状态和消息库中的数据,如果发现事务参与方业务的执行状态与消息库中的数据不一致(这里的不一致,通常是指本地事务执行失败),消息恢复服务就会恢复消息库中消息的状态,使此消息的状态回滚为事务发起方发送消息成功,但未被事务参与方消费的状态。
问题
事务发送方本地事务与消息发送的原子型问题
要求事务发送方的本地事务与消息发送的操作具有原子性
可以通过消息确认服务解决
事务参与方接收消息的可靠性问题
事务参与方无法接收消息或无处理过程中异常,无法将结果回传到消息库中
通过消息恢复服务保证事务参与方的可靠性
事务参与方接收消息的幂等性问题
可靠消息可能会多次向事务参与方发送消息
需要参与方具有幂等性
最大努力通知
适用于最终一致性时间敏感度低的场景,并且事务被动方的处理结果不会影响主动方的处理结果。最典型的使用场景就是支付成功后,支付平台异步通知商户支付结果
需要实现的服务模型:可查询操作和幂等操作
流程
业务主动方在完成业务处理后,会向业务被动方发送消息通知。发送消息通知时允许消息丢失
在实现上,业务主动方可以设置时间阶梯型通知规则,在消息通知失败后,可以按照规则再次通知,直到到达最大通知次数为止
业务主动方需要提供查询接口供业务被动方按照需要 查询,用于恢复丢失的消息







