总言
主要内容:介绍事务。理解事务概念(为什么存在),理解事务的四种属性(原子性、持久性、隔离性、一致性),理解事务的隔离级别(四种隔离级别,读写并发说明)。
文章目录
- 总言
- 1、什么是事务?
- 1.1、概念理解
- 1.2、事务属性(ACID)
- 2、事务常见操作方式
- 2.1、事务的版本支持
- 2.2、事务提交方式
- 2.3、操作演示(事务原子性和持久性)
- 2.3.1、演示前准备工作:将隔离级别降到最低
- 2.3.2、相关指令汇总
- 2.3.3、演示一:证明事务的开始与回滚(我们手动演示状态)
- 2.3.4、演示二:遇到异常情况时,MySQL会自动回滚
- 2.3.4.1、事务未提交,演示mysqld崩溃(客户端)、终端崩溃
- 2.3.4.2、事务已提交,演示崩溃
- 2.3.5、演示三:证明单条 SQL 语句与事务的关系
- 2.3.6、小结
- 2.4、隔离级别与事务隔离性
- 2.4.1、一个感性认识(阶段理解一)
- 2.4.2、查看与设置隔离性(相关SQL语句与演示)
- 2.4.2.1、查看隔离级别
- 2.4.2.2、设置隔离级别
- 2.4.2.3、相关演示
- 2.4.3、四种隔离级别
- 2.4.3.1、总览
- 2.4.3.2、读未提交【Read Uncommitted】
- 2.4.3.3、读提交【Read Committed】
- 2.4.3.4、可重复读【Repeatable Read】
- 2.4.3.5、串行化【Serializable】
- 2.4.3.6、小结:理解事务一致性
- 2.4.5、MySQL中的实现(阶段理解二)
- 2.4.5.1、三种并发场景
- 2.4.5.2、读-写并发:三个前提知识
- 2.4.5.3、读-写并发:多版本并发控制( MVCC )
- 2.4.5.4、Read View
- 2.5.6、基于阶段理解二,解释RR 与 RC的本质区别
- 2.5.6.1、说明
- 2.5.6.2、演示
- Fin、共勉。
1、什么是事务?
1.1、概念理解
1)、一个重要说明
上层业务角度:上层看来比较简单的需求,可能对应的后端要做很多的工作,组合起来才是一个完整的需求解决的方案。
技术角度:事务是一个一个或者多个sql语句的集合,这一组DML语句要么全部成功,要么全部失败,是一个整体。(实际意义:这些语句的结合,构成了现实世界中一个实际事件的操作。)
关系: “事务”不是数据库类软件设计本身的天然需求。而是由于与现实生活所关联,为了简化程序员的工作模型而考虑。(因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
)
特别说明:对事务的学习,站在上层角度(使用者),而非程序角度。如此理解起来较为容易。
2)、什么是事务?
MySQL中的事务(Transaction)是一个数据库操作序列(由一组DML语句组成)。这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。事务还规定不同的客户端看到的数据是不相同的。
MySQL提供一种机制,确保数据的完整性和一致性,即使在并发操作或系统崩溃的情况下也能保持。 (需要注意,对于事务不要抽象的理解它,mysqld要提供事务机制,注定了mysqld内部编码和数据结构的支持,而同时存在多个事务也就意味着mysqld要对多个事务进行管理工作。因此,事务落到实处便是以某种数据结构+算法管理起来的结构。)
1.2、事务属性(ACID)
这些属性间的关系: 实现了原子性、隔离性、持久性,也就保障了一致性。换句话说,一致性是概念层面的性质,而其它三者是MySQL内部需要实现的。
原子性: 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
2、事务常见操作方式
2.1、事务的版本支持
show engines\G
可以查看哪些引擎支持事务。如下述,在 MySQL 中, 使用了 Innodb 数据库引擎的数据库或表能够支持事务, 而MyISAM 不支持。
mysql> show engines\G;
*************************** 1. row ***************************
Engine: InnoDB -- 引擎名称
Support: DEFAULT -- 默认引擎
Comment: Supports transactions, row-level locking, and foreign keys -- 描述
Transactions: YES -- 支持事务
XA: YES
Savepoints: YES -- 支持事务保存点
*************************** 2. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 3. row ***************************
Engine: MEMORY
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 4. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 5. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 7. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 8. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
*************************** 9. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
9 rows in set (0.00 sec)
ERROR:
No query specified
mysql>
2.2、事务提交方式
1)、MySQL中,事务的提交方式
常见的有两种:自动提交、手动提交。
show variables like 'autocommit';
:可用于查看当前事务的提交方式。
show variables;
:该指令会列出MySQL服务器的所有系统变量及其当前值。 这些变量控制MySQL服务器的各种操作和行为。执行该指令会返回一个包含大量行的结果集,每行代表一个系统变量及其对应的值。由于结果集可能非常大,可以通过条件筛选过滤结果以查找需要的变量。
2)、如何修改 MySQL 的自动提交模式?
在 MySQL 中,可以使用 SET
指令来修改自动提交(autocommit)模式。
SET AUTOCOMMIT=0; --禁止自动提交
SET AUTOCOMMIT=1; --开启自动提交
3)、事务提交该选项影响什么?
2.3、操作演示(事务原子性和持久性)
2.3.1、演示前准备工作:将隔离级别降到最低
1、这里主要演示原子性和持久性,为了便于演示,我们将mysql的默认隔离级别设置成读未提交。(关于隔离性相关使用和设置,见博文后续小节)
2、创建测试使用的表
mysql> show create table Person \G;
*************************** 1. row ***************************
Table: Person
Create Table: CREATE TABLE `Person` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`description` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
ERROR:
No query specified
2.3.2、相关指令汇总
1)、开启事务
在MySQL中,默认情况下,每个SQL语句都是一个单独的事务。但是,如果想要将多个SQL语句组合成一个事务,需要首先开始一个事务。 可通过下述命令完成(这两个命令在功能上是等效的,都会开始一个新的事务)
START TRANSACTION;
-- 或者
BEGIN;
2)、回滚事务
说明: 如果在执行SQL语句的过程中出现了错误,或者决定撤销到目前为止在事务中所做的所有更改,可以使用ROLLBACK
相关命令。
ROLLBACK;
ROLLBACK ;
这将撤销自事务开始以来所做的所有更改,并将数据库恢复到事务开始时的状态。
3)、事务保存点
说明: 在MySQL中,保存点(Savepoint)是一个在事务处理中设置的临时标记或命名点,它允许你在事务中回滚到特定的点,而不是整个事务的开始。
①创建一个保存点: 使用SAVEPOINT
语句来创建一个保存点。其中,savepoint_name
是为当前保存点指定一个唯一的名称。
SAVEPOINT savepoint_name;
②回滚到保存点: 如果在执行一系列操作之后,你需要回滚到保存点的状态,可以使用ROLLBACK TO SAVEPOINT
语句。
ROLLBACK TO savepoint_name;
这里的savepoint_name
是之前创建的保存点的名称。执行该语句后,带来效果如下:
Ⅰ、数据库将回滚到指定的保存点状态,并且保存点将被释放(除非再次设置)。
Ⅱ、自该保存点之后的所有操作都将被撤销。
③释放保存点(可选): 虽然在回滚到保存点后,保存点会自动释放,但如果需要在不执行回滚的情况下释放保存点,可以使用RELEASE SAVEPOINT
语句。
RELEASE SAVEPOINT savepoint_name;
--释放指定的保存点,但不会回滚任何操作。
4)、提交事务
如果对在事务中执行的所有SQL语句都满意,并希望将这些更改永久保存到数据库中,可以使用COMMIT命令。这将使自事务开始以来所做的所有更改成为永久性的。
COMMIT;
5)、结束事务
严格来说,"结束事务"并不是一个单独的MySQL命令。当使用COMMIT或ROLLBACK命令时,事务就会结束。 但是,也可以通过断开与MySQL服务器的连接来结束未提交的事务(后者通常不是一个好的做法,因为它可能导致数据不一致)。
2.3.3、演示一:证明事务的开始与回滚(我们手动演示状态)
分别演示了rollback 到指定保存点和 单独rollback的情况。
2.3.4、演示二:遇到异常情况时,MySQL会自动回滚
2.3.4.1、事务未提交,演示mysqld崩溃(客户端)、终端崩溃
演示客户端崩溃:MySQL会自动回滚
演示终端崩溃:MySQL会自动回滚
演示关闭自动提交:MySQL仍旧回滚
上述例子主要在于理解事务的原子性,同时也证实了自动提交不影响手动开启事务。(通过START TRANSACTION或BEGIN语句显式控制的开始事务。可以在该事务中执行多条SQL语句,这些语句在提交(COMMIT)之前,对数据库所做的更改都是临时的。)
那么, 事务提交该选项影响什么?后续将演示到。
2.3.4.2、事务已提交,演示崩溃
在我们使用commit后发生客户端崩溃或终端崩溃,不影响已经提交的操作(已经持久化)。
2.3.5、演示三:证明单条 SQL 语句与事务的关系
1)、问题引入
回顾我们以前的MySQL语句,我们并未手动开启事务,但这些CRUD操作仍旧有效记录的。这是为什么?
回答:默认情况下,SQL语句是以事务的方式进行提交的,只不过该事务只有一个sql语句(历史上我们学习的CURD操作)。
2)、演示autocommit开启与否下,执行SQL语句的结果(不手动开启事务)
在先前的2.3.4小节中,我们演示过手动开启事务,发现自动提交对其并无影响。实际上,自动提交影响的是未使用主动开启事务时的CURD操作。
自动提交(AUTOCOMMIT): 这是MySQL的默认设置,意味着每执行一条SQL语句,都会立即提交到数据库,无论该语句是数据修改操作(如INSERT、UPDATE、DELETE)还是数据查询操作(如SELECT)。当autocommit设置为ON或1时,MySQL处于自动提交模式。
以下是相关演示验证:
下述演示验证了:对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。
下述演示验证了,使用commit提交,事务持久化,与是否设置set autocommit无关。
2.3.6、小结
我们将上述演示的各内容进行一个汇总:
- 只要输入
begin
或者start transaction
,事务便必须要通过commit
提交,才会持久化,与是否设置set autocommit
无关。 - 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用
rollback
(前提是事务还没有提交) - 如果一个事务被提交了(
commit
),则不可以回退(rollback
) - InnoDB 支持事务, MyISAM 不支持事务。对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL 有 MVCC )
2.4、隔离级别与事务隔离性
2.4.1、一个感性认识(阶段理解一)
1)、如何理解事务的隔离性?
MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行一个事务可能由多条SQL构成。也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。
所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
但毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
2)、为什么要设置不同的隔离级别?
根据上述,隔离级别体现了不同程度的抗干扰能力(就比如有人能在人群嚷嚷中心无旁骛的看书,但有人在周围环境纷杂下无法专心下来,这体现了个人抗干扰能力。换到数据库也是同样道理)。
这就意味着,隔离级别设置得越强,其所需成本越高,相应的效率也会越低。
而我们实际开发的场景是多样化的,可灵活变通,因此,设置不同的隔离级别,实际就是允许我们针对不同场景,采取不同程度的妥协,以达到 效率 vs 可靠性 之间的平衡。
3)、MySQL中隔离级别有哪些?
见下述。
2.4.2、查看与设置隔离性(相关SQL语句与演示)
一个前提说明: 在MySQL中,事务的隔离级别可以设置为全局级别或会话级别。这些隔离级别定义了多个并发事务之间的隔离程度,以防止脏读、不可重复读和幻读等问题。
2.4.2.1、查看隔离级别
全局隔离级别是MySQL服务器级别的设置,它决定了新建立的连接默认使用的事务隔离级别。全局隔离级别对所有新建立的连接有效,但不会影响已经存在的连接。
查看全局隔级别:
SELECT @@global.tx_isolation
SELECT @@global.transaction_isolation
--MySQL8 开始,关键字由简写变成全写,其他都一样。
--MySQL8 以前,两种写法都行。
会话隔离级别是针对当前数据库会话的设置,它决定了当前连接使用的事务隔离级别。会话隔离级别仅对当前连接有效,不会影响其他连接。
查看会话隔级别:
SELECT @@session.tx_isolation
SELECT @@session.transaction_isolation
如果没有指明全局还是会话,采用就近原则,默认指会话隔离级别:
SELECT @@tx_isolation;
2.4.2.2、设置隔离级别
更改/设置当前会话/全局隔离级别:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL
{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
SESSION | GLOBAL
:这两选项表明要设置当前会话还是全局隔离级别。
READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE
:分别指代四种隔离级别:
READ UNCOMMITTED --读取未提交
READ COMMITTED --读取已提交
REPEATABLE READ --可重复读
SERIALIZABLE --可串行化
2.4.2.3、相关演示
1)、演示
查看隔离级别:
现在,我们将会话窗口关掉/将终端关掉再重新登录,来看看隔离级别有什么变化:
我们将MySQL服务器重启,查看一下隔离级别有什么变化:
2)、一个小结
重启MySQL服务器后,之前设置的全局隔离级别通常会恢复到MySQL配置文件(通常是my.cnf
或my.ini
)中指定的默认隔离级别。 如果没有在配置文件中明确指定隔离级别,那么它通常会使用MySQL的默认隔离级别,对于InnoDB存储引擎来说,默认隔离级别是REPEATABLE READ
。
关闭终端或退出MySQL服务端 并不会直接影响事务的隔离级别。但是,如果在会话级别设置了隔离级别,并且这个会话因为关闭终端或退出MySQL服务端而结束,那么该会话设置的隔离级别将不再有效。 对于新的会话,它将使用全局隔离级别,除非我们在新的会话中再次设置会话级别的隔离级别。
因此,如果我们想要确保某个隔离级别在MySQL服务器重启后仍然有效,应该在MySQL的配置文件中明确指定它。这样,无论服务器何时重启,都会使用配置文件中指定的隔离级别。
2.4.3、四种隔离级别
2.4.3.1、总览
事务存在长短事务之分。我们所说的事务间互相影响,更多情况下是指事务在并行执行的时候产生的影响(如多个事务同时运行,周期短的事务执行结果干扰到周期长的事务后续执行流程。)
隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
2.4.3.2、读未提交【Read Uncommitted】
1)、如何理解事务中的commit操作与刷盘?
问题描述: 我们知道,commit提交事务后,就会将对应操作持久化到MySQL中,造成实际修改。而这些数据是保存在我们磁盘上的。那么,这是否意味着,进行了commit操作后,就会将数据刷盘?(刷盘:通常指的是将数据从内存中的缓冲区或缓存写入到磁盘上的物理存储介质中。)
回答:COMMIT操作本身并不直接等同于刷盘。 虽然COMMIT会触发一些内部机制来确保数据的持久性,但它并不直接控制或管理数据的刷盘过程。
实际上,MySQL的InnoDB存储引擎,使用了一个称为redo日志(redo log)的机制来确保事务的ACID属性(原子性、一致性、隔离性和持久性)。
①Redo日志: 当事务发生时,InnoDB会先将事务的更改写入到redo日志中,而不是直接写入到数据文件中。这样做的好处是可以提高性能,因为日志写入通常是顺序的,比随机I/O操作(如直接写入数据文件)要快得多。
②刷盘(Flush): 虽然COMMIT会触发redo日志的写入,但数据的实际刷盘(即将数据页从内存写入到磁盘上的数据文件中)是异步进行的。InnoDB会根据其配置和内部策略来决定何时将数据页刷盘。 这通常涉及到一些复杂的算法和参数,如innodb_flush_log_at_trx_commit和innodb_buffer_pool_size。
③后台进程: InnoDB有一个后台进程(I/O进程),它负责将数据页从内存中异步地刷新到磁盘上的数据文件中。这个过程是根据InnoDB的I/O调度算法和缓冲池(buffer pool)的使用情况来决定的。
但总体来说,执行了commit 操作,事务中所做的更改都会成为数据库中的永久部分(持久化)。
2)、演示理解读未提交
读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。这是最低的隔离级别,可能会导致很多问题,比如脏读、不可重复读和幻读。
读未提交这种隔离级别的优点是可以提高并发性能,因为它不会在读取数据时对它们进行任何锁定或并发控制。但是,这种隔离级别的缺点是可能会导致数据的不一致性,因为读取的数据可能包含未提交或已回滚的更改,因此不建议在生产环境中使用。
总之,READ-UNCOMMITTED是一种非常不严格的事务隔离级别,应该谨慎使用,并且只在某些特定情况下考虑使用,例如对于只读取数据的报表查询等场景。
2.4.3.3、读提交【Read Committed】
1)、先导演示
先前验证原子性、持久性时我们都是在读未提交下进行的,这里我们先演示一下非手动开始事务时,读提交情况:
2)、演示理解读提交
读提交【Read Committed】:一个事务只能读取另一个事务已经提交的修改。 可以防止脏读,但不可重复读和幻读仍可能发生。
如下图演示,其现象一个事务(T2)正在运行过程中,另一个事务(T3)提交了修改,导致T2在随后的查询中“看到”T3所做的修改。
实际上,这是读提交所带来的问题:由于 READ-COMMITTED 允许在事务中看到其他事务的提交更改,因此它可能会导致“不可重复读”的现象。即,同一个事务内的两个相同查询可能会返回不同的结果集。 (多见于并发事务中)
3)、不可重复读可能带来的主要影响说明
①数据不一致性: 由于在一个事务内多次读取同一数据时得到的结果可能不同,这会导致数据的不一致性。这种不一致性可能使应用程序的逻辑出错,因为它可能基于一个预期的数据状态进行决策,但实际上这个状态已经发生了变化。
②业务逻辑错误: 在涉及金融、订单处理、库存管理等关键业务逻辑的应用中,不可重复读可能导致严重的业务逻辑错误。例如,在订单处理系统中,如果一个事务在两次读取库存量之间,另一个事务已经完成了库存扣减并提交,那么第一个事务可能会基于一个已经过时的库存量进行决策,导致超卖或库存不足的情况。
③难以调试和排查: 由于不可重复读通常是由多个并发事务之间的交互引起的,因此它可能很难调试和排查。开发人员需要仔细分析事务的执行顺序、锁的使用情况以及数据的修改情况,才能确定问题的根本原因。
2.4.3.4、可重复读【Repeatable Read】
1)、演示理解可重复读
可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别。一个事务在整个生命周期内多次读取操作数据时,看到的数据是一致的。可以防止脏读和不可重复读,但幻读可能发生。
2)、可重复读存在的问题
通过上述演示,可以知道,对于隔离性为可重复读的事务,若终端A在特定事务中执行的insert操作,在与之并发的事务周期内,不会对终端B产生影响,即B事务在多次读取相同数据时能够获取到一致的结果,这符合可重复读(Repeatable Read)的特性。(符合可重复的特点)
然而,通常情况下,数据库在可重复读隔离级别下,虽然能够确保同一事务内多次读取相同数据的一致性,但无法直接屏蔽其他事务新插入(insert)的数据。
这是因为传统的隔离性实现通常依赖于对数据行的加锁机制。然而,当涉及到insert操作时,由于待插入的数据在插入之前是不存在的,因此无法直接对这些不存在的数据行进行加锁。这导致了一个问题:即使事务B在读取数据时加锁了某些行,事务A仍然可以在这些行之间插入新的数据,当事务B再次读取时,就会“看到”这些新插入的数据,这种现象被称为幻读(Phantom Read)。
幻读的出现是因为在可重复读隔离级别下,数据库系统虽然保证了已存在数据的一致性,但无法阻止新数据的“幻影般”出现。然而,值得注意的是,MySQL数据库在采用可重复读(Repeatable Read)隔离级别时,通过引入Next-Key锁(GAP锁与行锁的结合)来解决了幻读问题。这种锁机制不仅锁定了已存在的数据行,还锁定了数据行之间的间隙,从而防止了其他事务在这些间隙中插入新数据,确保了事务读取结果的一致性。
简而言之,虽然传统的数据库在可重复读隔离级别下无法直接屏蔽其他事务的insert操作,但MySQL通过其特有的Next-Key锁机制成功解决了这一问题。
2.4.3.5、串行化【Serializable】
1)、演示理解串行化
串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。(这种隔离级别太极端,实际生产基本不使用)
2)、关于Serializable隔离级别的简介
※并发控制: Serializable隔离级别通过强制事务串行执行来避免并发问题。这意味着在同一时间内,只有一个事务能够访问数据库中的资源,从而消除了其他事务可能引起的冲突。
※数据一致性: 由于Serializable隔离级别下的事务是串行执行的,因此它可以确保数据在任意时刻都是一致的。这意味着在事务执行过程中,其他事务无法修改或删除正在被当前事务访问的数据。
※锁机制: 在Serializable隔离级别下,MySQL使用了共享锁和排他锁来确保数据的独占访问。当一个事务开始执行时,它首先会获取对所需数据的共享锁。如果事务需要执行写操作(如INSERT、UPDATE或DELETE),则会将共享锁升级为排他锁,以阻止其他事务并发访问这些数据。
※性能影响: 由于Serializable隔离级别强制事务串行执行,因此它可能会对数据库性能产生较大的影响。特别是在高并发场景下,这种隔离级别可能会导致大量事务等待执行,从而降低系统的吞吐量。
2.4.3.6、小结:理解事务一致性
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。 当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑。也就是说,一致性是由用户决定的(技术上,MySQL通过AID保证C为操作数据库的用户提供一致性,但这也要使用者所操作执行的逻辑没有问题,才能达到整个业务程序的一致性。例如一个转账系统,在一方add,另一方delete,MySQL保证能单条SQL语句的一致性,但add与delete这个业务逻辑操作是由使用者来维护的。如果使用者只执行其中一项操作,那所获得的业务结果也是不正确的。)。
2.4.5、MySQL中的实现(阶段理解二)
2.4.5.1、三种并发场景
数据库并发的场景有三种:
1、读-读 : 不存在任何问题,也不需要并发控制。(多个读操作不会修改数据。因此它们可以并发执行而不会产生冲突。)
2、读-写 : 有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。(比如我们上述在隔离性中演示的各种例子,就是读写并发的场景。)
3、写-写 : 有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失(后面补充)
扩展:
第一类更新丢失(也称为覆盖更新丢失): 当两个事务都读取了同一数据,然后基于读取的值进行修改并提交时,后提交的事务会覆盖先提交的事务的修改。
第二类更新丢失(也称为写-写丢失或写偏斜): 当一个事务读取一个数据项,对其进行修改,并提交;但另一个事务也在同时读取了同一数据项,进行了不同的修改并提交,这导致第一个事务的修改被丢失。这与第一类更新丢失不同,因为两个事务都是基于原始数据进行修改的,而不是基于彼此修改后的数据。
这里我们主要介绍读写并发的场景,即在上一小节中,我们只是探讨了事务隔离性的现象,这里我们将探讨这种现象背后的实现机
制,从而进一步理解隔离性(当然,也只是加深理解,非事无巨细)。
2.4.5.2、读-写并发:三个前提知识
1)、MVCC引入
实际上,读-写并发过程中,存在的冲突主要是由多版本并发控制( MVCC ) 来解决的。MVCC是一种在数据库管理系统中实现对数据库的并发访问的技术,其为无锁并发控制。
要理解 MVCC ,需要知道三个前提知识:
3个记录隐藏字段
undo 日志
Read View
2)、3个记录隐藏字段
隐藏列字段(Invisible Column 或 Hidden Field)是一种数据库表结构中的特殊列,在建表时创建。这些列在常规查询操作中不会被显示或考虑,但在某些特定情况下(如数据库升级、数据审计等)可以被访问或操作。
InnoDB存储引擎为了支持其特有的功能(如MVCC、行级锁定等),引入了一些隐藏列字段。这里我们主要简介其中几个。
DB_TRX_ID
(6 byte):
这个字段存储了最近一次修改或插入这条记录的事务ID。每当一个事务修改或插入一条记录时,该事务的ID就会被写入这个字段。这使得InnoDB能够知道哪个事务对这条记录做了最后的修改。
DB_ROLL_PTR
(7 byte):
这个字段是一个回滚指针,它指向了这条记录在undo日志中的上一个版本。 当事务对记录进行修改时,InnoDB不会直接覆盖原始数据,而是将原始数据保存在undo日志中,并通过DB_ROLL_PTR指向它。如果其他事务需要读取这条记录在修改前的版本(例如,为了支持一致读或MVCC),InnoDB就可以通过DB_ROLL_PTR找到undo日志中的相应版本。
DB_ROW_ID
(6 byte):
隐含的自增ID(隐藏主键),如果一个表没有定义主键,InnoDB会自动以DB_ROW_ID 生成一个聚簇索引。 聚簇索引决定了表中数据的物理存储顺序。即使表中定义了其他唯一索引作为主键,DB_ROW_ID仍然会存在,但它不会被用到。只有当表既没有主键也没有唯一非空索引时,DB_ROW_ID才会作为聚簇索引的一部分被使用。
关于删除: 在InnoDB中,当一条记录被“删除”时,实际上它并没有被从磁盘上物理删除。相反,InnoDB会设置该记录的某个字段(“删除flag”字段)来标记这条记录为“已删除”。 这样做的好处是,如果后续的事务需要读取这条记录在“删除”之前的版本(例如,为了支持MVCC),InnoDB仍然可以找到它。
3)、undo 日志
Undo日志,也称为“回滚日志”。可以将其理解为MySQL(InnoDB存储引擎)在内存中用于保存事务修改前数据状态的一个缓冲区。(PS:不要将日志理解为语言层面上的只是用来记录信息的,这里的Undo日志就具有功能性,可以保存数据。)
简单来说,Undo 日志是MySQL(特别是InnoDB存储引擎)在内存中维护的一段特殊区域,用于记录数据修改前的状态。当事务对数据库中的数据进行修改时(如UPDATE或DELETE操作),InnoDB存储引擎不仅会在内存中修改数据,还会在Undo日志中记录修改前的数据状态。这样,如果事务需要回滚(即撤销之前的修改),InnoDB就可以通过Undo日志中的数据来恢复到事务开始之前的状态,保证事务的原子性和数据的一致性。
同时,为了持久化存储和防止数据丢失,当事务提交后,InnoDB会将相关的数据页和Undo日志信息刷新到磁盘上的数据文件和Undo日志文件中。 这样,即使MySQL服务进程重启或系统发生故障,也能通过读取磁盘上的数据和日志信息来恢复数据库到一致的状态。
undo log中的数据是临时的,当前用户commit后就会清除。(除非该数据在并发下被其它用户访问)
2.4.5.3、读-写并发:多版本并发控制( MVCC )
MVCC(多版本并发控制)是数据库管理系统(DBMS)中用于处理并发事务的一种技术,它允许事务在读取数据时不必等待其他事务完成,从而提高了数据库的并发性能。
在MVCC中,为事务分配单向增长的事务ID,对每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:①在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。②同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
1)、如何更新:update
1、现在有一个事务,其ID=10,对下表中记录进行修改(update):将name(李白)改成name(杜甫)。流程简述如下:
- 当前事务id = 10,要修改记录,加行锁。
- 修改前,先将待修改的原始行记录拷贝到undo log中(这就意味着undo log中有了一行副本数据。(原理类似写时拷贝)
- 进行修改操作,将原始记录中name 修改为 ‘杜甫’。获得新记录,将其隐藏字段 DB_TRX_ID 设置为事务10 的ID(表明最新一次修改的事务是id=10的事务,之后id递增)。
- 在新记录的回滚指针 DB_ROLL_PTR 列里,写入undo log中副本数据的原始记录的地址,既表示当前版本的上一个版本就它。
- 事务10提交,释放锁。
2、现在又有一个事务ID=11,要对表中记录进行修改(update):将age(22)改成age(38)。流程简述如下:
- 当前事务id=11,因为也要修改,所以要先给该记录加行锁。
- 修改前,先将待修改行记录拷贝到undo log中(对新的副本,采用头插方式,插入undo log。此时,undo log中就又有了一行副本数据)。
- 之后,修改原始记录中的age,将其修改为38。获得新记录,并修改原始记录的隐藏字段 DB_TRX_ID(10) 为DB_TRX_ID(11)
- 对修改后的记录的回滚指针列 DB_ROLL_PTR ,写入undo log中原始记录的副本数据地址,从而指向副本记录,既表示当前版本的上一个版本就是它。
- 事务11提交,释放锁。
如上述,当数据被修改时,不会直接覆盖原始数据,而是会创建一个新的数据版本。这些版本按时间顺序链接在一起,形成一个历史版本链(上面的一个个版本,我们可以称之为一个一个的快照)。
2)、如何删除:delete
设置flag标志位,也可以形成版本。
- 当执行DELETE操作时,InnoDB并不会立即从磁盘上删除记录,而是会标记这些记录为“已删除”或修改其元数据。同时,undo log中会记录这些被删除记录之前的旧版本。
- DELETE操作后,该行数据的版本号会被更新为当前事务的版本号(或稍后的某个版本号),以表示该行数据已经被当前事务删除。其他事务在读取数据时,会根据它们的隔离级别和版本信息来决定是否读取undo log中保存的旧版本数据。
3)、如何插入:insert - INSERT操作是在向表中添加新数据,这表明其之前没有数据,按理应该不涉及到读取旧版本的数据,但其他并发执行的事务可能会需要读取在INSERT操作之前的数据版本,此外为了支持事务的回滚,InnoDB仍然会生成undo log记录,来保存这次insert操作的相关信息。
- 当事务提交(commit)后,这些undo log记录可以被标记为可重用或可覆盖(实际的清理过程是由InnoDB的后台进程自动管理)。
4)、如何查询:select - SELECT操作本身不会对数据做任何修改(只是在查询内容),因此,从逻辑上讲,为SELECT维护多版本数据似乎没有直接的意义,因为SELECT并不需要查看数据的多个历史版本。
- 但有一个问题,在MVCC的上下文中,SELECT读取时,是读取最新的版本呢?还是读取历史版本?
实际上,select有两种不同的读取方式:当前读(Current Read)和快照读(Snapshot Read)。
当前读:读取的是最新的记录,即读取时数据库中的实际数据版本。
① 所有的增删改操作(如INSERT
、UPDATE
、DELETE
)以及使用特定锁定选项的SELECT操作(如SELECT … LOCK IN SHARE MODE或SELECT … FOR UPDATE)都属于当前读。
②当前读通常需要加锁以确保数据的一致性和隔离性。
快照读:读取的是数据的历史版本(一般而言),这是MVCC的核心特性之一。
①普通的SELECT
操作(不带有任何锁定选项)通常是快照读。
②快照读允许事务读取某个时间点的数据快照,而无需等待其他事务完成。
③快照读不需要加锁,因此多个事务可以并发执行(读写并发),从而提高了并发性能。
在多个事务同时执行增删改操作时,它们都是当前读,此时是需要加锁的。那同时有SELECT操作过来,也需要读取最新版(当前读),那么也就需要加锁,这就是串行化。
然而,如果SELECT操作采用快照读的方式,即读取数据的历史版本,那么它就不会受到其他事务加锁的影响。这种非锁定读取的特性使得多个事务可以并行执行,从而显著提高了系统的并发性能。这正是MVCC的意义所在:它允许事务在读取数据时不需要等待其他事务的完成,从而提高了系统的吞吐量和响应速度。
2.4.5.4、Read View
1)、是什么决定了select是当前读,还是快照读呢?
隔离级别。事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但不管怎么,启动多个事务,总是有先有后的。那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,应该让不同的事务看到它该看到的内容(比如,先来的事务,应不应该看到后来的事务所做的修改?),这就是所谓的隔离性与隔离级别要解决的问题。
2)、介绍Read View
Read View是事务进行 快照读 操作的时候生产的 读视图 (Read View)。在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
需要注意,begin
启动事务的时候,是没有read view
的, 但是肯定有事务ID和事务对象。当我们首次进行select
的时候,mysqld会自动给我们形成read view。
Read View 在 MySQL 源码中就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
以下为ReadView的简化结构:
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
我们需要注意以下几个类成员:
m_ids;
这是一张列表,用来维护Read View生成时刻,系统正活跃的事务ID。
up_limit_id;
记录m_ids列表中,最小的事务ID(没有写错)
low_limit_id;
记录了ReadView生成时刻,系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
creator_trx_id
记录了创建该ReadView的事务ID
下述图解释了当前快照读,应不应该读到当前版本记录:
对应源码如下: 如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。
3)、举例演示
假设当前有条记录如下:
name | age | DB_TRX_ID(创建该记录的事务ID) | DB_ROW_ID(隐式主键) | DB_ROLL_PTR(回滚指针) |
---|---|---|---|---|
李白 | 22 | null | 1 | null |
我们演示上述事务操作:
step1:在事务4中进行update操作,将name(李白)修改为name(杜甫)。
step2:之后,在事务2中select读取数据(此时才发生快照读)
事务1 [id=1] | 事务2 [id=2] | 事务3 [id=3] | 事务4 [id=4] |
---|---|---|---|
事务开始 | 事务开始 | 事务开始 | 事务开始 |
_ | _ | _ | 修改且已提交 |
进行中 | 快照读 | 进行中 | |
_ | _ | _ |
由此,数据库为该行数据生成一个 Read View 读视图:
//事务2的 Read View
m_ids; // 1,3(因为4在快照读前已经提交)
up_limit_id; // 1(根据m_ids获取)
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
现在,让我们分析事务2select读取时,能看到的记录版本: 事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID
去跟up_limit_id
,low_limit_id
和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。
//在版本链中逐个遍历查询
//获取到事务4提交的记录对应的事务ID
DB_TRX_ID=4
//比较步骤
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中
//结论
对事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
在MySQL中验证:
2.5.6、基于阶段理解二,解释RR 与 RC的本质区别
2.5.6.1、说明
RR 与 RC的本质区别:正是Read View生成时机的不同,从而造成RC(读已提交)、RR(可重复读)级别下快照读的结果的不同。
在RR级别下,当一个事务对某条记录进行第一次快照读时,系统会为该事务创建一个快照及对应的Read View,同时记录下当前系统中所有活跃的其他事务。此后,只要当前事务再次进行快照读操作,它将始终使用这个首次创建的Read View。 这意味着其它事务在Read View创建后所做的修改,对当前事务而言是不可见的。
换句话说,Read View在生成时会捕获当前所有其他活动事务的状态,并且这些事务的任何后续修改对当前事务来说都是不可见的,而Read View创建之前已经提交的事务所做的修改则是可见的。
在RC级别下,事务的每次快照读操作都会生成一个新的快照和对应的Read View。这就是为什么在RC级别下,一个事务能够看到其他事务提交的更新的原因,因为每次快照读都是基于最新的系统状态进行的。
总之,在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
2.5.6.2、演示
RR模式下,事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力(这里演示的是select,实则delete同样如此)
演示使用SQL:select * from XXX lock in share mode
,以加共享锁方式进行读取,对应的就是当前读。