InnoDB 锁的类型有如下几种:



官网文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

意向锁


意向锁表示的是下一层级需要加什么类型的锁,由 InnoDB 在数据操作之前自动加上,不需要用户干预。


意向锁不好理解,我们先来了解一下数据库中加锁的过程,通过加锁的过程来理解意向锁。


另外,在官方文档中说到 InnoDB 存储引擎中的意向锁都是表锁,其实描述的不是太合理,这个在下面也会说明,为什么意向锁是表锁。

加锁的过程

图片.png

假如此时有一个事务 tx1 需要对记录 A 加 X 锁,它的执行过程如下所示:


  1. 在该记录所在的数据库上加一把意向锁 IX。
  2. 在该记录所在的表上加一把意向锁 IX。
  3. 在该记录所在的页上加一把意向锁 IX。
  4. 最后在该记录 A 上加一把 X 锁。


假如此时有一个事物 tx2 需要对记录 B(假设和记录 A 在同一个页中)加 S 锁,它的执行过程如下所示:


  1. 在该记录所在的数据库上加一把意向锁 IS。
  2. 在该记录所在的表上加一把意向锁 IS。
  3. 在该记录所在的页上加一把意向锁 IS。
  4. 最后在该记录 B 上加一把 S 锁。


一般数据库中的加锁操作是从上往下,逐层进行加锁的,它不是只对某条记录进行加锁。


锁的释放过程:


锁释放的过程和加锁的过程是反过来的,是先释放记录锁,再释放页锁,再释放表锁,最后释放数据库锁。

锁的兼容性

锁兼容

X

IX

S

IS

X

冲突

冲突

冲突

冲突

IX

冲突

兼容

冲突

兼容

S

冲突

冲突

兼容

兼容

IS

冲突

兼容

兼容

兼容

共享锁(S)和排它锁(X)的兼容性比较好理解,除了 S 和 S 锁兼容以外,其他锁之间是互相不兼容的。


但是引入了意向锁(IS、IX)之后,就不好理解了,意向锁 IS 和 IX 之间都是相互兼容的因为意向锁仅仅表示下一层级加什么类型的锁,不代表当前层加什么类型的锁。


假如此时有一个事物 tx3 需要对记录 A 加 S 锁,它的执行过程如下所示:


  1. 在该记录所在的数据库上加一把意向锁 IS。
  2. 在该记录所在的表上加一把意向锁 IS。
  3. 在该记录所在的页上加一把意向锁 IS。
  4. 最后需要在在该记录 A 上加一把 S 锁,发现该记录上有一个 X 锁( tx1 的 X 锁 ),而 S 锁和 X 锁之间是互斥的,那么 tx3 需要等待,直到 tx1 进行 commit 后,才能加上。

意向锁的作用


为什么数据库要设计意向锁?


别急,先来了解一个概念,什么叫多粒度的锁?


多粒度锁的意思是在数据库中不但能实现行级别的锁,也能实现页级别的锁、表级别的锁和数据库级别的锁。


假设我们要对某一张表加一把 S 锁,如果没有意向锁,就不知道表下面是否有记录在被修改,就需要对这张表下的所有记录都进行加锁操作,这样操作代价很大,且其他事物新插入的记录(游标已经扫过的范围)都没有加锁,此时就没有实现锁表的功能。


如果有意向锁的话,假设此时有一个事务 tx4 需要对表4(记录A所在的表)加一把 S 锁,它的执行过程如下所示:


  1. 在该表所在的数据库上加一把意向锁 IS。
  2. 接下来需要在该表上加一把 S 锁,发现该表上有一把 IX 锁(tx1 的 IX 锁),而 S 锁和 IX 锁之间是互斥的,那么 tx4 需要等待,直到 tx1 进行 commit 后,才能加上,这样就实现了表级别的锁。


综上所述,所以意向锁是为了实现多粒度的锁而设计的。


演示案例:

-- IS锁的意义
-- 事务A执行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE id=1;

ROLLBACK;

-- 事务B执行
-- 因为没有通过索引条件进行数据检索,所以这里加的是表锁
UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13777777777';

image.png

结论:事务B的 SQL 因为没有通过索引条件进行数据检索,所以这里加的是表锁,在对表加锁之前会查看该表是否已经存在了意向锁,因为事务A已经获得了该表的意向锁了,所以事务B不需要判断每一行数据是否已经加锁,可以快速通过意向锁阻塞当前 SQL 的更新操作。


层层加锁很麻烦,会很耗性能么?


有人可能认为层层加锁,比较麻烦,且好性能。其实不然,因为大部分情况下,在这些层级加的都是意向锁,而意向锁之间是相互兼容的,加锁速度很快,另外这些操作都是内存中完成的,锁的信息是存放在内存中的,所以是很快的。


意向锁的好处是显而易见的,它可以实现多粒度级别的锁,避免在实现表锁的时候对表中的所有记录加锁。

为什么在 InnoDB 中意向锁是表锁


最后来回答为什么意向锁是表锁,因为 InnoDB 没有数据库、页级别的锁,只有表和记录级别的锁,意向锁只能加在表上面,所以说意向锁是表锁。

共享锁(Shared Locks)


共享锁:又称为读锁,简称 S 锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。


通过如下代码,加锁和释放锁:

-- 加锁
select * from users WHERE id=1 LOCK IN SHARE MODE;

-- 释放锁:提交事务 or 回滚事务
commit;
rollback;

演示案例:

-- 共享锁
-- 事务A执行
BEGIN;

SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE;

ROLLBACK;
COMMIT;

-- 事务B执行
SELECT * FROM users WHERE id=1;

UPDATE users SET age=19 WHERE id=1;
  1. 事务A手动开启事务,执行语句获取共享锁,注意这里没有提交事务
  2. 事务B分别执行 SELECT 和 UPDATE 语句,查看执行效果

image.png

结论:UPDATE 语句被锁住了,不能执行。在事务A获得共享锁的情况下,事务B可以执行查询操作,但是不能执行更新操作。

排他锁(Exclusive Locks)


排它锁:又称为写锁,简称 X 锁,排他锁不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的锁(共享锁、排他锁),只有该获取了排他锁的事务是可以对数据行进行读取(当前读)和修改操作(其他事务要读取数据可来自于快照)。


通过如下代码,加锁和释放锁:

-- 加锁
-- delete / update / insert 默认加上X锁
-- SELECT * FROM table_name WHERE ... FOR UPDATE
-- 释放锁:提交事务 or 回滚事务
commit;
rollback;

演示案例:

-- 排它锁
-- 事务A执行
BEGIN;

UPDATE users SET age=23 WHERE id=1;

COMMIT;
ROLLBACK;

-- 事务B执行
SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM users WHERE id=1 FOR UPDATE;
-- SELECT 可以执行,数据来自于快照
SELECT * FROM users WHERE id=1;
  1. 事务A手动开启事务,执行 UPDATE 语句,获取排它锁,注意这里没有提交事务
  2. 事务B分别执行三条语句,查看执行效果

image.png

结论:事务B的第一条 SQL 和第二条 SQL 语句都不能执行,都已经被锁住了,第三条 SQL 可以执行,数据来自于快照,关于这点后面会讲到。

行锁到底锁了什么


InnoDB 的行锁是通过给索引上的索引项加锁来实现的


只有通过索引条件进行数据检索,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁。


通过普通索引进行数据检索,比如通过下面例子中 UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';该 SQL 会在 name 字段的唯一索引上面加一把行锁,同时会在该唯一索引对应的主键索引上面也会加上一把锁,总共会加两把锁。


演示案例:


演示之前,先看一下 users 表的结构和数据内容。


image.png

image.png


-- 案例1
-- 事务A执行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13666666666';

ROLLBACK;

-- 事务B执行
UPDATE users SET lastUpdate=NOW() WHERE id=2;
UPDATE users SET lastUpdate=NOW() WHERE id=1;

-- 案例2
-- 事务A执行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE id=1;

ROLLBACK;

-- 事务B执行
UPDATE users SET lastUpdate=NOW() WHERE id=2;
UPDATE users SET lastUpdate=NOW() WHERE id=1;

-- 案例3
-- 事务A执行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';

ROLLBACK;

-- 事务B执行
UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';
UPDATE users SET lastUpdate=NOW() WHERE id=1;
UPDATE users SET lastUpdate=NOW() WHERE `name`='qingshan';
UPDATE users SET lastUpdate=NOW() WHERE id=2;


注意:这里演示的案例都是在事务A没有提交之前,执行事务B的语句。


案例1执行结果如下图所示:


image.png


案例2执行结果如下图所示:


image.png


案例3执行结果如下图所示:


image.png




自增锁(AUTO-INC Locks


定义


针对自增列自增长的一个特殊的表级别锁


通过如下命令查看自增锁的默认等级:


SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';


默认取值1,代表连续,事务未提交 ID 永久丢失。


演示案例


-- 事务A执行
BEGIN;
INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('tom2',30,'1344444444',NOW());
ROLLBACK;

BEGIN;
INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('xxx',30,'13444444444',NOW());
ROLLBACK;

-- 事务B执行
INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('yyy',30,'13444444444',NOW());


事务A执行完后,在执行事务B的语句,发现插入的 ID 数据不再连续,因为事务A获取的 ID 数据在 ROLLBACK 之后被丢弃了。




利用锁解决事务并发带来的问题


InnoDB 真正处理事务并发带来的问题不仅仅是依赖锁,还有其他的机制,下篇文章会讲到,所以这里只是演示利用锁是如何解决事务并发带来的问题,并不是 InnoDB 真实的处理方式。


利用锁怎么解决脏读


image.png


在事务B的更新语句上面加上一把 X 锁,这样就可以有效的解决脏读问题。


利用锁怎么解决不可重复读


image.png


在事务A的查询语句上面加上一把 S 锁,事务B的更新操作将会被阻塞,这样就可以有效的解决不可重复读的问题。


利用锁怎么解决幻读


image.png


在事务A的查询语句上面加上一把 Next-key 锁,通过临键锁的定义,可以知道这个时候,事务A会把 (-∞,+∞) 的区间数据都锁住,事务B的新增操作将会被阻塞,这样就可以有效的解决幻读的问题。



案例数据


在运行上面的演示案例之前,先导入表和测试数据:📎test.zip


作者:殷建卫

链接:https://www.yuque.com/yinjianwei/vyrvkf/ei0mep

来源:殷建卫 - 架构笔记

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。