事务基础概念图

原始笔记链接

事务的基本概念

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 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 事务执行流程

恢复流程

MySQL 事务恢复流程

MySQL 中的 XA 事务

XA 事务支持不同数据库之间实现分布式事务

本质是一种基于二阶段提交的分布式事务,在使用 XA 分布式事务时,InnoDB 存储引擎的事务隔离级别需要设置为串行化

事务模型

XA 事务模型

事务模型【飞书的图不能缩进,有问题】

事务管理器:主要对参与全局事务的各个分支事务进行协调,并与资源管理器进行通信

资源管理器:主要提供对事务资源的访问能力。数据库即资源管理器

应用程序:明确全局事务和各个分支事务,指定全局事务中的各个操作

两阶段

Prepare:事务管理器向资源管理器发送准备指令,资源管理器接到之后,执行数据的修改操作并记录相关的日志信息,然后向事务管理器返回可以提交或者不可用提交的结果信息

Commit:事务管理器接受所有资源管理器返回的结果信息,如果某个返回不可提交或者超时,事务管理器想所有资源管理器发送回滚指令。如果都可以提交,则事务管理器向所有资源管理器发送提交事务指令。

分布式事务理论知识

CAP: 一致性(Consistency)可用性(Availablity)和分区容忍性(Partition Tolerance)三选二

Base 是 AP 中的扩展,牺牲强一致性来获取可用性。基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent) 缩写

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

DTP

主要定义了实现分布式事务的规范和 API

重要概念

事务:完整工作单元,满足 ACID

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

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

控制线程:执行全局事务的线程,这个线程用来关联应用程序、事务管理器和资源管理器三者之间的关系,也就是表示全局事务和分支事务的关系,通常称为事务上下文环境。

示意图

DTP 分布式事务交互流程

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 实现。

事务参与方从可靠消息服务中接收消息,考虑网络原因需要引入消息确认服务和消息恢复服务

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

消息恢复服务会定期检测事务参与方业务的执行状态和消息库中的数据,如果发现事务参与方业务的执行状态与消息库中的数据不一致(这里的不一致,通常是指本地事务执行失败),消息恢复服务就会恢复消息库中消息的状态,使此消息的状态回滚为事务发起方发送消息成功,但未被事务参与方消费的状态。

问题

事务发送方本地事务与消息发送的原子型问题

要求事务发送方的本地事务与消息发送的操作具有原子性

可以通过消息确认服务解决

事务参与方接收消息的可靠性问题

事务参与方无法接收消息或无处理过程中异常,无法将结果回传到消息库中

通过消息恢复服务保证事务参与方的可靠性

事务参与方接收消息的幂等性问题

可靠消息可能会多次向事务参与方发送消息

需要参与方具有幂等性

最大努力通知

适用于最终一致性时间敏感度低的场景,并且事务被动方的处理结果不会影响主动方的处理结果。最典型的使用场景就是支付成功后,支付平台异步通知商户支付结果

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

流程

最大努力通知流程

业务主动方在完成业务处理后,会向业务被动方发送消息通知。发送消息通知时允许消息丢失

在实现上,业务主动方可以设置时间阶梯型通知规则,在消息通知失败后,可以按照规则再次通知,直到到达最大通知次数为止

业务主动方需要提供查询接口供业务被动方按照需要 查询,用于恢复丢失的消息