前言
不同隔离级别下会出现的一致性问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | ✅ | ✅ | ✅ |
READ-COMMITTED | ❌ | ✅ | ✅ |
REPEATABLE-READ | ❌ | ❌ | ✅ |
SERIALIZABLE | ❌ | ❌ | ❌ |
脏读
:事务A可以读取到事务B修改但未提交
的数据。不可重复读
:同一个事务两次读取同一条记录
的结果不一致,重点在于update
和delete
。幻读
:同一事务两次读取同样范围
的记录,第二次返回了第一次没有的记录,重点在于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_有索引的前提下)。
这里我们根据索引建立的情况,看看三种可能发生的场景:
ver_ 字段是唯一索引
:锁住一条记录。ver_ 字段是普通索引
:锁住N条记录,N取决于有多少条记录 ver_ = 0。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对应的数据库快照。
- 上面这句话什么意思呢?假设您在默认的RR隔离级别上运行,当您执行一致性读(普通的 SELECT 语句)时,InnoDB会根据 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官方文档