MVCC原理实现
最近在整理笔记,发现mysql中有些概念及实现理解不透彻,所以本文旨在搞懂相关概念及实现。
此文基于InnoDB存储引擎分析。
查询会开启事务
InnoDB默认autocommit=ON(开启状态):
- autocommit=ON时:
- 没有手动begin或start transaction开启事务,mysql默认也会将用户的操作当做事务即时提交;
- 手动开启事务 begin, 需手动commit提交事务;
- autocommit=OFF时:
- 需手动commit提交事务。
4种事务隔离级别
- READ_COMMITTED (RC读已提交):读取一个事务已提交的结果;
- READ_UNCOMMITTED (未提交读):读取一个事务未提交的结果;
- REPEATABLE_READ (RR可重复读):同一个事务内,多次查询结果一致;
- Serializable (可串行化):可串行读,读写相互都会阻塞。
隔离性(isolation):
- 脏读:事务A读取事务B未提交的数据,并在此基础上进行了操作。
- 不可重复读:事务A读取事务B已提交的更改数据。
- 幻读:事务A读取事务B已提交的新增数据。
不可重复读和幻读区别:不可重复读重点在update和delete, 幻读在insert
下文只分析RC、RR这两种隔离级别
- RC (每次读取数据前,都生成一个Read View) 不重复读,幻读
- RR (在第一次读取数据前,生成一个Read View) 可重复读,幻读
undo log/redo log/binlog 区别
undo log:回滚日志
逻辑日志(逻辑语句)
- 记录旧值,保证事务一致性;
- 用于事务回滚;
- MVCC。
每个独立UNDO表空间存在若干个(默认128)个回滚段,而每个回滚段又默认存在1024个undo log slot, 分配undo log 其实质就是在所有undo表空间中找到一个空闲的undo log slot。
mysql对记录改动时,都需要往undo log写入数据,用于回滚和快照读。
- insert时,将新插入的行记录(聚簇索引包含的column)写入undo log,回滚段指针为空。
- update时,将更新前记录旧值写入undo log, 并在新纪录的回滚段指针字段指向该地址。
- delete时,将所有column都记录在undo log中。
注:update、delete都属于更新。更新数据时,mysql会提前生成undo log日志,当事务提交时,并不会立即删除undo log, 后续可能会用到,如快照读。undo log日志删除通过后台purge线程回收处理。
redo log: 重做日志
物理日志(将变化对象的新旧状态都记录到日志中)
- 记录新值,保证事务原子性和持久性;
- 用于数据库的崩溃修复。
binlog: 逻辑日志
- 用于复制、数据备份等操作。
MVCC原理
先介绍概念:多版本并发控制MVCC, 指的就是在使用RC和RR隔离级别的事务,在执行普通的select操作时,访问记录版本链的过程;使不同事务的读写、写读操作并发执行,提高系统性能。
在MVCC并发控制中,读操作可以分为两类:
- 快照读(读可见版本,不加锁)不加锁的select 操作就是快照读。
- 当前读(读最新版本,加锁), 也叫锁定读像select … lock in share mode 共享锁;select … for update、insert、update、delete 排它锁
InnoDB表中有三个隐藏字段,这三个字段是mysql默认添加的:
- DB_ROW_ID: 如果表中没有显式定义主键或者没有唯一索引,则mysql会自动创建一个6字节的rowid存在记录中。
- DB_TRX_ID: 事务id,6字节。
- DB_ROLL_PTR: 回滚段指针,7字节。
图中新插入的数据字段name=A,对应的回滚指针段为NULL。
Read View机制:
在每个事务开始时,都会将当前系统中所有的活跃事务拷贝到一个列表(Read View)中,包含如下属性:
- creator_trx_id:当前事务id;
- trx_ids:活跃事务id集合,也就是未提交的事务集合;
- min_trx_id:trx_ids中最小的事务id;
- max_trx_id:活跃事务中最大的事务id + 1。
当读取一行记录时,判断规则:
- 行记录上trx_id小于Read View中最小事务id (min_trx_id),说明此事务在Read View生成时已经提交,记录可见。
- 行记录上trx_id大于max_trx_id,说明此事务是在Read View生成后提交,记录不可见。
- 行记录上trx_id是否在活跃事务trx_ids中:
- 如果在,说明此事务在Read View生成时还未提交,记录不可见;
- 如果不在,说明此事务在Read View生成时已提交,记录可见。
RR可重复读是怎么实现的?
举例:新插入一条记录值为F1数据并提交,记录上对应回滚段指针为NULL。
当更新记录时,将原记录放入Undo表空间中,我们查看的未修改数据就是从Undo表空间中返回的,如果存在多个数据的版本就会构成一个链表,也就是undo log版本链。
更新记录分为修改聚集索引(聚簇索引)记录或非聚集索引(二级索引)记录:
- 聚簇索引更新:是在原记录位置更新,通过记录指向undo log的隐藏列来重构早期版本的数据。因为聚簇索引上总是存储了回滚段指针和事务id, 是可以通过该指针找到对应的undo记录。
- 二级索引更新:需要将原记录标记为删除,再插入新的数据记录。因为二级索引没有聚集索引上的那些隐藏列,二级索引查找时通过索引键值+主键值来确定唯一记录。
update操作
假设此时开启事务A和事务B, 事务id分别为20和30;快照读时生成Read View列表如下:Read View图
A事务读取Field = F1这条记录时,根据规则1,行记录事务id 10小于事务A id 20,此记录可见;B事务也能看到。
此时B事务修改Field = F2并提交,此时会新增一条undo log,版本链为:
A事务再次读取,由于是RR隔离级别,Read View还是之前的那个列表。
根据版本查找规则,F2的记录被查到,对应的事务id=30,不满足规则1和2,此时判断规则3,事务id=30在活跃事务集合中,说明此记录在Read View生成时还未提交,所有查询不到。
根据回滚段指针找到上一个版本,也就是DB_TRX_ID=10的版本,上面说过规则,就不重复了。此条记录是可见的。
疑问:RR当前读怎么出现了幻读?
RR事务期间只有快照读是木有问题的,不会读到新增的数据。当前读的时候得分情况。
举例事务A和事务B;
事务A如果在整个事务期间一直是快照读,那数据永远是创建Read View列表时的那一份。
如果事务A第一次快照读之后,事务B新增了一条记录并提交。事务A随后update分两种情况:
- update修改了B新增的那条记录(此时事务A是不知道会影响这条记录的,因为第一次快照读的时候是没有的),那么再次查看就能看到(幻读)。原因是update执行之后,会将当前记录上存储的事务信息更新为当前的事务,而当前事务所做的任何更新,对本事务所有SELECT查询都变的可见,因此最后输出的结果是UPDATE执行后更新的所有记录。
- update没有修改B新增的那条记录,那么新增记录还是会查不到。
以上如果理解的不对,还请老铁们指出。
delete操作
记录上有个删除标志位,查找规则和update一致。
聚簇索引和二级索引都包含了DELETED BIT标记位来标识记录是否被删除,真正的删除在事务commit之后且没有读会引用该版本数据的时候。
RC不可重复读是怎么实现的?
假设表里是空记录。开启事务A和事务B,对应事务id为1和2。快照读时生成Read View列表如下:
表记录是空的,事务A和事务B都查不到数据。
此时事务B新增一条记录Field = F1,对应的undo log里面会写一条记录,事务B能查到,事务A查不到,因为没提交。
事务B提交后,事务A再次查询,生成的Read View列表:行记录F1不满足规则1和规则2,也不在活跃事务集合中,说明生成Read View时此事务已提交,记录可见。
RC不可重复读是每次快照读都会生成一个新的Read View列表。
[1][2]
参考资料
[1]阿里内核月报: http://mysql.taobao.org/monthly/
[2]书籍: Mysql性能优化金字塔法则
声明:来自阿飞技术,仅代表创作者观点。链接:https://eyangzhen.com/3545.html