MYSQL数据库Innodb 引擎mvcc锁实现原理
作者: 斜月
前言:
大家都知道在java 开发过程中,会经常用到锁,在java 代码中,我们都知道锁是加在对象头上的,在java对象布局中有锁的标志位。程序通过判断锁的标志位来获取加锁的情况。但是在mysql 中,锁的实现原理是什么呢。可能大家都听过 mvcc,但是mvcc 的实现原理是什么呢,可能就说不太清楚了,本文就以实例说明来mvcc 的实现原理。
1 数据库设置隔离级别
我们都知道数据库的隔离级别可以分为以下:
- 读未提交 RU : read uncommitted
- 读提交 RC : read committed
- 可重复读 RR : repeatable read 默认隔离级别
- 序列化 SE : serializable 序列化
我们使用select @@transaction_isolation;
来查看数据库的隔离级别,可能有读者会有疑问了,不是应该是select @@tx_isolation;
吗, 因为我的电脑上装的是 mysql 8.0, 之前的 tx 也是 transaction的简写,高版本的mysql 已经使用 transaction 了。
# 查询当前会话隔离级别 select @@transaction_isolation; # 查看当前系统的隔离级别 select @@global.transaction_isolation; # 命令行开启事务,区别在于前者是第一条语句的执行时间点为事务开始的时间点,建立一致性读,后者是立即建立一致性读,开始执行事务 start transaction 或者 start transaction with consistent snapshot # 设置数据库隔离级别,如若设置会话级别,则修改 global 为session 即可 set global transaction isolation level REPEATABLE READ; set global transaction isolation level READ COMMITTED; set global transaction isolation level READ UNCOMMITTED; set global transaction isolation level SERIALIZABLE;
2 数据库表以及案例操作
操作的数据库表为:
CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `age` int DEFAULT NULL COMMENT '年龄', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; # 初始化语句 INSERT INTO `t_user`(`id`, `age`) VALUES (1, 3); # 使用的操作语句 select * from t_user where id = 1; update t_user set age = age + 1 where id = 1;
案例如图所示:
sessionA,sessionB,sessionC 是连接数据库的3个会话窗口,mysql 数据库的默认隔离级别为RR
,为了达到效果我们把当前的数据库隔离级别改为RC
,语句如下:
set session transaction isolation level READ COMMITTED;
setp1 准备工作,设置当前会话事务隔离级别,查看数据库表中的数据。窗口从左往右依次是sessionA、sessinB和sessionC。
setp2 t1 时刻,sessionA 和 sessionB 开启事务。sessionA t2 时刻执行查询语句,sessionC 执行更新操作,可以看到此时 sessionC 立马执行成功。
step3 t3 时刻,sessionB 查询最新的数据,并执行更新操作
step4 t4 时刻,sessionA 查询最新数据,并在t5 时刻更新数据,这里因为sessionB 的事务还未结束,因此执行的操作会阻塞到超时(这里刻意等待到超时),t6 时刻sessionB 查询数据,并提交事务。
step5 sesionB 提交事务后,sessionA 重新执行语句,可以看到执行后的结果,然后提交事务。
通过上述的操作案例,我们可以观察到:
- 在一个事务中的数据更改对本事务是可见的,此外的事务是否可见取决于其隔离级别。
- 两个事务操作同一条数据,会造成等待。innodb 引擎默认开启死锁检测,并会设置锁的超时时间。
# 设置超时时间默认是50s innodb_lock_wait_timeout # 开启死锁检测,默认是开启死锁检测,默认值为 on,开启死锁检测 innodb_deadlock_detect # 查看更多innodb 引擎的配置 show variables like '%innodb%'
- 在会话中进行数据修改,默认事务是自动提交的。
3 mvcc 实现原理
mvcc,Multi-Version Concurrency Control,即版本并发访问控制,是 mysql innodb 引擎实现的一种并发访问控制方法,用于其事务的实现。其主要作用是为了提高数据库的并发性能,更好地处理读写冲突问题,在遇到冲突的境况也能够做到不加锁,非阻塞并发读取数据。 在解释实现原理前,先了解一下当前读和快照读。
- 当前读:像更改数据的操作(update/delete/insert)操作还有共享锁和排他锁(lock in share mode和for update)这操作都需要读取当前最新版本的数据,否则就会有更新丢失的情况。数据库的修改操作也是将数据所在的数据页从磁盘加载到内存,然后进行修改,最后写回到磁盘中。
- 快照读:不加锁的select获取数据就是读快照,不会产生阻塞。在串行化的数据库隔离级别下,快照读会退化成当前度。每行数据都有多个版本,当前读就是获取最新的已经提交过得版本数据。
接下来讲述的是非常重要的部分:
- 已经提交的事务
- 未被提交的事务
- 未开始的事务
在可重复读的隔离级别下,事务启动需要对整个库做备份,这显然是不可能的,实际上mvcc也没有这样做,而是利用版本号来控制,也就是事务开始时向 innodb 事务系统申请一个 transaction id,这个id申请顺序是严格递增的。每行数据的版本号 row trx_id = transaction id , 也就可以保证其版本的唯一性。可以保证事务启动时,会获取当前所有活跃事务id的数组,可以保证其唯一性。事务id数组中的最小的id记为低水位,数组中的最大id+1记为高水位。低水位和高水位便是已经提交的事务-未提交的数据、未提交的事务-未开始的事务的分界标志位,以上就组成了当前事务的一致性视图(read-view)。
前面已经提到了,所有的数据都是有版本的,这个版本即row trx_id。当需要读快照时,就需要读取trx_id小于低水位且最大的trx_id版本号的数据。读当前就是读取当前最新版本的数据,如果数据正在事务的处理中,那么久需要等待,这也是为什么会出现锁等待超时的情况。
当在RC
的数据库隔离级别下,每次的操作都会重新获取当前活跃的trx_id数组,形成新的一致性视图,这就是sessionB 在t3 时刻读取到了sessionC
在t2时刻提交的数据,因为在新的一致性视图中,sessionC 的trx_id 已经已经提交的事务了,所以就可以读取到。在sessionA 的t5 时刻,拿到的一致性视图中活跃的trx_id 数组中包含 sessionB 的活跃id,因为在更新数据时,需要当前读,而数据已经被锁住,所以出现了锁超时的情况。
而在RR
的数据库隔离级别下,在事务开始时生成一个一致性视图,此后在事务提交之前,其 trx_id 数组不会发生变化,这样才能保证其可重复读的特性。RR
相对于RC
实现简单,区别在于是否在执行语句前更新一致性视图,活跃事务id的数组是否更新。
综上,我们就知道了mvcc 是如何实现可重复读和读提交的隔离级别了。
4 ACID 的实现
- 事务的原子性 A 是通过 undolog 回滚日志来实现。
- 事务的持久性 C 性是通过 redolog 重做日志来实现的。
- 事务的隔离性 I 是通过 MVCC 来实现的。
- 原子性,持久性,隔离性都实现了,那么一致性 D 也就实现了。
redo log 是mysql innodb 引擎产生的物理日志,其大小有一定的限制,采用循环写入的方式,写满之后进行刷盘。主要用于宕机后的数据恢复。redo log是一个循环写入的日志,可以理解为一个环,有 checkpoint 和 write pos 两个标志点,checkpoint之前的空间是清除后的可写空间,清除之前会更新到磁盘中,write pos是数据写入的位置,当两个标志点相遇表明redo log已经满了,这时数据库停止进行数据库更新语句的执行,转而进行redo log日志同步到磁盘中。
binlog 是mysql server 层记录逻辑的日志,可以一致追加写入,主要用于主从数据同步。
到此这篇关于MYSQL数据库Innodb 引擎mvcc锁实现原理的文章就介绍到这了,更多相关mysql mvcc锁实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!