SQL 事务级别 与 支持 图解逻辑记录

语言: CN / TW / HK

MySQl 专栏持续更新 不说晦涩难懂的东西 尽量输出容易理解 和 使用的SQL技巧 和 初中级开发不是很常用的但很有用的知识

欢迎查看👉🏻👉🏻👉🏻SQL 专栏 查漏补缺 指教一二

src=http __img2.biaoqingjia.com_biaoqing_201810_2c3993f64eec252da6d674f9d80fc4e9.gif&refer=http __img2.biaoqingjia.gif

前言

事务这个东西基本都了解 但是很多人不能将整个逻辑串联起来 可能大部分人就知道ACID 但是事务的知识远远不止ACID 怎么能抗住追问 和 完善自己的知识目录 这个还是不错的 建议认真看完

SQL 标准事务隔离级别

SQL 标准中规定了 4 种事务隔离级别,在多事务并发时可以避免一些事务并发问题。

read uncommitted

read uncommitted(读未提交):简称 RU 级别,这个隔离级别是不允许脏写发生的,也就是说不允许两个事务在没提交的情况下去更新同一行数据,却允许读取未提交的数据。但是依旧可能发生脏读、不可重复读、幻读等问题。所以一般情况下都不会使用这个级别。

read committed

read committed(读已提交):简称 RC 级别,这个隔离级别是不允许脏写和脏读的,也就是说不允许读取和修改事务未提交的值。但是依旧可能发生不可重复读和幻读等问题,因为在事务期间内别的事务修改数据并提交后,还是会被这个事务读取到。

repeatable read

repeatable read(可重复读):简称 RR 级别,这个隔离级别是不允许脏写、脏读和不可重复读发生的,也就是说不会在事务内读到被其它事务修改的值,哪怕别的事务已提交。但是依旧有可能发生幻读问题,因为它不保证别的事务新增或删除数据后,这个事务不被影响。

serializable

serializable(串行化):简称 S 级别,这个隔离级别可以避免所有多事务问题,因为这个隔离级别下,所有的事务都将串行化,不会并发执行事务。但是这将会严重降低数据库的性能,所以这个级别一般也不会用。

综上,一般用的最多的就是 RC、RR 级别。并且在 MySQL 中 RR 级别已经可以避免幻读的问题了,所以 MySQL 默认的隔离级别就是 RR 级别。

修改事务隔离级别

MySQL 中默认的 RR 级别,可以避免多事务的问题,所以一般不用修改,但在某些业务场景下也许需要修改隔离级别。

MySQL 命令行

使用命令:SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL {level 级别};

leve 级别:READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ,SERIALIZABLE

Spring 事务注解

也可以使用 Spring 的注解 @Transactional 来修改事务级别, @Transactional 注解有一个参数 isolation 可以直接设置隔离级别,默认为 @Transactional(isolation=Isolation.DEFAULT),也就是说默认为 MySQL 设置的级别。

其它参数为:
  Isolation.READ_UNCOMMITTED
  Isolation.READ_COMMITTED
  Isolation.REPEATABLE_READ
  Isolation.SERIALIZABLE

undo log 版本链

MySQL 事务开始后会生成一个全局唯一的事务 ID,并且每一行数据其实都有两个隐藏字段 trx_id(最近更新这条数据的事务 ID)和 roll_pointer(undo log 编号),所以如果一个事务更新某行数据时,会把自己的事务 ID 写入这行数据的 trx_id,这行数据的 roll_pointer 会指向最近的 undo log,如此一来,这行数据在多事务场景下修改时,它的 undo log 就会形成一个 undo log 版本链作为这行数据在每个事务中的快照。

比如首先事务 A 插入一行数据值 A,同时生成 insert undo log,在事务 A 提交后,刚才生成的 undo log 就会直接被删除。

紧接着事务 B 去修改这行数据为值 B,同时生成 update undo log,在事务 B 提交后,刚才生成的 undo log 不会立即删除。

然后事务 C 把这行数据删除了,但是删除操作并不会直接删除数据行,而是改变数据行中 delete 标记位,标记这行数据是删除状态。

等待 purge 线程在适当时机删除这行数据的 undo log 版本链。

ReadView 机制

在了解 ReadView 之前,首先得知道 MySQL 读操作有两种方式:

当前读:读取的是数据最新版本,对当前读取的数据加锁,阻塞其它事务对数据行的修改,比如增删改操作,或者 select * from table for update | insert | delete

快照读:读取的是数据历史版本,通过 mvcc 机制读取数据的历史版本,比如普通的 Select 操作。

ReadView 机制是基于 undo log 版本链实现的。ReadView 保存当前系统内尚未提交的事务ID,需要说明的是,事务 ID 是 MySQL 按照时间顺序生成的一个版本号。当前读或者隔离级别为 RC 时,事务期间每次查询都会重新生成一个 ReadView;而快照读只会在事务开启时生成一次 ReadView,后续事务操作都不会修改这个 ReadView。

ReadView 中包含 4 个主要的属性:

m_ids:尚未提交的事务ID
min_trx_id:m_ids 中最小的值
max_trx_id:Mysql 下一个即将生成的ID
creator_trx_id:当前事务ID

通过 ReadView 生成机制可以知道,ReadView 中保存着当前事务开启时,系统内所有未提交事务的ID快照。所有小于 min_trx_id 的事务 ID 都是已经提交的,这个需要明确;所以当前读或 RC 级别下,每次查询都能感知到哪些事务是已经提交的,哪些事务是本事务开启时仍然未提交的,哪些事务是本事务开启后再开启的。通过时间轴可以更加清晰的了解 ReadView 对事务状态的划分:

m_ids 列表中有一个必然是本事务 ID creator_trx_id;而列表中第一个则是 min_trx_id;列表最后一个的下一个就是系统即将生成的事务 ID max_trx_id;

判断机制规则:

if (trx_id <= min_trx_id) { // 说明修改这行数据的事务已经提交,或者说就是本事务修改的,那么该版本可读。 } else if ( min_trx_id < trx_id && trx_id < max_trx_id) { // 这时需要判断修改这行数据的事务 ID trx_id 是否在 m_ids 列表中 if (m_ids.contains(trx_id)) { // 说明生成 ReadView 时,该事务还未提交,那么该版本不可读 // 根据 undo log 版本链读取最近的历史版本 // 如果没有合适的历史版本,那么这行数据就是不可读的 } else { // 说明生成 ReadView 时,该事务已提交,那么该版本可读。 } } else { // 说明修改这行数据的事务,在生成 ReadView 后才开启的,那么该版本不可读 // 根据 undo log 版本链读取最近的合适的历史版本, // 如果没有合适的历史版本,那么这行数据就是不可读的 }

这个机制就是事务在读取时,会判断这行数据的 trx_id 和事务本身 ReadView 中的 min_trx_id 的大小关系,以及 trx_id 是否在 m_ids 列表中,来决定这行数据是否可读,以及用什么方式来读。ReadView 机制和 undo log 版本链形成了 MySQL 的 MVCC(Multi-Version Concurrent Control)机制(多版本并发控制机制)。

MySQL 行锁

独占锁:事务更新数据时会加独占锁,加独占锁的数据行其它事务不可修改,但是可以通过 mvcc 机制读取数据行快照,其它事务必须等待当前事务提交才能修改。也可以使用 select * from table for update | insert | delete 在查询时对数据行添加独占锁,那么其它事务此时就不能更新这行数据,也不能对这行数据加共享锁。

共享锁:事务查询数据行时默认不会加锁,但是可以使用 select * from table lock in share mode 添加共享锁。

独占锁和独占锁之间是互斥的,意思就是在修改数据行时不允许其它事务修改,这也是避免脏写的方法。
  独占锁和共享锁之间是互斥的,意思就是在读取数据行时不允许其它事务修改。
  共享锁和共享锁之间是兼容的,意思就是事务之间读取数据行都不会影响彼此,毕竟只是读数据而已。

MySQL 表锁

Innodb 存储引擎提供表级锁,在执行 DDL 语句时会添加表级锁,此时会阻断所有的增删改操作;执行增删改操作会阻塞 DDL 执行。

表级独占锁:LOCK TABLES {table_name} WRITE
  表级共享锁:LOCK TABLES {table_name} READ
  意向独占锁:执行增删改时,会自动在表上添加。
  意向共享锁:执行查询操作时,会自动在表上添加。

所有的意向锁之间都是兼容的,就是说所有的事务都可以在同一时间对表内的数据进行增删改查操作。

1. 意向独占锁和表级独占锁是互斥的,所以在执行 DDL 语句修改表时,会阻塞所有的增删改操作;反之,执行增删改操作会阻塞 DDL 执行。
  2. 意向共享锁和表级独占锁是互斥的,所以在事务执行查询操作时,会阻塞 DDL 执行;反之,在执行 DDL 语句时,会阻塞所有的查询操作。
  3. 表级共享锁和表级独占锁、意向独占锁是互斥的,加上这个锁就不允许对这个表的数据进行增删改操作,只能读数据。
  4. 表级独占锁与其它锁都是互斥的,加上这个锁就不允许对表进行任何读写操作。

事务之间的增删改查不会被意向锁相互影响,但是增删改查的意向锁和表级独占锁是互斥的,意思就是执行增删改查时,不允许修改表;反之,修改表时,不允许增删改查。

MVCC 机制

MVCC 机制是基于ReadView 机制和 undo log 版本链形成的。主要用来解决多事务间的脏读、不可重复读、幻读等问题,使用无锁机制提高数据库的并发性能。
SQL 标准事务隔离机制有四个级别:RU、RC、RR、S 级。MVCC 在 RC、RR 级别中使用。

RU 级别

防止多事务间的脏写,但是允许脏读。也就是说不允许修改未提交事务修改的数据行,但是允许读取未提交事务修改的数据行。
要实现这个级别,只需要在事务修改数据时添加独占锁即可。此时其它修改数据行的事务会被阻塞,只允许读取。

当事务执行完成后会唤醒等待的其它事务,并修改它们的锁状态为 false。

RC 级别

防止多事务间的脏读,但是允许不可重复读、幻读。也就是说不允许读取未提交事务修改的数据,这个级别事务读取的数据行都是其它事务已经提交的。
这个级别的实现就需要使用 ReadView 机制和 undo log 版本链了,也就是 MVCC 机制。

首先事务每发起一次读取都会重新生成一个 ReadView 记录查询时系统内尚未提交的事务 ID 快照,这个是非常重要的。

使用 ReadView 和 undo log 就已经达到防止脏读的目的了,因为在发起读取时,所有被修改的事务未提交的数据都不会被读取到,只会读取已提交的或者历史版本。

然后本事务再次发起读取,再生成一个新的 ReadView,此时刚才修改数据行的事务可能已经提交了,那么这时再根据 ReadView 机制进行读取,就可能读取到刚才被修改的数据了。也就产生了不可重复读的问题。如果刚才那个事务是添加操作,那么根据上述读取规则就会产生幻读的问题。

RR 级别

防止多事务间的不可重复读和幻读(只针对快照读,当前读需要加锁),也就是说事务开启后读取的数据其实就是一个快照版本,无论其它事务对数据的增删改,是否提交,都不会影响本事务对数据的查询。这个级别的实现也是需要 MVCC 机制实现的。

与 RC 级别不同的是,这个级别的事务在开启后首次查询时会生成一个 ReadView,后续都不会对其修改,也就是说这个事务所有的读取操作都会基于这个 ReadView 记录的事务ID快照进行读取。就这个 ReadView 生成的区别就会让 RR 级别的事务不会产生不可重复读和幻读的问题,因为事务期间所有读取操作都是基于同一个 ReadView。还有就是如果数据行是 ReadView 生成后其它事务新增的,那么这行数据是不会有合适的历史版本的。

综上,RC、RR 主要区别就是 ReadView 生成的规则,就是这个规则才会使 RR 级防止幻读和不可重复读的出现。当然,上述的读,都是基于快照读而言的。

S 级别

加锁,使所有事务串行化,可以避免所有的多事务问题。而后果也是可想而知的,将会导致 MySQL 性能及其低下。

挺不错的 建议收藏 或关注专栏 持续整理相关知识

推荐阅读相关文章:小白也能看到索引的使用和规则