在 MySQL中,InnoDB 不是直接锁住磁盘上的数据行,而是锁住该行数据所对应的索引项。即使你没有为表定义任何主键(PRIMARY KEY),InnoDB 也会自动为你生成一个隐藏的、名为 DB_ROW_ID 的聚簇索引来组织数据。所以,每张 InnoDB 表至少有一个索引,如果你在没有二级索引(即你自己创建的索引)的列上进行查询,InnoDB 就不得不退而求其次,使用这个隐藏的聚簇索引(或主键)来锁定记录。

InnoDB 支持 三种行锁定方式,分别是记录锁(Record Lock)间隙锁(Gap Lock)临键锁(Next-Key Lock)

特性记录锁 (Record Lock)间隙锁 (Gap Lock)临键锁 (Next-Key Lock)
核心作用锁定索引中的一条具体记录。防止其他事务对这条已存在的记录进行修改或删除,允许在间隙中插入。锁定索引记录之间的间隙(一个开区间)。防止其他事务在范围内插入新的记录,从而解决幻读问题。记录锁与间隙锁的组合。锁定一条记录及其之前的间隙(一个左开右闭区间)。InnoDB默认的加锁单位,同时防止幻读和当前记录被修改。
生动类比锁住一个座位:防止已经坐在这里的人被换掉,但允许其他空位坐人。锁住两个座位间的空档:不允许任何人在这个空档加塞插入新的座位,但允许修改现有的座位。锁住一个座位及其前面的空档:既不允许加塞,也不允许这个座位上的人被换掉。
锁定范围特定的、已存在的索引项。一个区间,例如 (3, 5),不包含任何现有记录。一个区间,例如 (3, 5],包含右边界的记录。
兼容性与其他事务对不同记录的S/X锁兼容。与同一记录的X锁不兼容。Gap锁之间可以兼容。即使两个事务锁定了相同的间隙,也允许,因为它们目的都是防止插入。本质上是对记录+间隙的锁定,兼容性遵循其组成部分的规则。
性能影响开销小。标准的行锁,并发性能高。开销中等。虽然锁的是间隙,但仍会降低插入操作的并发性。这是为解决幻读付出的必要代价。开销最大。默认模式,提供了最强的隔离性(防止幻读),但也带来了最大的性能开销和并发约束。
主要解决脏读、不可重复读(针对已存在的记录)。幻读(针对范围插入)。幻读(InnoDB在RR隔离级别下解决幻读的主要手段)。

下面有两个例子分别具体介绍记录锁 (Record Lock)、间隙锁 (Gap Lock):

1. 记录锁 (Record Lock)

准备两个窗口A、B。(同时启动同时A、B窗口需要保持同级事务隔离级别)

进入A窗口看看A窗口的隔离级别:

SHOW VARIABLES LIKE 'transaction_isolation';

当前事务隔离等级为可重复读

在B窗口也是同理,结果是一样的,不一样你就手动调成一样。

在A窗口准备一张表格,我准备的dotcpp_user如下:

dotcpp_user表格

在A窗口中开启事务并修改dotcpp_user表:

BEGIN;
UPDATE dotcpp_user SET hobby = "睡懒觉" WHERE id = 4;

同样的操作在B窗口进行:

BEGIN;
UPDATE dotcpp_user SET hobby = "睡懒觉" WHERE id = 4;

be like:

设置事务并进行修改

此时没有Query OK这一行提示,UPDATE一直在执行,这是为什么呢?

你不语,只是打开A窗口提交事务:

COMMIT;

回到B窗口发现此时执行成功:

B窗口完成修改

看一下修改了没有:(此时第四个用户爱好变成“睡懒觉”)

A窗口提交事务后B窗口修改成功

原来,多个事务同时更新一条记录,需要等待前一个事务把锁释放才能进行修改,这里B窗口一直没有成功修改记录直到A窗口提交记录后才能修改,这就有力说明这一点!

此时我们查看MySQL 中 InnoDB 存储引擎的状态:

SHOW ENGINE innodb status;

得到结果(重点看这部分):

TRANSACTIONS
------------
Trx id counter 2405
Purge done for trx's n:o < 2401 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 284587497428864, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 284587497428088, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 284587497427312, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 2404, ACTIVE 9 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 36, OS thread handle 2256, query id 186 localhost ::1 Dotcpp updating
UPDATE dotcpp_user SET hobby = "?????" WHERE id = 4
------- TRX HAS BEEN WAITING 9 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4 page no 4 n bits 72 index PRIMARY of table `dotcpp`.`dotcpp_user` trx id 2404 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000004; asc     ;;
 1: len 6; hex 00000000095c; asc      \;;
 2: len 7; hex 02000001210151; asc     ! Q;;
 3: len 12; hex 646f74637070557365723034; asc dotcppUser04;;
 4: len 9; hex e79da1e68792e8a789; asc          ;;
------------------
---TRANSACTION 2403, ACTIVE 35 sec
2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 20, OS thread handle 2232, query id 184 localhost ::1 Dotcpp
--------

对于B窗口的这个修改语句:“UPDATE dotcpp_user SET hobby = "?????" WHERE id = 4“,修改需要申请一个排他锁”lock_mode X locks rec but not gap waiting“,这是因为InnoDB 的规定:为了修改一行数据,InnoDB 必须为该行加上排他锁(X Lock)。排他锁的特性是:如果其他事务已经持有了这行数据的任何锁(共享锁S或排他锁X),则当前事务必须等待。

当超过事务等待锁允许的最大时间,此时会报错(1205:超过锁等待超时时间;尝试重新启动事务):

1205报错

及当前事务执行失败,则自动执行回滚操作。

MySQL 数据库采用 InnoDB 模式,默认参数 innodb_lock_wait_timeout 设置锁等待的时间是 50s,一旦数据库锁超过这个时间就会报错。

我们可以通过这个变量innodb_lock_wait_timeout查看当前数据库锁等待的时间

SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';

前数据库锁等待的时间


2. 间隙锁 (Gap Lock)

准备A、B两个窗口,在A窗口为dotcpp_user表中的id 字段添加唯一性限制,同时开启事务并修改id=1的用户爱好为"睡懒觉";

这里添加主键索引注意id是否曾设置主键哈,有的就自行跳过,不知道的就输入 "show index from yourTable;" 看看你的id是否设置主键。

ALTER TABLE dotcpp.dotcpp_user ADD unique key  index_id(id);
BEGIN;
UPDATE dotcpp.dotcpp_user SET hobby = '睡懒觉'  WHERE id = 1;

在B窗口开启一个事务并修改:id=2的用户爱好为"睡懒觉";

BEGIN;
UPDATE dotcpp.dotcpp_user SET hobby = '睡懒觉'  WHERE id = 2;

此时分别提交A、B窗口的事务,查看表dotcpp_user内容:

SELECT * FROM dotcpp.dotcpp_user;

dotcpp_user完成更新

可以看到两个id=1、2的用户都变成了"睡懒觉",由此我们知道,InnoDB 的行级锁采用间隙锁机制,仅锁定目标记录,因此不同事务可以并发更新互不冲突的数据行。

点赞(0)

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

Dotcpp在线编译      (登录可减少运行等待时间)