【眼见为实】自己动手实践理解READ COMMITTED && MVCC
[READ COMMITTED]
首先设置数据库隔离级别为读已提交(READ COMMITTED):
set global transaction isolation level READ COMMITTED ;
set session transaction isolation level READ COMMITTED ;
[READ COMMITTED]能解决的问题
我们来看一下为什么[READ COMMITTED]如何解决脏读的问题:
事务1:
START TRANSACTION;
① UPDATE users SET state=1 WHERE id=1;
② SELECT sleep(10);
ROLLBACK;
事务2:
START TRANSACTION;
① SELECT * FROM users WHERE id=1;
COMMIT;
事务1先于事务2执行。
事务1的执行信息:
[SQL 1]START TRANSACTION;
受影响的行: 0
时间: 0.001s
[SQL 2]
UPDATE users SET state=1 WHERE id=1;
受影响的行: 1
时间: 0.001s
[SQL 3]
SELECT sleep(10);
受影响的行: 0
时间: 10.000s
[SQL 4]
ROLLBACK;
受影响的行: 0
时间: 0.051s
事务2的执行信息:
[SQL 1]START TRANSACTION;
受影响的行: 0
时间: 0.001s
[SQL 2]
SELECT * FROM users WHERE id=1;
受影响的行: 0
时间: 0.005s
[SQL 3]
COMMIT;
受影响的行: 0
时间: 0.001s
最终结果:

结论:
读已提交[READ COMMITTED]隔离级别可以解决脏读的问题,但是貌似不是按照二级封锁协议解决的脏读问题。
分析:
因为读已提交[READ COMMITTED]隔离级别对应数据库的二级封锁协议。二级封锁协议在修改数据之前对其加X锁,直到事务结束释放X锁。读数据之前必须加S锁,读完即可释放S锁。因为事务1先执行修改,修改前申请持有X锁,事务结束释放X锁。持锁时间段为[SQL 2]开始前到[SQL 4]结束,持锁时间大约为10.056s。事务2在事务1之后进行读操作,按照二级封锁协议所说,事务2在读数据之前会申请持有S锁。但是事务1持有此数据的X锁,所以事务2必须等待事务1释放X锁,这个过程大约在10秒左右。但是我们通过事务2的执行信息可以看到执行查询的时间为0.005s,远远小于10秒。所以我们可以大胆推断Mysql的InnoDB引擎在[READ COMMITTED]隔离级别下对读操作没有加锁。但是[READ COMMITTED]隔离级别确实解决了脏读的问题,那么Mysql是怎么解决的脏读问题呢?
MVCC(多版本并发控制)
答案是多版本并发控制(MVCC),可以认为是行级锁的一个变种,但是它在很多情况下都避免了加锁操作,因此开销更低。实现了非堵塞的读操作,写操作也只需要锁定必要的行。
如果我们理解了MVCC的工作机制,也就可以理解[READ COMMITTED]隔离级别是如何解决脏读问题的。
MVCC具体是如下操作的:
SELECT
InnoDB会根据以下两个条件检查记录:
①InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的版本号小于或是等于事务的系统版本 号),这样可以确保数据读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或修改过的。
②行的删除版本号要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
只有符合上述两个条件的记录,才能返回作为查询结果。
INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE
InnoDB为新插入的每一行保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
Innodb为每行记录都实现了三个隐藏字段:

6字节的事务ID(DB_TRX_ID)
7字节的回滚指针(DB_ROLL_PTR)
隐藏的ID 6字节的事物ID用来标识该行所述的事务
事务1会执行如下操作:
①用排他锁锁定该行
②记录redo log
③把该行修改前的值Copy到undo log,即上图中下面的行
④修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行

如果事务1最后执行COMMIT操作,则什么操作都不用做。如果执行ROLLBACK操作,则需要通过回滚指针从undo log中还原修改前的数据。
read view 判断当前版本数据项是否可见
在InnoDB中,创建一个新事务的时候,InnoDB会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,InnoDB会将该行当前的版本号与该read view进行比较。
具体的算法如下:
设该行的当前事务id为trx_id,read view中最早的事务id为trx_id_min, 最迟的事务id为trx_id_max。
- 如果trx_id< trx_id_min的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。
- 如果trx_id>trx_id_max的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见。
- 如果trx_id_min <= trx_id <= trx_id_max, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从trx_id_min到trx_id_max进行遍历,如果trx_id等于他们之中的某个事务id的话,那么不可见。
从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号的数据,将该可见行的值返回。
需要注意的是,新建事务(当前事务)与正在内存中commit 的事务不在活跃事务链表中。
对应源代码如下:
函数:read_view_sees_trx_id。
read_view中保存了当前全局的事务的范围:
【low_limit_id, up_limit_id】
1. 当行记录的事务ID小于当前系统的最小活动id,就是可见的。
if (trx_id < view->up_limit_id) {
return(TRUE);
}
2. 当行记录的事务ID大于当前系统的最大活动id(也就是尚未分配的下一个事务的id),就是不可见的。
if (trx_id >= view->low_limit_id) {
return(FALSE);
}
3. 当行记录的事务ID在活动范围之中时,判断是否在活动链表中,如果在就不可见,如果不在就是可见的。
for (i = 0; i < n_ids; i++) {
trx_id_t view_trx_id
= read_view_get_nth_trx_id(view, n_ids - i - 1);
if (trx_id <= view_trx_id) {
return(trx_id != view_trx_id);
}
}
事务2会执行如下操作:
理想状态下,事务1的事务id=1,事务2的事务id=2。因为事务2执行时查询时,事务1正处于等待状态。所以read view为{1},事务2读取的数据行 trx_id=1,read view中最早的事务id为trx_id_min=1, 最迟的事务id为trx_id_max=1。因为trx_id_min <= trx_id <= trx_id_max,并且trx_id_min = trx_id = trx_id_max,说明该行记录所在事务在本次新事务创建的时候处于活动状态,不可见。所以从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号的数据,将该可见行的值返回。所以不会出现脏读的现象。
[READ COMMITTED]不能解决的问题
[READ COMMITTED]隔离级别解决不了不可重复读的问题,一个事务中两次读取可能会出现不同的结果。
我们来模拟一下:
事务1:
START TRANSACTION;
① SELECT sleep(5);
② UPDATE users SET state=1 WHERE id=1;
COMMIT;
事务2:
START TRANSACTION;
① SELECT * FROM users WHERE id=1;
② SELECT sleep(10);
③ SELECT * FROM users WHERE id=1;
COMMIT;
事务1先于事务2执行。
执行结果:


结论:
读已提交[READ COMMITTED]隔离级别不能解决不可重复读的问题,但是如果按照上面所说,Mysql的InnoDB引擎是通过read view来判断当前版本数据项是否可见的。那么读已提交[READ COMMITTED]隔离级别下应该也不会出现不可重复读的问题,但是现实并不是。
分析:
读已提交[READ COMMITTED]隔离级别下出现不可重复读是由于read view的生成机制造成的。在[READ COMMITTED]级别下,只要当前语句执行前已经提交的数据都是可见的。在每次语句执行的过程中,都关闭read view, 重新创建当前的一份read view。这样就可以根据当前的全局事务链表创建read view的事务区间。
那么在我们模拟的事务中,事务1的事务id trx_id1=1,事务2的事务id trx_id2=2。假设事务2第一次读取数据前的此行数据的事务id=0。事务2中语句①执行前生成的read view为{1},trx_id_min=1,trx_id_max=1。因为trx_id(0)< trx_id_max(1),此行数据对本次事务可见,将该可见行的值state=0返回。语句①执行后等待10秒,第5秒时事务1对数据加X锁进行修改操作0->1,然后提交事务释放锁。语句②执行前生成的read view为{null},说明当前系统中的不存在其他的活跃事务,也就不存在不应该被本事务看到的其他事务,因此该行记录的当前值state=1可见。就出现两次读取数据不一致的问题,也就是不可重复读。
不可重复读的问题在Mysql默认的隔离级别[REPEATABLE READ]中得到了解决。至于是如何解决的,先卖个关子。可以给个小提示,也是和read view的生成机制有关。预知后事如何,请看下篇博客。
本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。
如果您认为还不错,不妨点击一下下方的[【推荐】](javascript:void(0)
【眼见为实】自己动手实践理解READ COMMITTED && MVCC的更多相关文章
- 【眼见为实】自己动手实践理解数据库READ COMMITTED && MVCC
[READ COMMITTED] 首先设置数据库隔离级别为读已提交(READ COMMITTED): set global transaction isolation level READ COMMI ...
- 【眼见为实】自己动手实践理解REPEATABLE READ && Next-Key Lock
首先设置数据库隔离级别为可重复读(REPEATABLE READ): set global transaction isolation level REPEATABLE READ ; set sess ...
- 【眼见为实】自己动手实践理解数据库REPEATABLE READ && Next-Key Lock
[REPEATABLE READ] 首先设置数据库隔离级别为可重复读(REPEATABLE READ): set global transaction isolation level REPEATAB ...
- 【眼见为实】自己动手实践理解数据库READ UNCOMMITED && SERIALIZABLE
目录 准备工作 ①准备测试表和测试数据 ②关闭数据库事务自动提交 ③设置InnoDB存储引擎隔离级别 [READ UNCOMMITTED] [READ UNCOMMITTED]能解决的问题 [READ ...
- 【原创 Hadoop&Spark 动手实践 8】Spark 应用经验、调优与动手实践
[原创 Hadoop&Spark 动手实践 7]Spark 应用经验.调优与动手实践 目标: 1. 了解Spark 应用经验与调优的理论与方法,如果遇到Spark调优的事情,有理论思考框架. ...
- 【原创 Hadoop&Spark 动手实践 9】Spark SQL 程序设计基础与动手实践(上)
[原创 Hadoop&Spark 动手实践 9]SparkSQL程序设计基础与动手实践(上) 目标: 1. 理解Spark SQL最基础的原理 2. 可以使用Spark SQL完成一些简单的数 ...
- 【原创 Hadoop&Spark 动手实践 10】Spark SQL 程序设计基础与动手实践(下)
[原创 Hadoop&Spark 动手实践 10]Spark SQL 程序设计基础与动手实践(下) 目标: 1. 深入理解Spark SQL 程序设计的原理 2. 通过简单的命令来验证Spar ...
- 【原创 Hadoop&Spark 动手实践 6】Spark 编程实例与案例演示
[原创 Hadoop&Spark 动手实践 6]Spark 编程实例与案例演示 Spark 编程实例和简易电影分析系统的编写 目标: 1. 掌握理论:了解Spark编程的理论基础 2. 搭建 ...
- 【原创 Hadoop&Spark 动手实践 7】Spark 计算引擎剖析与动手实践
[原创 Hadoop&Spark 动手实践 7]Spark计算引擎剖析与动手实践 目标: 1. 理解Spark计算引擎的理论知识 2. 动手实践更深入的理解Spark计算引擎的细节 3. 通过 ...
随机推荐
- Docker(一):Docker入门教程
如今Docker的使用已经非常普遍,特别在一线互联网公司.使用Docker技术可以帮助企业快速水平扩展服务,从而到达弹性部署业务的能力.在云服务概念兴起之后,Docker的使用场景和范围进一步发展,如 ...
- AndFix
AndFix 是阿里巴巴开源的 Android 应用热修复工具,帮助 Anroid 开发者修复应用的线上问题.Andfix 是 "Android hot-fix" 的缩写.支持 A ...
- HI3531的DDR3配置流程
DDR3 初始化配置流程 系统上电之后,必须先完成DDR3 SDRAM 的初始化操作,系统才能访问DDR3 SDRAM.在进行初始化之前需要注意以下几点: 对DDR3 SDRAM 进行上电操作时,需要 ...
- Nginx负载均衡和反向代理的配置和优化
负载均衡 负载均衡是由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位, 反向代理 是指以代理服务器来接受internet上的请求,然后将请求转给内部的服务器 常见的负载均衡 1. ...
- directX枚举系统设备类
void CSysEnumDlg::DisplayFullCategorySet(void){ USES_CONVERSION; HRESULT hr; IEnumMoniker * ...
- 初识 systemd
从 init 系统说起 linux 操作系统的启动首先从 BIOS 开始,接下来进入 boot loader,由 bootloader 载入内核,进行内核初始化.内核初始化的最后一步就是启动 PID ...
- css样式--表格
1.示例源码 <!DOCTYPE html><html><head><meta charset="utf-8"> <title ...
- 你需要了解的高可用方案之使用keepalived搭建双机热备一览
在之前一篇使用nginx搭建高可用的解决方案的时候,很多同学会问,如果nginx挂掉怎么办,比如下面这张图: 你可以清楚的看到,如果192.168.2.100这台机器挂掉了,那么整个集群就下线了,这个 ...
- [ZJOI2014]力
推公式发现(这不是水题吗,这要推吗) \[E_i=\Sigma^{i-1}_{j=1} \frac{q_j}{(i-j)^2} - \Sigma^{n}_{j=i+1} \frac{q_j}{(i-j ...
- ES2015 类中的静态方法
在ES2015中,终于不用用函数原型来实现类系统,可以直接使用关键字class,下面是对class的静态属性的研究: 举例:一个Node类,每一个Node类之间都可以建立从属关系,每一个Node实例下 ...