一致性无锁读 & 锁定读

31丨为什么大部分RDBMS都会支持MVCC?
一致性无锁读(Consistent Reads)也被称为快照读。
锁定读(Locking Reads)也被称为当前读。


一致性无锁读
不加锁的读取操作被称为一致性读,或者一致性无锁读,有的地方也称之为快照读。
在 MySQL 中,普通的 select 语句在 读已提交、可重复读 隔离级别下都是基于 MVCC 机制读取的。

为什么这里要强调 在 读已提交、可重复读 隔离级别下 呢? 这是因为在可串行化隔离级别下,普通的 select 语句是锁定锁。

因此,MySQL 中,普通的 select 语句在 读已提交、可重复读 隔离级别下都算是一致性无锁读 ,比方说:

  1. # 在 读已提交、可重复读 隔离级别 下,普通的select是一致性无锁读
  2. select * from t1;
  3. select * from t1 inner join t2 on t1.col1 = t2.col2

一致性无锁读 并不会对表中的任何记录做 加锁 操作,其他事务可以自由的对表中的记录做改动。


锁定读
在 MySQL 加锁的 select,或者 DML 操作(对数据进行增删改)都会进行锁定读,比如:

  1. # 在 可串行化隔离级别 下,普通的select是锁定锁 (加共享锁)
  2. select * from t1;
  3. select * from t1 lock in share mode;
  4. select * from t1 for update;
  5. insert into t1 values ...
  6. delete from t1 where ...
  7. update t1 set ...

在 MySQL 中,锁定读都是基于最新的数据进行读取,而不使用 MVCC 机制。

从请求开始

1.首先客户端通过tcp/ip发送一条sql语句到server层的SQL interface
2.SQL interface接到该请求后,先对该条语句进行解析,验证权限是否匹配
3.验证通过以后,分析器会对该语句分析,是否语法有错误等
4.接下来是优化器器生成相应的执行计划,选择最优的执行计划
5.之后会是执行器根据执行计划执行这条语句。在这一步会去open table,如果该table上有MDL,则等待。
如果没有,则加在该表上加短暂的MDL(S)
(如果opend_table太大,表明open_table_cache太小。需要不停的去打开frm文件)
6.进入到引擎层,首先会去innodb_buffer_pool里的data dictionary(元数据信息)得到表信息
7.通过元数据信息,去lock info里查出是否会有相关的锁信息,并把这条update语句需要的
锁信息写入到lock info里(锁这里还有待补充)
8.然后涉及到的老数据通过快照的方式存储到innodb_buffer_pool里的undo page里,并且记录undo log修改的redo
(如果data page里有就直接载入到undo page里,如果没有,则需要去磁盘里取出相应page的数据,载入到undo page里)
9.在innodb_buffer_pool的data page做update操作。并把操作的物理数据页修改记录到redo log buffer里
由于update这个事务会涉及到多个页面的修改,所以redo log buffer里会记录多条页面的修改信息。
因为group commit的原因,这次事务所产生的redo log buffer可能会跟随其它事务一同flush并且sync到磁盘上
10.同时修改的信息,会按照event的格式,记录到binlog_cache中。(这里注意binlog_cache_size是transaction级别的,不是session级别的参数,
一旦commit之后,dump线程会从binlog_cache里把event主动发送给slave的I/O线程)
11.之后把这条sql,需要在二级索引上做的修改,写入到change buffer page,等到下次有其他sql需要读取该二级索引时,再去与二级索引做merge
(随机I/O变为顺序I/O,但是由于现在的磁盘都是SSD,所以对于寻址来说,随机I/O和顺序I/O差距不大)
12.此时update语句已经完成,需要commit或者rollback。这里讨论commit的情况,并且双1
13.commit操作,由于存储引擎层与server层之间采用的是内部XA(保证两个事务的一致性,这里主要保证redo log和binlog的原子性),
所以提交分为prepare阶段与commit阶段
14.prepare阶段,将事务的xid写入,将binlog_cache里的进行flush以及sync操作(大事务的话这步非常耗时)
15.commit阶段,由于之前该事务产生的redo log已经sync到磁盘了。所以这步只是在redo log里标记commit
16.当binlog和redo log都已经落盘以后,如果触发了刷新脏页的操作,先把该脏页复制到doublewrite buffer里,把doublewrite buffer里的刷新到共享表空间,然后才是通过page cleaner线程把脏页写入到磁盘中
老师,你看我的步骤中有什么问题嘛?我感觉第6步那里有点问题,因为第5步已经去open table了,第6步还有没有必要去buffer里查找元数据呢?这元数据是表示的系统的元数据嘛,还是所有表的?谢谢老师指正
作者回复: 其实在实现上5是调用了6的过程了的,所以是一回事。MySQL server 层和InnoDB层都保存了表结构,所以有书上描述时会拆开说。

这个描述很详细,同时还有点到我们后面要讲的内通(编辑快来,有人来砸场子啦😄😄

select

普通的 select 语句:select * from t1 where id = 1;
给读到的记录加共享锁:select … lock in share mode;
给读到的记录加独占锁:select … for update;


对于普通的 select 语句,使用的是快照读,或者称为一致性读,基于 MVCC 判断数据版本的可见性,读的时候不需要加锁,也不受锁的限制。(可串行化隔离级别下除外)
对于给读到的记录加读锁 / 写锁的 select 语句,使用的是当前读,读取最新版本的数据,但是受锁的限制。
for update 指令指示数据库对返回的所有结果行要加锁。
对于 update 语句,基于最新版本的数据修改;加写锁,事务提交后,释放写锁。


InnoDB 的锁,与索引类型,事务的隔离级别相关。
InnoDB 到底是行锁还是表锁取决于你的 SQL 语句。
InnoDB 的行锁是实现在索引上的,而不是锁在物理行记录上。
如果查询没有命中索引,也将退化为表锁。
所以如果访问没有命中索引,也无法使用行锁,将要退化为表锁。

01 | 基础架构:一条SQL查询语句是如何执行的?
19 | 为什么我只查一行的语句,也执行这么慢?

MySQL 的查询结果,发送给客户端的过程
33 | 我查这么多数据,会不会把数据库内存打爆?

带 join 的查询

34 | 到底可不可以使用join?

修改

当我们对数据进行更新的时候,也就是INSERT、DELETE或者UPDATE的时候,数据库也会自动使用排它锁,防止其他事务对该数据行进行操作。
02 | 日志系统:一条SQL更新语句是如何执行的?
08 | 事务到底是隔离的还是不隔离的?

delete

13 | 为什么表数据删掉一半,表文件大小不变?

insert

参考资料