MySQL 事务是比较重要且核心的一部分,在操作数据库 DML 语句时,以及开源框架基于 MySQL 进行事务操作时,保持事务的 ACID 特性是数据可靠的一大保障
原子性(Atomicity)
一个事务必须被视为不可分割的最小单元,事务的所有操作要么全部提交成功、要么全部提交失败,对于一整个事务来说,不能只执行其中的一部分操作,例如:A 转账给 B,A 余额必须减少,B 余额必须增加
一致性(Consistency)
事务从一种状态扭转到另外一种状态,在事务开始前、结束后,数据的完整性没有被破坏
例如:A、B 事务操作前的总额是多少,转账后,事务操作后的总额也应该是一样的
隔离性(Isolation)
事务的执行不能被其他事务执行所干扰,即一个事务的执行应该与其他并发执行的事务是相互隔离的
持久性(Durability)
一旦事务被提交,对其所做的任何信息变更,都应该永久持久化到数据库中,即使系统瘫痪,已提交的数据也不会丢失
MySQL 基于客户端 / 服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接之后,就可以称之为一个会话 Session;每个客户端都可以在开启的会话中向 MySQL 服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说,可以同时处理多个事务
MySQL 四大事务特性中 > 隔离性,理论上在某个事务在对数据进行访问或 DML 操作时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问此数据,这样的话,由并发事务执行就转变为了串行化执行;串行化执行方式对性能影响太大,既想保持事务的隔离性,又想让服务器在处理同一数据 > 多事务时
性能尽量高些,从而会为我们带来以下数据问题:脏读、不可重复读、幻读.
当一个事务读取到了另外一个事务修改但未提交的数据,称之为脏读
如上图,在事务 A 执行过程中,事务 A 对数据资源进行了修改,事务 B 读取到了事务 A 修改后的数据;可能由于某些原因,导致事务 A 没完成提交,发生了 Rollback 操作,则事务 B 读取到的数据就是脏数据
这种读取到另外一个事务未提交的数据的现象称为脏读(DD)
当在一个事务内的记录被检索过两次,若两次得到的结果不同,此现象称为不可重复读
事务 B 读取了两次数据,在第一次读取完准备读第二次期间,事务 A 修改了数据,导致事务 B 在第二次读出来的数据与第一次是不一致的.
在事务执行过程中,另外一个事务新增或删除了记录,正在读取记录的事务,会发生幻读.
事务 B 在前后两次读取同一个范围内的数据,在第一次读取完准备读第二次期间,事务 A 新增或删除了数据,导致事务 B 后一次读取到前一次未统计到的行数
幻读、不可重复读有些类似,但幻读重点强调了
读取到了之前没有获取到的记录
SQL 标准中规定了四种隔离级别:未提交读、已提交读、可重复读、可串行化读,但不同数据库厂商对 SQL 标准规定的四种隔离级别支持不一样,比如:Oracle 只支持已提交读、可串行化读两种隔离级别,MySQL 同 SQL 标准一样支持四种隔离级别,但与其不同的是,MySQL 在可重复读隔离级别下,是一大程度下是可以避免幻读问题发生的.
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 READ UNCOMMITTED |
可能 | 可能 | 可能 |
已提交读 READ COMMITTED |
— | 可能 | 可能 |
可重复读 REPEATABLE READ |
— | — | SQL 标准可能 MySQL 少数场景会发生 |
可串行化 SERIALIZABLE |
— | — | — |
MySQL 默认隔离级别:REPEATABLE READ,可以手动修改事务的隔离级别
通过下面的语句可以更改事务的隔离级别:
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL level;
level 可选值有四个:REPEATABLE READ、READ COMMITTED、READ UNCOMMITTED、SERIALIZABLE
设置事务的隔离级别语句中,在 SET 关键字后面可以放置 GLOBAL 关键字、SESSION 关键字,这样会对不同范围的事务产生不同的影响,具体如下:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
对执行完该语句之后产生的会话起作用,当前已经存在的会话无效
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
对当前会话的所有后续事务生效,该语句可以在已经开启的事务中执行,但不会影响当前正在执行的事务
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
对当前会话中下一个即将开启的事务有效,下一个事务执行完以后,将会恢复到默认的隔离级别,该语句不能在已经开启事务中间执行,会报错 Transaction characteristics can't be changed while a transaction is in progress
transaction-isolation
值,比如:在启动服务器时指定了 --transaction-isolation=SERIABLIZABLE
,那么事务的默认隔离级别就会从原来的 REPEATABLE READ -> SERIABLIZABLE
查看当前会话默认隔离级别可以通过查看系统变量 transaction_isolation
值或 SELECT @@transaction_isolation
来确定
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
注意:transaction_isolation 系统变量是在MySQL 5.7.20 版本中引入来替换
tx_isolation
的,若你使用的还是之前版本,请将上述用到系统变量 transaction_isolation 地方替换为 tx_isolation
事务开始:begin、start transaction(推荐)、begin work
事务回滚:rollback
事务提交:commit
事务保存点:savepoint
回滚保存点:ROLLBACK TO [SAVEPOINT] 保存点名称;
在我们进行事务操作时,操作了不同的 DML 语句,可以基于不同的语句作 savepoint
比如:插入 A 数据,执行 savepoint a、更新 B 数据,执行 savepoint b、删除 C 数据,执行 savepoint c
此时,若想取消删除 C 数据这个步骤,可以执行:ROLLBACK TO c;
或RELEASE SAVEPOINT c;
隐式提交:是否开启隐式提交 > 取决于 autocommit ON 开或 OFF 关
当使用 START TRANSACTION 或 BEGIN 语句开启了一个事务,或者把系统变量 autocommit 值设置为 OFF 时,事务就不会进行自动提交,但是如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了 COMMIT 语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交
会导致事务隐式提交的语句包括,如下:
执行 DDL 语句 > create、alter、drop,当执行这些语句时,就会隐式提交前面 SQL 语句所属的事务.
更新 MySQL 数据库表信息:使用 ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD 等语句时也会隐式的提交前边语句所属于的事务
事务控制或关于锁定的语句:在一个会话中,一个事务还未提交或回滚,又使用 START TRANSACTION 或 BEGIN 语句开启了一个事务,会隐式提交上一个事务;或者使用了 LOCK TABLES、UNLOCK TABLES 等关于锁定的语句也会隐式提交前面语句所属的事务
加载数据语句:使用 LOAD DATA 语句来批量往数据库导入数据时,也会隐式提交前面语句所属的事务
其他语句:START SLAVE、STOP SLAVE、RESET SLAVE、ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH 等语句也会隐式提交前面语句所属的事务
MVCC 全称 Multi-Version Concurrency Control,多版本并发控制,主要是为了提高数据库的并发性能
平时,在同一行数据上同时发生读写请求时,会上锁阻塞住,但 MVCC 提供更好的方式去处理读-写请求,可以做到在发生读-写请求冲突时不用加锁
这个读是快照读,不是当前读,当前读是一种加锁的操作,是悲观锁 > FOR UPDATE
前面在介绍 隔离级别
时,说到了 MySQL 在 REPEATABLE READ
隔离级别下,可以很大程度上避免幻读问题的发生,从以下几个概念及实操来说明是如何去避免的
当对一条数据并发执行多次操作时,对该条数据会形成版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中包含了两个必要的隐藏列,不包括 row_id
row_id 并不是必要的,在创建的表中有主键或非 NULL 的 UNIQUE 键时,row_id 就是主键或唯一键,若没有主键或唯一键,会默认生成 row_id
必要的两个隐藏列,如下:
溯源
该记录修改前的信息undo 日志:为了实现事务的原子性操作,InnoDB 存储引擎在实际进行 DML 操作记录时,都先要把对应的 undo 日志记下来。
一般对一条记录进行一次改动,就对应一条 undo 日志
,但在某些更新操作中,也可能会对应两条 undo 日志;一个事务在执行过程中可能会新增、删除、更新若干条记录,也就是说会记录很多条 undo 日志,这些 undo 日志会从 0 开始编号,依此按顺序生成:1、2、…、N,此编号被称之为undo no
undo 日志是 MySQL 三大日志其中之一,还包含了 redo log、bin log,日志这方面的内容后续文章再详细分析
为了说明【MySQL 在 REPEATABLE READ 隔离级别下,可以很大程度上避免幻读问题的发生】此问题,创建一张演示表,如下:
create table technology_column(
id BIGINT(10) not null primary key auto_increment COMMENT '主键',
category_name varchar(30) not null COMMENT '专栏名称'
) Engine=InnoDB CHARSET=utf8 COMMENT '技术专栏表';
往这张表中插入一条数据,如下:
INSERT INTO technology_column VALUES(1, 'Spring');
假设插入该条数据的事务 id > trx_id = 80,那么此条记录的示意图如下:
假设之后有两个事务:trx_id 分别为 100、120,对这条记录进行了 UPDATE 操作,操作流程如下:
对记录每次进行改动,都会记录一条 undo 日志,每条 undo 日志都有一个 roll_pointer
属性
(INSERT INTO 操作对应的 undo 日志没有该属性,因为该记录没有更早的版本)可以将这些 undo 日志连起来,串成一个链表,如上图右侧所示~
对 INSERT INTO 操作后的记录,每次更新后,都会将旧值放到一条 undo 日志中,就当是该记录的一个旧版本,
便于作事务回滚、数据溯源
,随着更新的次数增多,所有的版本都会被 roll_pointer 属性连接成一个链表,将这个链表称为版本链
,版本链的头节点就是当前记录最新的值;另外,每个版本中还包含了生成该版本对应的事务 id
基于此,利用该记录的版本链来控制并发事务同时访问该记录的行为,那么这种机制就称之为多版本并发控制 MVCC
读取视图 > 作用于 SQL 查询语句
对于使用 READ UNCOMMITTED 读未提交隔离级别的事务来说,由于可以
读取到未提交的事务修改过的信息,所以直接读取记录的最新版本即可,由此读未提交就会出现脏读、不可重复读、幻读
对于使用 SERIALIZABLE 可串行化读隔离级别的事务来说,InnoDB 采用加锁的方式来访问记录,
当事务正在执行时,其他事务就会阻塞住直到前面的事务提交或回滚后才会执行,所以不会出现脏读、不可重复读、幻读
引入版本链的机制主要是为了解决:已提交读、可重复读的事务隔离级别
对于使用 READ COMMITTED、REPEATABLE READ 隔离级别事务来说,都必须保证读取到的数据是已经事务已提交修改过的记录,也就是说:假如事务已经修改了记录但尚未提交,是不能直接读取到最新版本记录的
核心问题:READ COMMITTED、REPEATABLE READ 隔离级别在不可重复读、幻读上的区别是从何而来,基于前面所介绍的版本链,主要关键是需要判断这两种级别在版本链中哪个版本是当前事务可见的
,为此 InnoDB 提出了 ReadView 概念
ReadView 主要包含了四个比较重要的内容,如下:
max_trx_id 并不是 m_ids 集合中的最大值,事务 id 是递增分配的;比如:现在有 id > 1、2、3 三个活跃事务,之后 id=3 的事务提交了,那么新的读事务在生成 ReadView 时, m_ids 集合中还有 1、2,min_trx_id 值就为 1,max_trx_id 值就为 4
下面来具体介绍,READ COMMITTED、REPEATABLE READ 隔离级别是如何分别处理脏读、不可重复读、幻读问题的.
在 MySQL 中,READ COMMITTED、REPEATABLE READ 隔离级别非常大的一个区别就是它们生成 ReadView 的时机不同
READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个 ReadView
以上面的 technology_column
表为例,现在只有一条事务 id 为 80 插入的一条记录
比如:现有系统中有两个事务 > Trx_id 100、120 在执行,事务 > Trx_id 100、120 SQL 语句如下:
Trx_id-100、120 Begin;
Trx_id-100、120:select * from technology_column where id = 1;
Trx_id-100:update technology_column set category_name ='MySQL' where id = 1;
Trx_id-100:update technology_column set category_name ='Redis' where id = 1;
Trx_id-100:Commit;
Trx_id-120:update technology_column set category_name ='分布式' where id = 1;
Trx_id-120:update technology_column set category_name ='Linux' where id = 1;
Trx_id-120:Commit;
在 Trx_id-100:Commit;
语句执行之前,technology_column.id =1 记录得到的版本链表,如下所示:
# 查询语句
select * from technology_column where id = 1;
以上使用 READ COMMITTED 隔离级别的事务,Trx_id 100、120 的事务均未提交,所以此时查询的数据仍然为 Spring!在 Trx_id-100:Commit;
语句执行之前,整个的执行过程如下:
trx_id 属性值在 ReadView 中 min_trx_id、max_trx_id 之间,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;若不在 min_trx_id、max_trx_id 之间,说明创建 ReadView 时生成该版本的事务已经被提交,该版本才可以被访问
所以有这种机制存在,就不会发生脏读问题!因为会去判断活跃的事务版本,必须是不在活跃中的事务版本才能使用,也就不可能读到没有 commit 提交的记录
基于上面的操作,先将 trx_id = 100 事务提交,然后再到 trx_id =120 事务更新表中 technology_column.id 为 1 的记录,也就是执行如下 SQL 语句:
Trx_id-100:Commit;
Trx_id-120:update technology_column set category_name ='分布式' where id = 1;
Trx_id-120:update technology_column set category_name ='Linux' where id = 1;
此时,technology_column 表中 id=1 记录的版本链如下图所示:
接着使用 READ COMMITTED 隔离级别事务,在 Trx_id-120 begin;
语句后查询的数据为 Spring
,而在 Trx_id-100:Commit;
语句执行之后,Trx_id-120:Commit;
语句执行之前,使用 Trx_id 为 120 的事务,查询 id=1 记录,此时查询到的数据为 Redis,整个的执行过程如下:
Trx_id 为 100 的事务已经提交了,所以再次生成 ReadView 快照时就没有它了
READ COMMITTED 读已提交隔离级别事务避免不了此问题的发生
依此类推,若之后 Trx_id 为 120 的事务也提交了,再次使用 READ COMMITTED 隔离级别事务,查询 technology_column.id 为 1 的记录时,得到的结果就是 Linux,具体流程类似于上面
通过 m_ids(活跃事务集合)、min_trx_id(最小事务 id) 结合隔离级别的特性,来比对版本链的记录是否符合可见性要求,而读已提交是在每次执行 SELECT 语句时都会生成 ReadView 视图快照.
REPEATABLE READ 隔离级别的事务只有在第一次读取数据时才会生成一个 ReadView,之后的查询就不会重复生成了
比如:现有系统中有两个事务 > Trx_id 100、120 在执行,事务 > Trx_id 100、120 SQL 语句如下:
Trx_id-100、120 Begin;
Trx_id-100、120:select * from technology_column where id = 1;
Trx_id-100:update technology_column set category_name ='MySQL' where id = 1;
Trx_id-100:update technology_column set category_name ='Redis' where id = 1;
Trx_id-100:select * from technology_column where id = 1;
Trx_id-100:Commit;
Trx_id-120:update technology_column set category_name ='分布式' where id = 1;
Trx_id-120:update technology_column set category_name ='Linux' where id = 1;
Trx_id-120:select * from technology_column where id = 1;
Trx_id-120:Commit;
在事务 100、120 执行前都会先生成 ReadView 读取视图,也就是它们读取到的 category_name 内容都是 Spring,在前面介绍过,READ COMMITTED 读已提交隔离级别会在 Trx_id 为 120 的事务执行期间发生不可重复读问题
,所以在这里主要分析 REPEATABLE READ 可重复读隔离级别是如何解决此问题的, 它在执行事务时对应的版本链表如下:
Trx_id-120
在 Trx_id 为 120 第一次 SELECT 语句执行以后
,生成了 ReadView 读取视图快照
ReadView 内容:m_ids 活跃事务集合为 100、120,min_trx_id 为 121,creator_trx_id 为 0
因为当前事务隔离级别为 REPEATABLE READ,所以在 Trx_id 为 120 的事务执行期间会直接复用上面所生成的 ReadView 快照信息,也就是前后两次 SELECT 查询语句执行后的结果都是一致的,读取到的 category_name 内容都是 Spring,这就是可重复读解决不可重复读问题的核心所在
因为它在事务执行期间,一直都是使用的第一次生成的 ReadView,自然而然也不会发生脏读问题,不会读取到其他事务已提交的数据信息
总结一下 ReadView 比较规则,如下:
REPEATABLE READ 隔离级别在 MVCC 机制下可以解决不可重复读问题,也可以在一定程度下解决幻读问题
幻读问题:一个事务在某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自于另外一个事务添加的新记录
比如:在 REPEATABLE READ 隔离级别下,事务 T1 先通过某个搜索条件读取到多条记录,然后事务 T2 插入一条符合这个搜索条件的记录并提交完成,此时 T1 再次通过这个搜索条件执行查询,结果如下:无论事务 T2 比 事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交信息的,因为在 REPEATABLE READ 隔离级别下,只会在第一次查询时才生成 ReadView 读取视图快照,后续的查询会延续使用第一次生成的 ReadView 信息,由此可见,REPEATABLE READ 可以在一定程序下解决幻读问题的发生
但在某些情况下,REPEATABLE READ 隔离级别,InnoDB MVCC 会发生幻读问题,结合案例如下:
首先在事务 T1 中执行查询语句,如下:
select * from technology_column where id between 2 and 3;
此时 technology_column 表中只有一条数据,id 为 2、3 的数据并未存在;接着我们在事务 T2 中执行 INSERT 语句,如下图:
通过以上语句,在 technology_column 表中插入了一条 id 为 2 的数据,此时,再回到事务 T1 执行如下语句:
update technology_column set category_name = 'RocketMQ' where id = 2;
select * from technology_column where id between 2 and 3;
执行结果如下:
很明显事务 T1 出现了幻读现象,在 REPEATABLE READ 隔离级别下,事务 T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView(但此时版本链表中没有生成的对应的条目)之后 T2 事务向表中新插入一条记录并提交,然后 T1 事务执行了 Update 语句;ReadView 并不能阻止事务 T1 执行 UPDATE 或 DELETE 语句来改动这个新插入的记录
,但这样一来,这条新记录的 Trx_id 隐藏列的值就变成了事务 T1 的事务 id
之后事务 T1 再次使用普通 SELECT 语句去查询这条记录时就可以看到了,也可以把这条记录返回给客户端,因为这种特殊现象的存在,所以 REPEATABLE READ 可重复隔离级别不能完全避免幻读问题的发生
事务 T1 第一次读是空的情况,事务 T2 新增了这条数据,事务 T1 在自己事务中对这条数据进行了修改
所谓的 MVCC(Multi-Version Concurrency Control)多版本并发控制,指的就是在使用 READ COMMITTED、REPEATABLE READ 这两种隔离级别事务时,执行普通 SELECT 操作时访问数据的版本链过程,这样子可以使不同的事务读写、写读操作并发执行,从而提高系统的性能
READ COMMITTED、REPEATABLE READ 这两种隔离级别事务一个最大不同:生成 ReadView 时机不同,READ COMMITTED 在每次进行普通 SELECT 操作时都会生成一个 ReadView,而 REPEATABLE READ 只有在第一次进行普通 SELECT 操作时生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 即可
,从而基本上可以避免幻读问题的发生,但如果第一次读 ReadView 是空数据的情况下 > 某些场景则无法避免幻读的发生
最后,所谓的 MVCC 只是在我们进行普通 SELECT 查询时才生效,对于锁定读就是不普通的查询,所以它就无法让我们的 MVCC 机制发挥作用了.
该篇博文,简而言之说明了 MySQL 中事务四大特性,详细阐述了由于四种隔离级别下各自会产生什么样的问题,从实战方面进行演示了 MySQL 基于事务上的一些基本操作,最重要的是讲述了 InnoDB 引擎下的 MVCC 机制,有提及它的版本链、ReadView 以及其下一些比较重要的概念,最后,重点说明了 READ COMMITTED 读已提交、REPEATABLE READ 可重复读,这两种隔离级别会是如何解决一些并发问题以及会在什么样的场景下发生一些异常问题。希望您能够喜欢,能帮助到你是我最大的快乐!
另外,博主有专门讲解 Spring 事务是如何结合 MySQL 一起应用的,博文链接如下:
Spring 事务传播机制、隔离级别以及事务执行流程源码结合案例分析
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!