《凤凰架构》一书中对事务的隔离级别以及事务的定义很清晰

https://www.cnblogs.com/suBlog/p/16592859.html

总结

写锁:X 排他锁,其他事务不能写入数据,也不能施加读锁(可读,但是不可加读锁)

读锁:S 共享锁,多个事务可以同时施加读锁,但是其他事务不能写入数据

范围锁:不能修改范围内已有的数据,也不能对这个范围新增或删除数据

 

事务的隔离级别由高到低

可串行化:对所有读写数据的操作全部加上 写锁、读锁、范围锁

可重复读:只有读锁和写锁,但是没有范围锁

问题:幻读--同一事务两个相同的查询范围得到了不同的结果。

因为更改了这个范围内的数据

读已提交:比起可重复读缺乏贯穿整个事务的读锁,select 后读锁立马释放了。

问题:不可重复读--同一事务对同一行数据的两次查询得到了不同的结果。

事务1会对同一行数据先后读两次,但是事务1第一次读完读锁就释放了,使得事务2可以修改数据并提交了事务,事务1再读第二次时数据就变了。如果有贯穿整个事务的读锁的话,事务1两次都读完了,事务1提交后读锁释放 事务2才可以改数据,事务1两次读到的就是一样的。

读未提交:完全不加读锁

  问题:脏读--读到了另一个事务未提交的数据。

事务2更改了数据,事务还没提交,持有写锁。但是因为没有读锁,所以事务1可以读到这个被更改的加了写锁但还没提交的数据(加了写锁可读但不可加读锁)

 

事务是在 MySQL 引擎层实现的,我们常见的 InnoDB 引擎是支持事务的。InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。是通过 redo log (重做日志)来保证的;
  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完。通过 undo log(回滚日志) 来保证的;
  • 隔离性(Isolation):允许多个并发事务同时对其数据进行读写和修改的能力。 MVCC(多版本并发控制) 或锁机制来保证的;
  • 一致性(Consistency):是事务的目标,通过 持久性+原子性+隔离性 来保证;

不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样,有的数据库只实现了其中几种隔离级别,我们讨论的 MySQL 虽然支持 4 种隔离级别,但是与SQL 标准中规定的各级隔离级别允许发生的现象却有些出入。

MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了,详见这篇文章 (opens new window)),解决的方案有两种:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

 

MVCC 是怎么样工作的?

以下是 Read View 中的四个字段

 

 

Read View 有四个重要的字段:

  • m_ids :指的是在创建 Read View 时当前数据库中「活跃事务」的事务 id 列表注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
  • min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
  • max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
  • creator_trx_id :指的是创建该 Read View 的事务的事务 id。

 

聚簇索引记录中 的 两个隐藏列

对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:

  • trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里(最新的已经提交了的事务id);
  • roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本的版本链,于是就可以通过它找到修改前的记录。

在创建 Read View 后,我们可以将索引记录中的 trx_id 划分这三种情况:

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,总结一下就是:

  • 索引记录上的版本在创建 readView 时已经提交事务,可见。将聚簇索引记录上的版本号 trx_id 与 readView 中的字段比,包括以下两种情况:
    • (绿色部分)小于 min_trx_id 的(在所有活跃事务之前)
    • (黄色部分)在 min_trx_id 和 max_trx_id 之间,但是不存在于 m_ids 之中的(不在创建 readView 时还活跃的事务中)
  • 索引记录上的版本在在创建 readView 时还没提交事务(或说活跃着),不可见;将聚簇索引记录上的版本号 trx_id 与 readView 中的字段比,包括以下两种情况:
    • (红色部分)大于 max_trx_id 的
    • (黄色部分)在 min_trx_id 和 max_trx_id 之间,且存在于 m_ids 之中的(在创建 readView 时还活跃的事务中)
  • 版本链中如果 trx_id  creator_trx_id 相等也是可见的,也就是说,自己对自己是可见的。

这种通过「版本链」(+ ReadView)来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

 

 

MVCC 怎样实现可重复读和读已提交隔离级别?

有锁化时它们的区别是 「读已提交」没有贯穿整个事务的读锁,而「可重复读」有。对于无锁化的 MVCC 实现(select 时是无锁的),就在于创建新 Read View 的时机不同:

  • 「读已提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。相当于读取版本链中最新提交的那条记录。
  • 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View这样就保证了在事务期间读到的数据都是事务启动前的记录。相当于读取版本链中事务 id 比自己小于等于的最新记录。

这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列(版本链)」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。

在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。

select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据(最新提交的)但是它会对读到的记录加上 next-key lock 锁。