往期回顾:
事务管理是数据库管理系统( DBMS )中的关键概念,用于确保数据库操作能够以一致、可靠的方式进行。
事务是数据库中一组相关的操作,被视为一个不可分割的工作单元。这组操作要么全部执行成功,要么全部失败回滚,不存在部分执行的情况。
事务管理的目标是保障数据库的完整性和一致性,确保在并发操作的环境下,多个事务能够正确地进行交互,而不会导致数据不一致或损坏。
ACID 是数据库管理系统( DBMS )中用于描述事务特性的一组原则,其中包括:
这些原则旨在确保在数据库执行的事务中维护数据的正确性和可靠性。
事务管理是数据库系统中为了满足 ACID 属性而设计的一套机制和操作,通过这些机制,数据库系统可以确保事务的正确执行,维护数据的一致性和可靠性。ACID 属性是事务管理的核心原则,指导着事务在数据库中的正确执行和处理。
#并发编程## 3 、事务的开始与结束
在 MySQL 中,使用 START TRANSACTION 或者简单的 BEGIN 语句来标志事务的开始。
事务的结束可以通过 COMMIT 来确认事务的全部操作成功,或者通过 ROLLBACK 来回滚事务,撤销事务的全部操作,将数据库恢复到事务开始前的状态。
如下所示:
-- 开始事务
START TRANSACTION; -- 或者使用 BEGIN;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
MySQL 支持多个事务隔离级别,通过 SET TRANSACTION ISOLATION LEVEL 语句进行设置。不同的隔离级别影响事务在并发环境中的行为,包括读未提交、读已提交、可重复读和串行化。
如下所示:
-- 设置事务隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
MySQL 允许在事务执行期间设置保存点,以便在事务的不同阶段进行回滚,而不是回滚整个事务。这可以通过 SAVEPOINT 和 ROLLBACK TO 语句来实现。
如下所示:
-- 设置保存点
SAVEPOINT savepoint_name;
-- 回滚到保存点
ROLLBACK TO savepoint_name;
MySQL 支持事务的嵌套执行,可以在一个事务中包含另一个事务。但需要注意,嵌套事务的使用要谨慎,确保能够正确提交或回滚各个层次的事务。
-- 嵌套事务
START TRANSACTION; -- 外层事务开始
-- 在这里执行其他 SQL 语句
SAVEPOINT inner_savepoint; -- 设置保存点
-- 在这里执行其他 SQL 语句
ROLLBACK TO inner_savepoint; -- 回滚到保存点
COMMIT; -- 外层事务提交
事务隔离级别是指在多个事务并发执行时,一个事务的执行不应该被其他事务干扰的程度。
SQL 标准定义了四个隔离级别:
从上面可以看出,隔离级别越高就是越接近串行化,而串行化就是按照事务的先后顺序执行,一个事务操作可能就要等待很久才能执行,并发执行的效率就没有了。
不同隔离级别的用途就是为了提高并发执行效率,隔离级别越低,并发执行效率越高,资源的共享程度就越高。
在都是读取操作的时候,资源共享有利于提高效率,并且不会造成问题。但是只要一个事务中出现写操作,就会带来问题。
在并发事务下,可能会导致以下几种问题:
问题 | 描述 | 例子 |
---|---|---|
脏读 | 一个事务读取了另一个事务未提交的数据。 | 事务 A 读取某一行数据,事务 B 修改了这行数据但尚未提交,事务 A 读取到了事务 B 未提交的修改。 |
不可重复读 | 在同一个事务中,两次读取同一数据得到的结果可能不同,因为其他事务可能在两次读取之间修改了数据。 | 事务 A 两次读取同一行数据,事务 B 在两次读取之间修改了这行数据,导致事务 A 得到不同的结果。 |
丢失更新 | 两个事务同时读取相同的数据,然后对其进行修改,最后只有一个事务的修改生效,另一个事务的修改被丢失。 | 事务 A 和事务 B 同时读取相同的数据,然后分别修改并提交,由于并发,只有一个事务的修改生效,另一个被覆盖。 |
幻读 | 一个事务多次查询时,由于其他并发事务的插入或删除操作,导致看到了不同的数据行。这通常与范围查询有关。 | 事务 A 查询某一范围内的数据,事务 B 在该范围内插入了新的数据,导致事务 A 多次查询时看到了不同的数据行。 |
从上文可以看出不可重复读与幻读其实在概念上很接近的,它们的主要区别在于:
对于上面的问题,不同的事务隔离级别都分别解决了什么问题呢?具体如下所示:
问题 \ 隔离级别 | 读未提交 | 读已提交 | 可重复读 | 串行化 |
---|---|---|---|---|
脏读 | 无法解决 | 解决 | 解决 | 解决 |
不可重复读 | 可能发生 | 解决 | 解决 | 解决 |
丢失更新 | 无法解决 | 无法解决 | 解决 | 解决 |
幻读 | 可能发生 | 可能发生 | 部分解决 | 解决 |
从上面的表格中可以看出,可重复读隔离级无法完全解决幻读的问题,接下来我们就来具体了解一下:
可重复读可以解决的场景:
可重复读无法解决的场景:
总体而言,可重复读提供了一定程度的幻读保护,但无法完全消除所有可能的幻读情况。如果对于幻读问题有更严格的要求,可以考虑使用更高的隔离级别,如串行化,但需要注意可能带来的性能开销。
注:
在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。
那什么又是 Read View 呢?
Read View 是一个数据库的内部快照,该快照被用于 InnoDB 存储引擎中的 MVCC 机制,它保存着数据库某个时刻的数据信息。
Read View 会根据事务的隔离级别决定在某个事务开始时,该事务能看到什么信息。
注意,执行开始事务命令,并不等于启动了事务。在 MySQL 有两种开启事务的命令,分别是:
这两种开启事务的命令,事务的启动时机是不同的:
上面提到了 MVCC 机制,接下来我们来具体了解一下什么是 MVCC 机制,以及 Read View 在 MVCC 里是如何工作的。
MVCC ( Multi-Version Concurrency Control )是一种数据库管理系统中用于处理并发事务的机制。它的原理是通过为每个事务创建数据的多个版本,以允许多个事务同时并发执行而不会相互干扰。
MVCC 的作用是确保事务的隔离性,防止并发事务中的读取和写入冲突,从而维护数据库的一致性。
MVCC 的基本原理包括以下几个关键点:
MVCC 的作用:
在 MVCC 中,每行数据都有一个版本号或时间戳,表示该行数据的创建时间或最后修改时间。每个事务在开始时会创建一个 "Read View"(读视图),该视图定义了该事务所能看到的数据版本范围。
Read View 是事务用于确定其可见数据版本范围的一种重要数据结构。Read View 记录了事务开始时的全局事务标识,以及其他用于决定可见数据范围的信息。
Read View 的组成结构大致如下图所示:
各个字段含义,如下所示:
活跃事务
的 ID 。活跃事务
是指已经启动但尚未提交的事务。Read View 需要跟踪这些活跃事务,以确保读取数据时可以考虑它们的未提交修改。总的来说,Read View 通过记录创建者事务 ID 、活跃事务 ID 列表、最小事务 ID 和最大事务 ID ,提供了一个一致性的快照视图,以支持多版本并发控制。
这有助于确保事务在读取数据时能够看到一致性的状态,同时处理活跃事务的未提交修改。
接下我们就通过下面这个例子,来进一步了解一下这些字段是如何被使用的。
假设我们有一张账户余额表,小明在其中有中一条余额为 50 万的记录,如下图所示:
![隐藏列.png](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
其中 trx_id 、roll_pointer 为隐藏列,对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含这两个隐藏列。
这两个隐藏列的存在使得 InnoDB 能够实现多版本并发控制( MVCC ),从而提供了事务的隔离性和一致性。通过这些信息,InnoDB 可以追踪记录的修改历史,支持事务的回滚操作,以及在并发读取时提供一致的快照视图。这对于数据库引擎的性能和并发控制来说是至关重要的。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况,如下图所示:
这就是 MVCC 的核心原理,这样确保了 Read View 对事务的隔离性和一致性。活跃事务(未提交的事务)对于其他事务是不可见的,已提交事务则是可见的。
读已提交是在每次读取数据时,都会生成一个新的 Read View 。
也就是说,在事务执行期间,多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能在这期间另外一个事务修改了该记录,并提交了事务。
我们以上面小明的例子来举例:
假设现在有两个事务,事务 A (事务 id 为 11 )启动后,紧接着事务 B (事务 id 为 12 )也启动了,两个事务的运行顺序如下所示:
前两次事务 B 读取数据时创建的 Read View 如下图所示:
事务 B 第一次读取数据:
![事务 B 创建的 ReadView](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
事务 A 修改数据:
事务 B 第二次读取数据:
从上面的 3 张图我们可以知道:
那为什么事务 A 提交后,事务 B 就可以读到事务 A 修改的数据?
因为在事务 A 提交后,由于隔离级别是读提交,所以事务 B 在每次读数据的时候,会重新创建 Read View ,此时事务 B 第三次读取数据时创建的 Read View 如下:
![事务 B 第三次创建的 ReadView](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
从上图中我们可以知道,事务 B 在找到小明的这条记录时,会对比这条记录的 trx_id (值为 11 ),发现比事务 B 的 Read View 中的 min_trx_id (值为 12 )小,这意味着修改这条记录的事务早就在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。
通过上面的例子,我们知道了在读提交隔离级别下,事务每次读数据时都重新创建 Read View ,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
接下来我们就继续了解一下可重复读是如何避免这个问题的。
可重复读是启动事务时生成一个 Read View ,然后整个事务期间都在用这个 Read View 。
我们继续以上面小明的例子来举例:
假设现在有两个事务,事务 A (事务 id 为 11 )启动后,紧接着事务 B (事务 id 为 12 )也启动了,两个事务的运行顺序如下所示:
事务 A 和 事务 B 的 Read View 具体如下图所示:
从上图我们可以知道:
事务 B 第一次读小明的账户余额记录,在找到记录后,它会先看这条记录的 trx_id ,发现 trx_id (值为 10 )比事务 B 的 Read View 中的 min_trx_id (值为 11 )还小,这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见的。
事务 A ,将小明的余额修改为 100 万,但未提交事务,这时 MySQL 会记录相应的 undo log ,并以链表的方式串联起来,如下图所示:
因为事务 A 修改了记录,之前的记录就变成旧版本记录了,最新的 trx_id 是事务 A 的事务 id (值为 11 )。
事务 B 第二次去读取该记录,会去对比这条记录的 trx_id (值为 11 ),发现在 min_trx_id 和 max_trx_id 之间,且 trx_id 值是在 m_ids 范围内的,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链条往下找,直到找到 trx_id 小于事务 B 的 min_trx_id 值的第一条记录,所以事务 B 读取到的是 trx_id 为 10 的记录,也就是小明余额是 50 万的这条记录。
最后,当事务 A 提交事务后,由于隔离级别为可重复读,所以事务 B 再次读取记录时,还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。
所以,即使事务 A 将小明余额修改为 100 万并提交了事务,事务 B 第三次读取记录时,读到的记录仍然是小明的余额是 50 万的这条记录。
通过上面两个例子,我们清楚地了解了 MySQL 是如何实现的读已提交和可重复读隔离级别。虽然可重复读隔离级别在很大程度上解决了幻读问题,但并不能完全消除幻读的问题。在特定情况下,依然可能出现由于插入或删除操作引起的幻读现象。
针对不同场景,MySQL 采用了两种不同的解决方案,具体如下:
额外知识点:
快照读( Snapshot Read ):
当前读( Current Read ):
通过上面的内容,我们了解了 MySQL 是如何通过事务隔离级别、MVCC 等机制来确保数据的一致性。其中还涉及到了 MySQL 的锁机制、事务日志等,由于篇幅限制,这些方面的细节暂未详细展开。在后续的文章中,我们将继续深入探讨这些内容。
总体而言,MySQL 在保障数据完整性和一致性方面的设计和实现,为开发者提供了强大的能力,让他们能够构建稳定、可靠的应用程序。通过合理选择隔离级别、优化查询语句以及合理设计数据库结构,开发者可以更好地发挥 MySQL 的能力,确保数据在复杂的多用户并发操作中能够保持一致性,从而为用户提供更好的使用体验。
原文链接:解密 MySQL:MySQL 是如何确保数据完整性和一致性的?
微信公众号:啊杰在拱趴
求关注!!!!
图片补充: 隐藏列:
事务 B 第一次读取数据:
事务 B 第三次创建的 ReadView:
1
littlewing 309 天前
提个建议哈,那些图不要截图,直接导出 jpg png 之类的
|
2
victoriamerrick 309 天前
楼主是福州人吗
|
3
kuituosi 309 天前
好笨啊,自己不思考网上随便抄的文章,但是你不觉得标题怪怪的吗?
既然 mysql 保证了一致性为什么又提供了隔离级别这种东西? 隔离级别的作用到底是干什么的? |
4
taogen 309 天前
先翻在看。翻到最后,不出意外,果然有公众号
|
5
xwayway 309 天前 via iPhone
卷到这里来了吗
|
6
Betsy 309 天前 via iPhone
整理的蛮详细哈,好奇 OP 有没有自己实现过这套机制?
|
7
jefferyJQ 309 天前
|
9
th3ee9ine OP @littlewing 好的,下次我注意一下
|
10
th3ee9ine OP @victoriamerrick 是的
|
14
JackSlowFcck 309 天前
@taogen 所以,直接看评论,啊哈哈哈哈
|