注意以下操作都是以InnoDB引擎为操作基准。
MVCC 是多版本并发控制(Multiversion Concurrency Control)的缩写。它是一种数据库事务管理技术,用于解决并发访问数据库的问题。MVCC 通过创建多个版本的同一数据,每个版本与一个事务关联,来实现并发控制。
数据库在执行更新操作时,会保留之前版本的数据,以便其他正在执行事务的用户可以访问这些数据。每个事务都能看到一个稳定的数据快照,并且仅接触到他们自己的版本,这意味着每个事务可以独立地读取和写入数据,而不会干扰其它事务。
MVCC 在数据库的可伸缩性和性能方面具有重要作用,尤其是对于高并发的应用程序,如电子商务网站和社交媒体应用。
第一层:处理客户端连接、授权认证,安全校验等。
第二层:服务器server层,负责对SQL解释、分析、优化、执行操作引擎等。
第三层:存储引擎,负责MySQL中数据的存储和提取。
ACID是指数据库事务所必须具备的四个特性,包括:
原子性(Atomicity):事务是一个不可分割的原子操作,要么全部执行成功,要么全部失败回滚,不允许出现部分执行成功或失败的情况。【undolog】
一致性(Consistency):事务执行前和执行后,数据库的完整性约束没有被破坏,也就是说,在事务完成后,数据库从一个一致性状态转变为另一个一致性状态。
隔离性(Isolation):事务之间是相互隔离的,一个事务的执行不能被其他事务干扰。多个事务并发执行时,它们之间的执行是独立的,每个事务感觉就像是在独占数据库。【锁,mvcc】
持久性(Durability):事务完成后,对于数据的修改是永久性的,即使系统故障也不会导致数据的丢失。因此,数据库中所有的数据修改都需要记录到日志中,以便在系统故障恢复时进行重做。【redolog】
事务的隔离级别是指多个事务同时操作一个数据库时,数据库系统对这些事务分配的隔离程度。在不同的隔离级别下,事务之间的隔离程度也不同。常见的隔离级别包括:
读未提交(Read Uncommitted):允许一个事务读取另一个事务未提交的数据。这种隔离级别对数据的完整性和一致性影响较大。【存在脏读】
读已提交(Read Committed):一个事务只能读取已经提交的数据,其他未提交的事务所做的修改对它不可见。这种隔离级别对数据完整性和一致性的保护较好。【解决脏读,存在不可重复读】【mysql InnoDB 通过 mvcc 解决 】
可重复读(Repeatable Read):一个事务在执行期间看到的数据是固定的,不受其他并发事务的影响。即使其他事务已经提交了对数据的修改,当前事务也只能看到自己在读取数据时的版本。这种隔离级别对数据的完整性和一致性有一定保护。【解决不可重复读,存在幻读】【mysql InnoDB 通过 mvcc 解决 】
串行化(Serializable):最高的隔离级别,确保事务之间的操作是完全独立的,一个事务的操作必须等另一个事务结束后才能开始。这种隔离级别对数据完整性和一致性的保护最好,但并发性能较低。【解决脏读,解决不可重复读,解决幻读】【mysql InnoDB 通过 加锁解决 】
在实际应用中,应根据业务需求和系统性能等因素选择合适的隔离级别。
server 层
引擎层
在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读。
在InnoDB中的每一条记录实际都会存在三个隐藏列
DB_TRX_ID:事务 ID,是根据事务产生时间顺序自动递增的,是独一无二的。如果某个事务执行过程中对该记录执行了增、删、改操作,那么InnoDB存储引擎就会记录下该条事务的 id。
DB_ROLL_PTR:回滚指针,本质上就是一个指向记录对应的undo log的一个指针,InnoDB 通过这个指针找到之前版本的数据
DB_ROW_ID:主键,如果有自定义主键,那么该值就是主键;如果没有主键,那么就会使用定义的第一个唯一索引;如果没有唯一索引,那么就会默认生成一个隐藏列作为主键。
这些数据快照都是存储在undolog 中的,这些数据分为两类
Insert undo log :insert生成的日志,仅在事务回滚中需要,并且可以在事务提交后立即丢弃。
Update undo log:update/delete生成的日志,除了用于事务回滚,还用于一致性读取,只有不存在innodb为其分配快照的事务之后才能丢弃它们,在一致读取中可能需要update undo log中的信息来构建数据库行的早期版本。
删除操作实际上不会直接删除,而只是标记为删除,最终的删除操作是purge线程完成的
purge线程作用,一是清理undo log,二是清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识删除,真正的删除工作需要后台purge线程去完成。
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
记录并维护系统当前活跃事务的ID(trx_id)(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。
Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View几个属性
MVCC 只在 Read Commited(读已提交) 和 Repeatable Read(可重读读) 两种隔离级别下工作。
在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,从而做到可重复读。
1、首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断。
2、接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断。
3、判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。
在一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。而多出来或者少的哪一行被叫做幻行。
在快照读读情况下,mysql通过mvcc来避免幻读。
在当前读读情况下,mysql通过next-key来避免幻读。
不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以mysql的RR级别是解决了幻读的。
所谓光说不练假把式,光练不说傻把式,又练又说真把式,上面说的只是结论,下面进行实际的操作演示。
mysql 版本 5.7
创建测试表
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `age` tinyint(3) DEFAULT NULL, `gender` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_abc` (`name`,`age`,`gender`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8
(开始之前将事务自动提交关闭)
当前的事务隔离级别为,可重复读(Repeatable Read)
步骤一,t1时刻同时开启事务
步骤二,t2时刻事务2更新并提交
步骤三,t3时刻事务1查询
问题,事务1在t3时刻是否能查询t2时刻事务2提交的数据。
预期结果能
因为执行的是select所以为快照读,但是由于Read View是在快照读的时候才产生,并且可重复读(Repeatable Read)隔离级别下,只产生一个Read View。事务2更新并提交完成时候,由于此时事务1还没进行快照读。所以此时事务1查询的时候是可以查询到事务2更新并提交后的数据的。
sql 执行验证
查询结果符合预期
通过算法核对
trx_list:1,3 【当前活跃事务为 1,3,因为2已经提交了】
low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】
up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】
算法验证:验证当前事务1是否能看到事务2提交的数据。
(1)DB_TRX_ID < up_limit_id 【2<1】不成立继续判断
(2)DB_TRX_ID >= low_limit_id 【2>=4】不成立继续判断
(3)判断DB_TRX_ID是否在活跃事务中,不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。
当前的事务隔离级别为,可重复读(Repeatable Read)
步骤一,t1时刻同时开启事务
步骤二,t2时刻事务1进行快照读
步骤三,t3时刻事务2修改并提交
步骤四,t4时刻事务1进行快照读
问题,事务1在t4时刻是否能查询t3时刻事务2提交的数据。
预期结果不能
事务1在t2时刻执行的是select所以为快照读,此时Read View产生,并且可重复读(Repeatable Read)隔离级别下,只产生一个Read View。事务2在t3更新并提交完成时候,由于此时事务1已经产生了Read View,再次进行快照读。所以此时事务1查询的时候是可以查询不到事务2更新并提交后的数据的,因为读取的是快照中的数据。
sql 执行验证
查询结果符合预期
通过算法核对
第一次查询
trx_ids:1,2,3 【当前活跃事务为 1,2,3,此时事务2还没开始修改提交】
low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】
up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】
第二次查询
可重复读(Repeatable Read)事务隔离级别下只产生一个read view
trx_ids:1,3 【当前活跃事务为 1,3,此时事务2已经修改提交】
low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】
up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】
第二次快照读的时候,当前数据的DB_TRX_ID为2
(1)DB_TRX_ID < up_limit_id 【2<1】不成立继续判断
(2)DB_TRX_ID >= low_limit_id 【2>=4】不成立继续判断
(3)判断DB_TRX_ID是否在活跃事务中,在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到。
当前的事务隔离级别为,可重复读(Repeatable Read)
如果事务中全部都是快照读,不会产生幻读问题,但是当快照读和当前读一起使用的时候就会产生幻读问题。
Mysql的解决方案是加锁,想要解决这个问题,必须要保证当前读和快照读的数据必须要一致,只能去阻止其他事务进行插入操作,所以只能加锁。
sql 执行验证
1,如果事务中全部都是快照读,不会产生幻读问题
2,当快照读和当前读一起使用的时候就会产生幻读问题
3,如果事务中全部都是当前读,不会产生幻读问题【加锁实现的】
提交后立刻执行
当前的事务隔离级别为,读已提交(Read Committed)
验证在此隔离级别,每一次快照读都会产生一次新的read view
步骤一,t1时刻同时开启事务
步骤二,t2时刻事务1进行快照读
步骤三,t3时刻事务2修改并提交
步骤四,t4时刻事务1进行快照读
问题,事务1在t4时刻是否能查询t3时刻事务2提交的数据。
预期结果能
事务1在t2时刻执行的是select所以为快照读,此时Read View产生,读已提交(Read Committed)隔离级别下,产生多个Read View。所以事务2在t3更新并提交完成时候,事务1在t4时刻再次进行快照读。此时又产生了新的Read View。新的Read View是在t3时刻事务2修改并提交产生的所以可以查看到。
sql 验证
符合预期结果
通过算法核对
第一次查询
trx_ids:1,2,3 【当前活跃事务为 1,2,3,此时事务2还没开始修改提交】
low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】
up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】
第二次查询
可重复读(Repeatable Read)事务隔离级别下只产生一个read view
trx_ids:1,3 【当前活跃事务为 1,3,此时事务2已经修改提交】
low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】
up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】
第二次快照读的时候,当前数据的DB_TRX_ID为2
(1)DB_TRX_ID < up_limit_id 【2<1】不成立继续判断
(2)DB_TRX_ID >= low_limit_id 【2>=4】不成立继续判断
(3)判断DB_TRX_ID是否在活跃事务中,不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。