InnoDB的隔离级别和锁

前言

这次主要来聊聊InnoDB中隔离级别和锁,主要包括三个部分:各种隔离级别存在的问题,InnoDB 中锁的介绍,各种隔离级别是如何加锁的。

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

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

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

READ-UNCOMMITTED

SET SESSION TRANSACTION ISOLATION LEVEL 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

事务A对 id_ = 1 的记录进行修改,但是还没有提交。在事务B中就已经可以读取到了。

不可重复读 ✅

幻读 ✅

该隔离级别既然连脏读都存在(一个事务可以读取另外一个事务未提交的UPDATE数据),那么不可重复读、幻读 必然是存在的。

READ-COMMITTED

**SET SESSION TRANSACTION ISOLATION LEVEL 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

可以看到在 R-C 隔离级别下不会读取到其他事务未提交的数据,所以不存在脏读。

不可重复读 ✅

当前数据库快照

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;

可以看到,事务A两次读取 id_ = 1 的记录,结果不一样,这就是不可重复读。

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; # 查出两条

事务A两次读取同一范围记录,查询结果不一样。

REPEATABLE READ

SET SESSION TRANSACTION ISOLATION LEVEL 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 

事务A两次读取同一记录,不会因为其他事务对该记录进行了UPDATE而导致两次读取结果不一致。

幻读 ✅

当前数据库快照

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 两条记录

从上面的结果看来,RR隔离级别已经不存在幻读问题。但这是不严谨的,如果幻读定义为同一事务中两次查询同一范围的结果一致,那么确实是解决了。但是现实中,不可能都只是查询,存在的场景时先查询,后插入,那么这时候就出问题了。

我们继续上面的演示

事务A

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

可以看到,其实还是存在幻读问题,想要解决这个问题,我们可以使用间隙锁解决(后面会详细介绍)。

SERIALIZABLE

SERIALIZABLE 就不一一做演示了,跟RR隔离级流程基本一致。

InnoDB存储引擎中的锁

为了保证数据库事务一致性的性质,一般使用加锁这种方式。所以,想要了解不同隔离级别下的加锁策略,需要先了解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 的作用是为了阻止多个事务将记录插入到同一范围内,避免幻读问题的产生。

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

我们在事务A执行 SELECT * FROM ql_tx_test.ql_tx WHERE ver_ > 5 AND ver_ < 9 FOR UPDATE;

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

间隙锁唯一的目的是防止其他事务插入到统一范围。间隙锁可以共存,一个事务获取的间隙锁并不阻止另一个事务获取同一间隙的间隙锁。

临键锁(Next-Key Locks)

Next-Key Locks 是Record Lock 和Record之前的间隙的间隙锁的一种结合。

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

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

PS:间隙锁、临键锁,在RC隔离级别下是不生效的。

无锁

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 不会加间隙锁,并发度自然上来。

参考资料

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值