InnoDB 锁的类型有如下几种:

  • S(shared lock):共享锁,行锁
  • X(exclusive lock):排它锁,行锁
  • IS(intention shared lock):意向锁共享锁,表锁
  • IX(intention exclusive lock):意向锁排它锁,表锁
  • AUTO-INC Locks:自增锁

官网文档: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 后,才能加上,这样就实现了表级别的锁。

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

演示案例:

  1. -- IS锁的意义
  2. -- 事务A执行
  3. BEGIN;
  4. UPDATE users SET lastUpdate=NOW() WHERE id=1;
  5. ROLLBACK;
  6. -- 事务B执行
  7. -- 因为没有通过索引条件进行数据检索,所以这里加的是表锁
  8. UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13777777777';

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

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

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

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

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

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

共享锁(Shared Locks)

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

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

  1. -- 加锁
  2. select * from users WHERE id=1 LOCK IN SHARE MODE;
  3. -- 释放锁:提交事务 or 回滚事务
  4. commit;
  5. rollback;

演示案例:

  1. -- 共享锁
  2. -- 事务A执行
  3. BEGIN;
  4. SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE;
  5. ROLLBACK;
  6. COMMIT;
  7. -- 事务B执行
  8. SELECT * FROM users WHERE id=1;
  9. UPDATE users SET age=19 WHERE id=1;
  1. 事务A手动开启事务,执行语句获取共享锁,注意这里没有提交事务
  2. 事务B分别执行 SELECT 和 UPDATE 语句,查看执行效果

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

排他锁(Exclusive Locks)

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

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

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

演示案例:

  1. -- 排它锁
  2. -- 事务A执行
  3. BEGIN;
  4. UPDATE users SET age=23 WHERE id=1;
  5. COMMIT;
  6. ROLLBACK;
  7. -- 事务B执行
  8. SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE;
  9. SELECT * FROM users WHERE id=1 FOR UPDATE;
  10. -- SELECT 可以执行,数据来自于快照
  11. 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() WHEREname='seven';该 SQL 会在 name 字段的唯一索引上面加一把行锁,同时会在该唯一索引对应的主键索引上面也会加上一把行锁,总共会加两把行锁。

演示案例:

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

image.png
image.png

  1. -- 案例1
  2. -- 事务A执行
  3. BEGIN;
  4. UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13666666666';
  5. ROLLBACK;
  6. -- 事务B执行
  7. UPDATE users SET lastUpdate=NOW() WHERE id=2;
  8. UPDATE users SET lastUpdate=NOW() WHERE id=1;
  9. -- 案例2
  10. -- 事务A执行
  11. BEGIN;
  12. UPDATE users SET lastUpdate=NOW() WHERE id=1;
  13. ROLLBACK;
  14. -- 事务B执行
  15. UPDATE users SET lastUpdate=NOW() WHERE id=2;
  16. UPDATE users SET lastUpdate=NOW() WHERE id=1;
  17. -- 案例3
  18. -- 事务A执行
  19. BEGIN;
  20. UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';
  21. ROLLBACK;
  22. -- 事务B执行
  23. UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';
  24. UPDATE users SET lastUpdate=NOW() WHERE id=1;
  25. UPDATE users SET lastUpdate=NOW() WHERE `name`='qingshan';
  26. UPDATE users SET lastUpdate=NOW() WHERE id=2;

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

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

image.png

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

image.png

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

image.png

自增锁(AUTO-INC Locks)

定义

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

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

  1. SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';

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

演示案例

  1. -- 事务A执行
  2. BEGIN;
  3. INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('tom2',30,'1344444444',NOW());
  4. ROLLBACK;
  5. BEGIN;
  6. INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('xxx',30,'13444444444',NOW());
  7. ROLLBACK;
  8. -- 事务B执行
  9. 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 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。