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使用mysqld_multi部署单机多实例的方法教程
栏 目:Mysql
下一篇:没有了
本文标题:MYSQL数据库Innodb 引擎mvcc锁实现原理
本文地址:http://www.codeinn.net/misctech/212096.html