文章目录
快照读与当前读
快照读(普通select语句)通过MVCC方式解决幻读
- MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理
读-写冲突
,做到即使有读写冲突时,也能做到不加锁 , 非阻塞并发读
,而这个读指的就是快照读(属于乐观锁)
, 而非 当前读 。
当前读(select...for update等语句)通过next-key lock(记录锁+间隙锁)
当前读
实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。- 当前读读取的是记录的
最新版本
(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
隔离级别
MySQL通过MVCC解决幻读,MVCC多版本并发控制结束,多版本通过undolog
实现,管理通过ReadView
实现
- 事务有四个隔离级别,可能存在三种并发问题:
- 但是在MySQL中,默认的隔离级别是可重复读,可以解决
脏读和不可重复读
,如果从定义来看,它不能解决幻读,只能采取可串行化
- 但是MVCC可以不采用锁机制,而是通过
乐观锁
方式解决幻读,所以MySQL在可重复读也解决了幻读问题。
这四种隔离级别具体是如何实现的呢?
- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
注意,执行「开始事务」命令,并不意味着启动了事务。在 MySQL 有两种开启事务的命令,分别是:
- 第一种:
begin/start transaction
命令;- 第二种:
start transaction with consistent snapshot
命令;这两种开启事务的命令,事务的启动时机是不同的:
- 执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机;
- 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。
总结
:
- MVCC读的是
快照
,也就是乐观锁
的实现方式,幻读不会出现,相当于解决了幻读,但是并不是编程串行化了,串行化读是通过加锁,一个一个的进行解决。
隐藏字段、Undo log版本链
- 针对每一条记录(行格式),有三个隐藏字段
UUID
:如果没有主键也没有唯一性索引,默认提供一个UUID隐藏字段trx_id
:记录最近一个更新事务的ID(当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里)roll_pointer
:回滚指针,是通过和undo log日志,旧版本写入到undo日志中,旧版本通过链表指针相连接,roll_point指向最近的undo log记录
ReadView
- 使用
READ COMMITTED
和REPEATABLE READ
隔离级别的事务,都必须保证读到已经提交了的
事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的 核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题
creator_trx_id
,创建这个 Read View 的事务 ID。m_ids
,表示在生成ReadView时当前系统中「活跃且未提交」的事务id列表
。(“活跃事务”指的就是,启动了但还没提交的事务。)
min_trx_id
,创建 Read View 时,当前数据库中「活跃事务且未提交的」事务中最小事务的事务 idmax_trx_id
,表示生成ReadView时系统中应该分配给下一个事务的 id 值。max_trx_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
MVCC整体操作流程/如何解决幻读(重点
)
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;(在第一个查询语句后,会创建一个Read View后续的查询语句利用这个 Read View)
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
总结
- MVCC 在
READ COMMITTD
、REPEATABLE READ
这俩个隔离级别才有效,因为这俩个情况下考虑快照(读旧数据),而剩下的读为提交和串行化,读的都是最新数据 - 核心点在于
ReadView
的原理,READ COMMITTD
、REPEATABLE READ
这两个隔离级别的一个很大不同
就是生成ReadView
的时机不同:READ COMMITTD
在每一次进行普通SELECT
操作前都会生成一个ReadViewREPEATABLE READ
只在第一次进行普通SELECT
操作前生成一个ReadView
,之后的查询操作都重复
使用这个ReadView
就好了。
当前读是如何避免幻读的?
- MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
- 假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
使用间隙锁,对范围加锁使得插入或者删除或者更新无法操作
幻读没有被完全解决?
场景一:
- 在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
场景二:
- T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
- T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
- T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。