0
点赞
收藏
分享

微信扫一扫

InnoDB的隔离级别和锁

独孤凌雪 2022-04-06 阅读 51
MySQL

前言

不同隔离级别下会出现的一致性问题:

隔离级别脏读不可重复读幻读
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE
  • 脏读:事务A可以读取到事务B修改但未提交的数据。
  • 不可重复读:同一个事务两次读取同一条记录的结果不一致,重点在于 updatedelete
  • 幻读:同一事务两次读取同样范围的记录,第二次返回了第一次没有的记录,重点在于 insert

不同隔离级别下存在的问题演示

READ-UNCOMMITTED

脏读 ✅

当前数据库快照

id_name_
1脏读的事务_V1

事务A

BEGIN;

UPDATE ql_tx_test.`ql_tx` SET name_ = '脏读的事务_V2' WHERE id_ = 1;

# 未提交

事务B

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1; #脏读的事务_V2

不可重复读 ✅

幻读 ✅

READ-COMMITTED

脏读 ❌

当前数据库快照

id_name_
1脏读的事务_V9

事务A

BEGIN;

UPDATE ql_tx_test.`ql_tx` SET name_ = '脏读的事务_V10' WHERE id_ = 1;

# 未提交

事务B

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1; # 脏读的事务_V9

不可重复读 ✅

当前数据库快照

id_name_
1脏读的事务_V9

事务A

BEGIN;

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1; # 脏读的事务_V9;

事务B

BEGIN;

UPDATE ql_tx_test.`ql_tx` SET name_ = '脏读的事务_V10' WHERE id_ = 1;

COMMIT; #提交

事务A

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1; # 脏读的事务_V10;

PS:跟上面脏读的案例唯一的区别就是,事务B提交了事务。也许这就是为什么该隔离级别叫做读已提交吧。

幻读 ✅

当前数据库快照

id_name_
1脏读的事务_V9

事务A

BEGIN;

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ <=10; # 查出一条

事务B

INSERT INTO ql_tx_test.`ql_tx`(`id_`,`name_`) values(4,"幻读事务_V4");

事务A

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ <=10; # 查出两条

REPEATABLE READ

脏读 ❌

脏读就不演示了。

不可重复读 ❌

当前数据库快照

id_name_
1脏读的事务_V9

事务A

BEGIN;

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1; #脏读的事务_V9 

事务B

UPDATE ql_tx_test.`ql_tx` SET name_ = '脏读的事务_V11' WHERE id_ = 1;

事务A

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1; #脏读的事务_V9 

幻读 ✅

当前数据库快照

id_name_
1脏读的事务_V9
2脏读的事务_V2

事务A

BEGIN;

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ <=10; # 查出 id_=1、2 两条记录

事务B

INSERT INTO ql_tx_test.`ql_tx`(`id_`,`name_`) values(5,"幻读事务_V5");

事务A

SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ <= 10; # 查出 id_=1、2 两条记录

我们继续上面的演示

事务A

INSERT INTO ql_tx_test.`ql_tx`(`id_`,`name_`) values(5,"幻读事务_V5");#插入其他事务已提交的id_=5的记录,主键冲突

SERIALIZABLE

InnoDB存储引擎中的锁

共享锁和排它锁

用法

# 共享锁/读锁
SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1 LOCK IN SHARE MODE;

# 排它锁/写锁
SELECT * FROM ql_tx_test.`ql_tx` WHERE id_ = 1 FOR UPDATE;

效果:

  • 读读不冲突,读写冲突,写写冲突。

意向锁(Intention Locks)

意向锁是表级锁,有两种类型:

  • IS:表明一个事务打算对表中的一行记录加共享锁。
  • IX:表明一个事务打算对表中的一行记录加排它锁。

意向锁遵循如下两个规则:

  • 事务给数据行加共享锁之前,必须先获得该表的IS锁。
  • 事务给数据行加排它锁之前,必须先获得该表的IX锁。

表级锁的兼容情况

说明:这里抽取一两个样例进行说明

  • X X 冲突:两个事务同时对一个表加表级的排它锁,肯定冲突。
  • X IX 冲突:一个事务想要对一个表加标记的排它锁,但是发现该表的 IX 已经被持有了,冲突。
  • 两个意向锁之间绝不会冲突。

记录锁(Record Locks)

例如:

SELECT * FROM ql_tx_test.`ql_tx` WHERE ver_ = 0 FOR UPDATE;

它会对 ver_ = 0 的所有记录加上排他锁(ver_有索引的前提下)。

这里我们根据索引建立的情况,看看三种可能发生的场景:

  1. ver_ 字段是唯一索引:锁住一条记录。
  2. ver_ 字段是普通索引:锁住N条记录,N取决于有多少条记录 ver_ = 0。
  3. ver_ 字段没有索引:整张表所有的记录都会被锁上。

间隙锁(Gap Locks)

Gap Locks 的作用是为了阻止多个事务将记录插入到同一范围内,避免幻读问题的产生。

我们同样还是要结合索引建立情况,分三种情况讨论:

  • ver_ 字段是普通索引:【此时产生间隙锁】,(5,9)这个范围会被锁定,当我们在 事务B 中插入 ver_ = 7 的记录是,执行会被阻塞,事务A再次执行同样查询时返回相同的记录,幻读问题就可以避免。当我们在事务B中插入 ver_ = 11 的记录时,因为11不在锁定的范围,执行新增操作成功。
  • ver_ 字段是唯一索引:【此时产生间隙锁】,(5,9)这个范围会被锁定。
  • ver_ 字段没有索引:整张表全部记录都会被加锁。

临键锁(Next-Key Locks)

我们执行一条SQL语句:SELECT * FROM user WHERE id > 7 AND id < 11 FOR UPDATE,锁住的不是9这单个值,而是对(5,9]、(9,12] 这2个区间加了X锁。因此任何对于这个范围的插入都是不被允许的,从而避免幻读。

所以触发临键锁与触发间隙锁的区别就在于:查询条件范围的端点是否在索引上。不在,用临键锁。在,用间隙锁。

无锁

InnoDB中还有一种非常关键的技术,利用InnoDB的多版本控制,我们可以实现无锁。

InnoDB Multi-Versioning

InnoDB是一个多版本存储引擎。它保存了被修改记录的旧版本信息,用于一致性读(consistent read)和事务回滚。这些信息被存储在 Undo logs 中。

Undo logs 分为 insert和update 两种。insert undo logs 仅用作事务的回滚,事务提交之后就可以被移除。但是 update undo logs 还要用与一致性读,当没有事务持有该数据的快照的时候,update undo logs才可以被移除。

在 InnoBD 多版本体系中,当你DELETE一条数据的时候,该数据不会马上被物理移除,只有等待该DELETE语句对应的 update undo logs 被移除之后,对应的记录才会被物理的删除。

一致性读(Consistent Nonlocking Reads)

  • 如果是 RR 隔离级别,则同一事务中的所有一致性读都将读取该事务 第一次获取到的数据快照。
    • 上面这句话什么意思呢?假设您在默认的RR隔离级别上运行,当您执行一致性读(普通的 SELECT 语句)时,InnoDB会根据 timepoint 点查询当前时刻的数据库快照。如果另一个事务在该 timepoint 之后删除、插入和更新一行并提交,都不会看到相应的改变。简单来说,根据事务开始的timepoint去undo logs 中查该timepoint对应的数据库快照。
  • 如果是RC隔离级别,同一事务中的所有一致性读都是读取数据最新的快照。

非阻塞

一致性读是 InnoDB 在 RC 和 RR 隔离级别处理 SELECT 语句的默认模式。一致性读不会在它访问的表上设置任何锁,因此其他会话可以在对表执行一致性读的同时自由修改这些表。

Locking Reads

如果你执行先查询,存在插入或者更新一行数据的操作时,简单 SELECT 不能提供足够的保护。因为其他事务可以更新或删除您刚才查询的行。InnoDB 支持两种 locking read 以提供额外的安全保障。

  • SELECT ... LOCK IN SHARE MODE:在任何行上设置共享锁。其他事务可以读取这些行,但在事务提交之前不能修改它。
  • SELECT ... FOR UPDATE:在任何行上设置排它锁。其他事务进不能加读锁(LOCK IN SHARE MODE)也不能加写锁(FOR UPDATE)。

不同SQL的加锁策略

  • SELECT ... FROM :一致性读,读取的是数据的快照(RR是读取事务开始的快照,而RC则是读取最新的快照)。
  • SELECT ... LOCK IN SHARE MODE:在任何行上设置共享锁。其他事务可以读取这些行,但在事务提交之前不能修改它。
  • SELECT ... FOR UPDATE:在任何行上设置排它锁。其他事务进不能加读锁(LOCK IN SHARE MODE)也不能加写锁(FOR UPDATE)。
  • UPDATE ... WHERE ...:在每条搜索记录上设置一个独占的 next-key lock。但是,对于使用唯一索引锁定行以搜索唯一行的语句,只需索引记录锁定。
  • DELETE FROM ... WHERE ... :在每条搜索记录上设置一个独占的 next-key lock。但是,对于使用唯一索引锁定行以搜索唯一行的语句,只需索引记录锁定。
  • INSERT:在插入的行上设置排他锁。这个锁是索引记录锁,而不是 next-key lock(也就是说,没有间隙锁) ,并且不会阻止其他事务插入到插入的行之前的间隙中。

不同隔离级别的加锁策略

下面的列表描述了MySQL如何支持不同的事务级别,该列表按照使用频率先后展示:

  • REPEATABLE READ
    • 普通的 SELECT:同一事务内读取由第一次读取建立的快照。
    • locking read:(SELECT … FOR UPDATE ,SELECT … LOCK IN SHARE MODE,UPDATE和DELETE):则取决于查询条件使用的是唯一索引还是范围查询
      • 如果是唯一索引,仅仅是锁住一条索引记录。
      • 如果是非唯一索引,会加间隙锁或者临键锁以保证没有其他的事务往间隙中插入数据。
  • READ COMMITTED
    • 对于普通的 SELECT:总是读取数据最新的快照(fresh snapshot),所以存在不可重复读问题。
    • 对于 locking read:仅加记录锁,不会加间隙锁和临键锁,所以存在幻读问题。
  • READ UNCOMMITTED:最自由的隔离级别,可以读其他事务都没提交的数据【脏读】
  • SERIALIZABLE:读加共享锁,写加排他锁,读写互斥。

Q&A

为什么RC相对RR可以提高并发度?

很多公司为了提高并发度,没有使用RR,而是使用RC。从上面可以看出,RC隔离级别是不加间隙锁的,可能有人就会说,那我一般也不会使用 locking read啊。兄弟,不要忘记了,update、insert、delete都属于 locking read,一旦是locking read 就有可能加间隙锁。所以RC带来的性能提升就是locking read 不会加间隙锁,并发度自然上来。

参考资料

  • MySQL官方文档
举报

相关推荐

0 条评论