您好,欢迎访问一九零五行业门户网

一文详解MySQL中的事务和 MVCC 原理

本篇文章带大家了解一下mysql中的事务,并介绍一下mvcc 原理,希望能够给大家提供帮助!
01 什么是事务?数据库事务指的是一组数据操作,事务内的操作要么就是全部成功,要么就是全部失败,什么都不做,其实不是没做,是可能做了一部分但是只要有一步失败,就要回滚所有操作,有点一不做二不休的意思。
在 mysql 中,事务支持是在引擎层实现的。mysql 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 mysql 原生的 myisam 引擎就不支持事务,这也是 myisam 被 innodb 取代的重要原因之一。
1.1 四大特性
原子性(atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。一致性(consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如 a 向 b 转账,不可能 a 扣了钱,b 却没收到。隔离性(isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如 a 正在从一张银行卡中取钱,在 a 取钱的过程结束前,b 不能向这张卡转账。持久性(durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。1.2 隔离级别
sql 事务的四大特性中原子性、一致性、持久性都比较好理解。但事务的隔离级别确实比较难的,今天主要聊聊 mysql 事务的隔离性。
sql 标准的事务隔离从低到高级别依次是:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。级别越高,效率越低。
读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。读提交:一个事务提交之后,它做的变更才会被其他事务看到。可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。串行化:顾名思义是对于同一行记录,“写” 会加 “写锁”,“读” 会加 “读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。所以种隔离级别下所有的数据是最稳定的,但是性能也是最差的。1.3 解决的并发问题
sql 事务隔离级别的设计就是为了能最大限度的解决并发问题:
脏读:事务 a 读取了事务 b 更新的数据,然后 b 回滚操作,那么 a 读取到的数据是脏数据不可重复读:事务 a 多次读取同一数据,事务 b 在事务 a 多次读取的过程中,对数据作了更新并提交,导致事务 a 多次读取同一数据时,结果不一致。幻读:系统管理员 a 将数据库中所有学生的成绩从具体分数改为 abcde 等级,但是系统管理员 b 就在这个时候插入了一条具体分数的记录,当系统管理员 a 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。sql 不同的事务隔离级别能解决的并发问题也不一样,如下表所示:只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。
事务隔离级别脏读不可重复读幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
串行化 不可能 不可能 不可能
ps:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
1.4 举个栗子
这么说可能有点难以理解,举个栗子。还是之前的表结构以及表数据
create table `student`  (  `id` int(11) not null auto_increment,  `name` varchar(100) character set utf8 collate utf8_general_ci not null,  `age` int(11) null default null,  primary key (`id`) using btree) engine = innodb auto_increment = 66 character set = utf8 collate = utf8_general_ci row_format = compact;
假设现在,我要同时启动两个食物,一个事务 a 查询 id = 2 的学生的 age,一个事务 b 更新 id = 2 的学生的 age。流程如下,在四种隔离级别下的 x1、x2、x3 的值分别是怎样的呢?
读未提交:x1 的值是 23,因为事务 b 虽然没提交但它的更改已被 a 看到。(如果 b 后面又回滚了 x1 的值就是脏的)。x2、x3 的值也是 23,这无可厚非。读已提交:x1 的值是 22,因为 b 虽然改了,但 a 看不到。(如果 b 后面回滚了,x1 的值不变,解决了脏读),x2、x3 的值是 23,没毛病,b 提交了,a 才能看到。可重复读:x1、x2 都是 22,a 开启的时刻值是 22,那么在 a 的整个过程中,它的值都是 22。(不管 b 在这期间怎么修改,只要 a 还没提交,都是看不见的,解决了不可重复读),而 x3 的值是 23,因为 a 提交了,能看到 b 修改的值了。串行化:b 在执行更改期间会被锁住,直至 a 提交。b 才能继续执行。(a 在读期间,b 不能写。得保证此时数据是最新的。解决了幻读)所以 x1、x2 都是 22,而最后的 x3 在 b 提交之后执行,它的值就是 23。那为什么会出现这样的结果呢?事务隔离级别到底是怎么实现的呢?
事务隔离级别是怎么是实现的呢?我在极客时间丁奇老师的课上找到了答案:
实际上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在 “可重复读” 隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在 “读提交” 隔离级别下,这个视图是在每个 sql 语句开始执行的时候创建的。这里需要注意的是,“读未提交” 隔离级别下直接返回记录上的最新值,没有视图概念;而 “串行化” 隔离级别下直接用加锁的方式来避免并行访问。1.5 设置事务隔离级别
不同的数据库默认设置的事务隔离级别也大不一样,oracle 数据库的默认隔离级别是读提交,而 mysql 是可重复读。所以,当你的系统需要把数据库从 oracle 迁移到 mysql 时,请把级别设置成与搬迁之前的(读提交)一致,避免出现不可预测的问题。
1.5.1 查看事务隔离级别
# 查看事务隔离级别5.7.20 之前select @@transaction_isolationshow variables like 'transaction_isolation';# 5.7.20 以及之后select @@tx_isolationshow variables like 'tx_isolation'+---------------+-----------------+| variable_name | value           |+---------------+-----------------+| tx_isolation  | repeatable-read |+---------------+-----------------+
1.5.2 设置隔离级别
修改隔离级别语句格式是:set [作用域] transaction isolation level [事务隔离级别]
其中作用域可选:session(会话)、global(全局);隔离级别就是上面提到的 4 种,不区分大小写。
例如:设置全局隔离级别为读提交
set global transaction isolation level read committed;
1.6 事务的启动
mysql 的事务启动有以下几种方式:
显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,或者回滚语句是 rollback。# 更新学生名字start transaction;update student set name = '张三' where id = 2;commit;
set autocommit = 0,这个命令会将线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。set autocommit = 1,表示 mysql 自动开启和提交事务。 比如执行一个 update 语句,语句只完成后就自动提交了。不需要显示的使用 begin、commit 来开启和提交事务。所以当我们执行多个语句的时候,就需要手动的用 begin、commit 来开启和提交事务。start transaction with consistent snapshot;上面提到的 begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 innodb 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 命令。 第一种启动方式,一致性视图是在执行第一个快照读语句时创建的; 第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。02 事务隔离的实现理解了隔离级别,那事务的隔离是怎么实现的呢?要想理解事务隔离,先得了解 mvcc 多版本的并发控制这个概念。而 mvcc 又依赖于 undo log 和 read view 实现。
2.1 什么是 mvcc?
百度上的解释是这样的:
mvcc,全称 multi-version concurrency control,即多版本并发控制。mvcc 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。mvcc 使得数据库读不会对数据加锁,普通的 select 请求不会加锁,提高了数据库的并发处理能力;数据库写才会加锁。 借助 mvcc,数据库可以实现 read committed,repeatable read 等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了 acid 中的 i 特性(隔离性)。
mvcc 只在 repeatable read 和 read commitied 两个隔离级别下工作。其他两个隔离级别都和 mvcc 不兼容 ,因为 read uncommitied 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 serializable 则会对所有读取的行都加锁。2.1.1 inndb 中的 mvcc
inndb 中每个事务都有一个唯一的事务 id,记为 transaction_id。它在事务开始时向 inndb 申请,按照时间先后严格递增。
而每行数据其实都有多个版本,这就依赖 undo log 来实现了。每次事务更新数据就会生成一个新的数据版本,并把  transaction_id 记为 row trx_id。同时旧的数据版本会保留在 undo log 中,而且新的版本会记录旧版本的回滚指针,通过它直接拿到上一个版本。
所以,inndb 中的 mvcc 其实是通过在每行记录后面保存两个隐藏的列来实现的。一列是事务 id:trx_id;另一列是回滚指针:roll_pt。
2.2 undo log
回滚日志保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(mvcc),也即非锁定读。
根据操作的不同,undo log 分为两种: insert undo log 和 update undo log。
2.2.1 insert undo log
insert 操作产生的 undo log,因为 insert 操作记录没有历史版本只对当前事务本身可见,对于其他事务此记录不可见,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作。
purge 的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收 undo pages所以,插入数据时。它的初始状态是这样的:
2.2.2 update undo log
update 和 delete 操作产生的 undo log 都属于同一类型:update_undo。(update 可以视为 insert 新数据到原位置,delete 旧数据,undo log 暂时保留旧数据)。
事务提交时放到 history list 上,没有事务要用到这些回滚日志,即系统中没有比这个回滚日志更早的版本时,purge 线程将进行最后的删除操作。
一个事务修改当前数据:
另一个事务修改数据:
这样的同一条记录在数据库中存在多个版本,就是上面提到的多版本并发控制 mvcc。
另外,借助 undo log 通过回滚可以回到上一个版本状态。比如要回到 v1 只需要顺序执行两次回滚即可。
2.3 read-view
read view 是 inndb 在实现 mvcc 时用到的一致性读视图,用于支持 rc(读提交)以及 rr(可重复读)隔离级别的实现。
read view 不是真实存在的,只是一个概念,undo log 才是它的体现。它主要是通过版本和 undolog 计算出来的。作用是决定事务能看到哪些数据。
每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。
2.3.1 数据版本的可见性规则
read view 中主要包含当前系统中还有哪些活跃的读写事务,在实现上 inndb 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正活跃(还未提交)的事务。
前面说了事务 id 随时间严格递增的,把系统中已提交的事务 id 的最大值记为数组的低水位,已创建过的事务 id + 1记为高水位。
这个视图数组和高水位就组成了当前事务的一致性视图(read view)
这个数组画个图,长这样:
规则如下:
1 如果 trx_id 在灰色区域,表明被访问版本的 trx_id 小于数组中低水位的 id 值,也即生成该版本的事务在生成 read view 前已经提交,所以该版本可见,可以被当前事务访问。2 如果 trx_id 在橙色区域,表明被访问版本的 trx_id 大于数组中高水位的 id 值,也即生成该版本的事务在生成 read view 后才生成,所以该版本不可见,不能被当前事务访问。3 如果在绿色区域,就会有两种情况:
a) trx_id 在数组中,证明这个版本是由还未提交的事务生成的,不可见b) trx_id 不在数组中,证明这个版本是由已提交的事务生成的,可见第三点我在看教程的时候也有点疑惑,好在有热心网友解答:
落在绿色区域意味着是事务 id 在低水位和高水位这个范围里面,而真正是否可见,看绿色区域是否有这个值。如果绿色区域没有这个事务 id,则可见,如果有,则不可见。在这个范围里面并不意味着这个范围就有这个值,比如 [1,2,3,5],4 在这个数组 1-5 的范围里,却没在这个数组里面。这样说可能有点难以理解,我假设一个场景:三个事务对同一条数据进行查询更新等操作,为此画了张图以方便理解:
原始数据还是下图这样的,对 id = 2 的张三进行信息的更新:
针对上图,我想提个问题。分别在 rc(读提交)以及 rr(可重复读)隔离级别下,t4 和 t5 时间点的查询 age 值分别是多少呢?t4 更新的值又是多少呢?思考片刻,相信大家都有自己的答案。答案在文末,希望大家能带着自己的疑问继续读下去。
2.3.2 rr(可重复读)下的结果
rr 级别下,查询只承认在事务启动前就已经提交完成的数据,一旦启动事务就会建视图。所以使用 start transaction with consistent snapshot 命令,马上就会建视图。
现在假设:
事务 a 开始前,只有一个活跃的事务,id = 2,已提交的事务也就是插入数据的事务 id = 1事务 a、b、c 的事务 id 分别是 3、4、5在这种隔离级别下,他们创建视图的时刻如下:
根据上图得,事务 a 的视图数组是[2,3];事务 b 的视图数组是 [2,3,4];事务 c 的视图数组是[2,3,4,5]。分析一波:
t4 时刻,b 读数据都是从当前版本读起,过程是这样的:
读到当前版本的 trx_id = 4,刚好是自己,可见所以 age = 24t5 时刻,a 读数据都是从当前版本读起,过程是这样的:
读到当前版本的 trx_id = 4,比自己视图数组的高水位大,不可见再往上读到 trx_id = 5,比自己视图数组高水位大,不可见再往上读到 trx_id = 1,比自己视图数组低水位小,可见所以 age = 22这样执行下来,虽然期间这一行数据被修改过,但是事务 a 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。
其实视图是否可见主要看创建视图和提交的时机,总结下规律:
版本未提交,不可见版本已提交,但在视图创建后提交,不可见版本已提交,但在视图创建前提交,可见2.3.2.1 快照读和当前读事务 b 的 update 语句,如果按照上图的一致性读,好像结果不大对?
如下图周明,b 的视图数组是先生成的,之后事务 c 才提交。那就应该看不见 c 修改的 age = 23 呀?最后 b 怎么得出 24 了?
没错,如果 b 在更新之前执行查询语句,那返回的结果肯定是 age = 22。问题是更新就不能在历史版本更新了呀,否则 c 的更新不就丢失了?
所以,更新有个规则:更新数据都是先读后写(读是更新语句执行,不是我们手动执行),读的就是当前版本的值,叫当前读;而我们普通的查询语句就叫快照读。
因此,在更新时,当前读读到的是 age = 23,更新之后就成 24 啦。
2.3.2.2 select 当前读除了更新语句,查询语句如果加锁也是当前读。如果把事务 a 的查询语句 select age from t where id = 2 改一下,加上锁(lock in mode 或者 for update),也都可以得到当前版本 4 返回的 age = 24
下面就是加了锁的 select 语句:
select age from t where id = 2 lock in mode; select age from t where id = 2 for update;
2.3.2.3 事务 c 不马上提交假设事务 c 不马上提交,但是 age = 23 版本已生成。事务 b 的更新将会怎么走呢?
事务 c 还没提交,写锁还没释放,但是事务 b 的更新必须要当前读且必须加锁。所以事务 b 就阻塞了,必须等到事务 c 提交,释放锁才能继续当前的读。
2.3.3 rc(读提交)下的结果
在读提交隔离级别下,查询只承认在语句启动前就已经提交完成的数据;每一个语句执行之前都会重新算出一个新的视图。
注意:在上图的表格中用于启动事务的是 start transaction with consistent snapshot 命令,它会创建一个持续整个事务的视图。所以,在  rc 级别下,这命令其实不起作用。等效于普通的 start transaction(在执行 sql 语句之前才算是启动了事务)。所以,事务 b 的更新其实是在事务 c 之后的,它还没真正启动事务,而 c 已提交。
现在假设:
事务 a 开始前,只有一个活跃的事务,id = 2,已提交的事务也就是插入数据的事务 id = 1事务 a、b、c 的事务 id 分别是 3、4、5在这种隔离级别下,他们创建视图的时刻如下:
根据上图得,事务 a 的视图数组是[2,3,4],但它的高水位是 6或者更大(已创建事务 id + 1);事务 b 的视图数组是 [2,4];事务 c 的视图数组是 [2,5]。分析一波:
t4 时刻,b 读数据都是从当前版本读起,过程是这样的:
读到当前版本的 trx_id = 4,刚好是自己,可见所以 age = 24t5 时刻,a 读数据都是从当前版本读起,过程是这样的:
读到当前版本的 trx_id = 4,在自己一致性视图范围内但包含 4,不可见再往上读到 trx_id = 5,在自己一致性视图范围内但不包含 5,可见所以 age = 2303巨人的肩膀cnblogs.com/wyaokai/p/10921323.htmltime.geekbang.org/column/article/70562zhuanlan.zhihu.com/p/117476959cnblogs.com/xd502djj/p/6668632.htmlblog.csdn.net/article/details/109044141blog.csdn.net/u014078930/article/details/9965927204 总结本文详细聊了事务的方方面面,比如:四大特性、隔离级别、解决的并发问题、如何设置、查看隔离级别、如何启动事务等。除此以外,还深入了解了 rr 和 rc 两个级别的隔离是怎么实现的?包括详解 mvcc、undo log 和 read view 是怎么配合实现 mvcc 的。最后还聊了快照读、当前读等等。可以说,事务相关的知识点都在这了。看完这一篇还不懂的话,你来捶我呀!
【相关推荐:mysql视频教程】
以上就是一文详解mysql中的事务和 mvcc 原理的详细内容。
其它类似信息

推荐信息