MySQL是如何实现可重复读的?
简单理解一下可重复读
可重复读是指:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
我们可以简单理解为:在可重复读隔离级别下,事务在启动的时候就”拍了个快照“。注意,这个快照是基于整个库的。
这时,你可能就会想,如果一个库有 100G,那么我启动一个事务,MySQL就要拷贝 100G 的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。
实际上,我们并不需要拷贝出这 100G 的数据。我们来看下”快照“是怎么实现的。
拍个快照
InnoDB 里面每个事务都有一个唯一的事务 ID,叫作 transaction id。它在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
每条记录在更新的时候都会同时记录一条 undo log,这条 log 就会记录上当前事务的 transaction id,记为 row trx_id。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
如下图所示,一行记录被多个事务更新之后,最新值为 k=22。假设事务A在 trx_id=15 这个事务提交后启动,事务A 要读取该行时,就通过 undo log,计算出该事务启动瞬间该行的值为 k=10。
在可重复读隔离级别下,一个事务在启动时,InnoDB 会为事务构造一个数组,用来保存这个事务启动瞬间,当前正在”活跃“的所有事务ID。”活跃“指的是,启动了但还没提交。
数组里面事务 ID 为最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
这个视图数组把所有的 row trx_id 分成了几种不同的情况。
- 如果 trx_id 小于低水位,表示这个版本在事务启动前已经提交,可见;
- 如果 trx_id 大于高水为,表示这个版本在事务启动后生成,不可见;
- 如果 trx_id 大于低水位,小于高水位,分为两种情况:
- 若 trx_id 在数组中,表示这个版本在事务启动时还未提交,不可见;
- 若 trx_id 不在数组中,表示这个版本在事务启动时已经提交,可见。
InnoDB 就是利用 undo log 和 trx_id 的配合,实现了事务启动瞬间”秒级创建快照“的能力。
举个栗子
初始化语句
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
下表为事务A, B, C 的执行流程
事务A |
事务B |
事务C |
---|---|---|
START TRANSACTION WITH CONSISTENT SNAPSHOT; |
||
START TRANSACTION WITH CONSISTENT SNAPSHOT; |
||
UPDATE t SET k=k+1 WHERE id=1; |
||
UPDATE t SET k=k+1 WHERE id=1; |
||
SELECT k FROM t WHERE id=1; |
||
SELECT k FROM t WHERE id=1; |
||
COMMIT; |
||
COMMIT; |
我们假设事务A, B, C 的 trx_id 分别为 100, 101, 102。事务A开始前活跃的事务 ID 只有 99,并且 id=1 这一行数据的 trx_id=90。
根据假设,我们得出事务启动瞬间的视图数组:事务A:[99, 100],事务B:[99, 100, 101],事务C:[99, 100, 101, 102]。
- 事务C通过更新语句,把 k 更新为 2,此时trx_id=102;
- 事务B通过更新语句,把 k 更新为 3,此时trx_id=101;
- 事务B通过查询语句,查询到最新一条记录为3,trx_id=101,满足隔离条件,返回 k=3;
- 事务A通过查询语句:
- 查询到最新一条记录为3,trx_id=101,比高水位大,不可见;
- 通过 undo log,找到上一个历史版本,trx_id=102,比高水位大,不可见;
- 继续找上一个历史版本,trx_id=90,比低水位小,可见。
提出问题:为啥事务B更新的时候能看到事务C的修改?
我们假设事务B在更新的看不到事务C的修改,是什么个情况?
- 事务B查询到最新一条记录为2,trx_id=102,比高水位大,不可见;
- 通过 undo log,找到上一个版本,trx_id=90,比低水位小,可见;
- 返回记录 k=1,执行 k=k+1,把 k 更新为2,此时 trx_id=101。
如果是这种情况,事务C可能就蒙了:“啥子情况,我的更新怎么就丢了”。事务B覆盖了事务C的更新。
所以,InnoDB在更新时运用一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读“ (current read)。
因此,事务B在更新时要拿到最新的数据,在此基础上做更新。紧接着,事务B在读取的时候,查询到最新的记录为3, trx_id=101 为当前事务ID,可见。
我们再假设另一种情况:
事务B在更新之后,事务C紧接着更新,事务B回滚了,事务C成功提交。
事务B |
事务C |
---|---|
START TRANSACTION WITH CONSISTENT SNAPSHOT; |
|
START TRANSACTION WITH CONSISTENT SNAPSHOT; |
|
UPDATE t SET k=k+1 WHERE id=1; |
|
UPDATE t SET k=k+1 WHERE id=1; |
|
SELECT k FROM t WHERE id=1; |
|
ROLLBACK; |
|
COMMIT; |
如果按照当前读的定义,会发生以下事故,假设当前 K=1:
- 事务B把 k 更新为 2;
- 事务C读取到当前最新值,k=2,更新为3;
- 事务B回滚;
- 事务C提交。
这时候,事务C发现自己想要执行的是 +1 操作,结果变成了 ”+2“ 操作。
InnoDB 肯定不允许这种情况的发生,事务B在执行更新语句时,会给该行加上行锁,直到事务B结束,才会释放这个锁。
小结
- InnoDB 的行数据有多个版本,每个版本都有 row trx_id。
- 事务根据 undo log 和 trx_id 构建出满足当前隔离级别的一致性视图。
- 可重复读的核心是一致性读,而事务更新数据的时候,只能使用当前读,如果当前记录的行锁被其他事务占用,就需要进入锁等待。
参考
- 无限级分类(非递归算法/存储过程版/GUID主键)完整数据库示例_(3)删除记录
- 部署Zipkin分布式性能追踪日志系统的操作记录
- 无限级分类(非递归算法/存储过程版/GUID主键)完整数据库示例_(4)显示记录
- Android 2.x中使用actionbar - Actionbarsherlock (2)
- python读txt和xml
- 让Jexus支持高并发请求的优化技巧
- 数据压缩算法LZO (C#)
- Html之初体验
- 基于Wolfpack开发业务监控系统
- Android 2.x中使用actionbar - Actionbarsherlock
- Python-操作Memcache、Redis、RabbitMQ、
- 推荐[搜索引擎架构]的几篇文章
- 中小型商城系统中的分类/产品属性/扩展属性的数据库设计
- Linux下FTP虚拟账号环境部署总结
- MySQL 教程
- MySQL 安装
- MySQL 管理与配置
- MySQL PHP 语法
- MySQL 连接
- MySQL 创建数据库
- MySQL 删除数据库
- MySQL 选择数据库
- MySQL 数据类型
- MySQL 创建数据表
- MySQL 删除数据表
- MySQL 插入数据
- MySQL 查询数据
- MySQL where 子句
- MySQL UPDATE 查询
- MySQL DELETE 语句
- MySQL LIKE 子句
- mysql order by
- Mysql Join的使用
- MySQL NULL 值处理
- MySQL 正则表达式
- MySQL 事务
- MySQL ALTER命令
- MySQL 索引
- MySQL 临时表
- MySQL 复制表
- 查看MySQL 元数据
- MySQL 序列 AUTO_INCREMENT
- MySQL 处理重复数据
- MySQL 及 SQL 注入
- MySQL 导出数据
- MySQL 导入数据
- MYSQL 函数大全
- MySQL Group By 实例讲解
- MySQL Max()函数实例讲解
- mysql count函数实例
- MYSQL UNION和UNION ALL实例
- MySQL IN 用法
- MySQL between and 实例讲解
- C++雾中风景15:聊聊让人抓狂的Name Mangling
- 【Java】15 File 类
- seaborn更高效的统计图表制作工具
- 牛客2019跨年AK场题解(一)
- qiankun proxySand 沙箱
- MATLAB 与 C 语言的混合编程实战之辛普森积分法、自适应辛普森积分
- Java那些事之JDK环境配置及集成开发环境Eclipse安装
- 敲代码、作诗、写论文无所不能?史上最大AI模型GPT-3霸榜Github
- 数据结构实验——校园导游 实现最小生成树+最短路
- Salesforce LWC学习(二十六) 简单知识总结篇三
- 【Java】13 异常
- 【Java】14 多线程
- 【Java】16 字节流
- 【Java】17 字符流
- 极坐标系在数据可视化中的巧妙运用