当前读
诸如select ... lock in share mode
、select ... for update
、update
、delete
、insert
均为当前读;当前读本质上是加了锁的增删该查语句,无论上的是共享锁还是排他锁均为当前读.
这些语句被称为当前读的根本原因是因为它读取的是记录的最新版本,并且在读取之后,还需保证其他事务不能修改当前记录,对读取的记录加锁;上面的除 select 语句加的是共享锁外,其他的都是排他锁,那为什么 update
、delete
、insert
也被称为当前读呢?
我们都知道 RSBMS 主要由两大部分组成,一部分是程序实例(即 MySQL 实例),另一部分是存储(即 InnoDB),
我们在执行 update
语句更新某几行数据时,每次都需要先读取相应数据行,然后在更新,这个时候在读取的时候需要读取最新行,所以需要使用当前读
快照读
快照读也叫非阻塞读,即所谓快照读就是不加锁的非阻塞读,就是我们最简单的 select
操作
当然了,这里不加锁的非阻塞读是以事务隔离级别不为最高级别的前提下成立,因为在最高隔离级别下,快照读也会变成当前读,在其后自动加lock in share mode
快照读的实现原理
之所以出现快照读,是基于提升并发访问性能考虑的;快照读的实现是基于多版本的并发控制,即 MVCC,可以认为 MVCC 是行级锁的一个变种,但是它在很多情况下避免了加锁的操作,因此开销更低.
既然基于多版本实现,那么快照读有可能读到的并不是数据的最新版本,可能是之前的历史版本。
在 RC【Read Committed】 隔离级别下,快照读和当前读读到读数据版本是一样的;
演示.
1.开启两个会话,并设置相应的事务隔离级别为 RC
【下面的命令是基于 MySQL 8.0的,8以下的版本可以自行查找相应的命令进行替换】
通过show variables like 'transaction%';
查看会话的隔离级别,然后通过set session transaction isolation level read committed;
设置会话的隔离级别为 RC
2.在一个会话中查询数据,另一个会话中修改相应数据
1)会话 1 中查询 deptno = 10 的 loc 为 NEW YORK
2)会话 2 中修改 deptno = 10 的 loc 为纽约
3)在会话1中分别使用当前读和快照读读取 deptno = 10 的数据
发现使用快照读和当前读读取到到数据是一样的,即在 RR 隔离级别下,使用快照读读取到到也是最新数据
而在 RR【Repeatable Read】隔离级别下,当前读返回的是数据的最新版本,而快照读在该隔离级别下可能读到数据的历史版本.在 RR 隔离级别下,事务首次调用快照读的时机很关键,即创造快照的时机决定了快照的版本
演示
1. 第一种情况,会话 1 先快照读读取 deptno = 40 的数据行,会话2修改 deptno = 40的数据行的 loc = 奥地利,并提交会话,然后比较会话 1 通过快照读和当前读读取读数据情况
1)会话 1 先快照读读取 deptno = 40 的数据行
2)会话2修改 deptno = 40的数据行的 loc = 奥地利,并提交,并查询结果显示已修改成功
3)比较会话 1 通过快照读和当前读读取读数据情况
结果显示,快照读读取到的是旧数据,而当前读读到读是最新数据
1. 第二种情况,会话2修改 deptno = 40的数据行的 loc = 乌克兰,并提交会话,然后比较会话 1 通过快照读和当前读读取读数据情况
1)会话2修改 deptno = 40的数据行的 loc = 乌克兰,并提交会话
2)比较会话 1 通过快照读和当前读读取读数据情况
可以看到当前读和快照读读取到的数据是一致的
RC|RR 隔离级别下的 InnoDB 的非阻塞读【即快照读】如何实现
快照读的实现依赖三个因素;每行数据记录除了存储数据以外,还有一些额外的字段,其中最关键的是三个:DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID字段
DB_TRX_ID
该字段用来标识最近一次对本行记录做修改,无论是insert,还是update,它都是事务的标识符,即最后一次修改本行记录的事务ID,delete对于innodb来说也是一个update操作,更新行中的一个特殊位,将行标识为deleted,并非做真正的删除【每次开启一个事务的时候,该事务ID就会递增,即越新开启的事务,它的 事务 ID 越大】
DB_ROLL_PTR
回滚指针,只写入回滚段( roll back segment)的undo 日志记录。如果一行记录被更新,则undo log report 包含重建该行记录被更新之前内容所必须的信息。
DB_ROW_ID
即行号包含一个随着新行插入而单调递增的行ID,当innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。 (以前提到的,在innodb存储引擎中,如果表中没有设置主键并且无唯一键时,Innodb会为我们创建一个隐藏主键字段,即我们这里的DB_ROW_ID)
光有上面这三个字段,并不足以实现快照读,还需要依托undo日志。
undo 日志
当我们对记录做了变更操作时,就会产生undo记录,undo记录中存储的是老版数据,当一个旧的事务需要读取数据时,为了能够读取到老版本的数据,需要顺着undo列找到满足其可见性的记录,这个找满足可见行的记录依赖 read view
undo 日志主要分为两种:即insert undo log 和 update undo log.
insert undo log
表示的是事务对insert新记录产生的undo log,只在事务回滚时需要,并且在事务提交后就可以立即丢弃
update undo log
事务对记录进行delete或者update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被线程删除。
read view
read view主要是用来做可见性判断的,即当我们去执行快照读 select 的时候,会针对我们查询的数据创建出一个 read view,来决定当前事务能看到的是哪个版本的数据,有可能是当前最新版本的数据,也可能是 undo log 中某个版本的数据,read view 遵循一个可见性算法。主要是将要修改的数据的 DB_TRX_ID 取出来,与系统其他活跃事务ID【DB_TRX_ID】做对比,如果大于或者等于这些 ID 的话,就通过 DB_ROW_PTR 指针去取出 un do log,上一层的 DB_TRX_ID 直到小于这些活跃事务 ID 为止,这样就保证了我们获取到的数据版本是当前可见的最稳定的版本
总结
正是以上的三个因子才使得 InnoDB 在 RR、RC 隔离级别下支持非阻塞读,而读取数据时的非阻塞就是所谓的 MVCC【Multiversion concurrency control】 ,而 InnDB 非阻塞读机制实现了仿照版的 MVCC,MVCC 代表多版本并发控制,读不加锁,读写不冲突,极大的增加了系统的并发性能,那为什么是伪 MVCC 机制呢,因为并没有实现核心的多版本并存,而undo log 中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。