锁
概念
锁在现实中的意义为: 封闭的器物,以钥匙或暗码开启。
在计算机中的锁一般用来管理对共享资源的并发访问,比如我们java同学熟悉的 Lock, synchronized 等都是我们常见的锁。
当然在我们的数据库中也有锁用来控制资源的并发访问,这也是数据库和文件系统的区别之一。
开发者要懂锁吗?
通常来说对于一般的开发人员,在使用数据库的时候一般懂点 DQL(select),DML(insert,update,delete) 就够了。
比如有一点线上死锁了,如果你只会上面的,估计无从下手。
MySQL 体系
本次测试版本:MySQL V5.6.0
Mysql 体系架构
- 查看
查看所有的引擎
mysql> show engines \G
*************************** 1. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 2. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 3. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 4. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 5. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6. row ***************************
Engine: MEMORY
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 7. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 8. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
XA: YES
Savepoints: YES
*************************** 9. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
当前数据库默认的引擎
上面列举了这么多,我们用的最多的。
也就是 InnoDB
mysql> show variables like '%storage_engine%';
+----------------------------+--------+
| Variable_name | Value |
+----------------------------+--------+
| default_storage_engine | InnoDB |
| default_tmp_storage_engine | InnoDB |
| storage_engine | InnoDB |
+----------------------------+--------+
MyIsam vs InnoDB
对比项 | InnoDB | MyIsAM |
---|---|---|
事务 | 支持 | 不支持 |
锁 | 支持MVCC行锁 | 表锁 |
外键 | 支持 | 不支持 |
存储空间 | 存储空间由于需要高速缓存,较大 | 可压缩 |
适用场景 | 有一定量的update和Insert | 大量的select |
事务的隔离性
InnoDB 锁类型
首先看下有哪些类型的锁
S or X
在InnoDb中实现了两个标准的行级锁,可以简单的看为两个读写锁:
S-共享锁:又叫读锁,其他事务可以继续加共享锁,但是不能继续加排他锁。
X-排他锁: 又叫写锁,一旦加了写锁之后,其他事务就不能加锁了。
兼容性: 是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。
ps: 除了 S-S 不冲突之外,其他全部冲突。
意向锁
意向锁在 InnoDB 中是表级锁,和他的名字一样他是用来表达一个事务想要获取什么。
意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。
意向锁分为:
IS-意向共享锁: 表达一个事务想要获取一张表中某几行的共享锁。
IX-意向排他锁: 表达一个事务想要获取一张表中某几行的排他锁。
这个锁有什么用呢?为什么需要这个锁呢?
首先说一下如果没有这个锁,如果要给这个表加上表锁,一般的做法是去遍历每一行看看他是否有行锁,这样的话效率太低,而我们有意向锁,只需要判断是否有意向锁即可,不需要再去一行行的去扫描。
- 特点
(1) 首先,意向锁,是一个表级别的锁(table-level locking);
(2) 意向锁分为:
意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁
意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁
- 兼容性
在 InnoDB 中由于支持的是行级的锁,因此 InnboDB 锁的兼容性可以扩展如下:
. | IX | IS | X | S |
IX | 兼容 | 兼容 | 冲突 | 冲突 |
IS | 兼容 | 兼容 | 冲突 | 兼容 |
X | 冲突 | 冲突 | 冲突 | 冲突 |
S | 冲突 | 兼容 | 冲突 | 兼容 |
意向锁协议(intention locking protocol)并不复杂:
-
事务要获得某些行的S锁,必须先获得表的IS锁
-
事务要获得某些行的X锁,必须先获得表的IX锁
- 例子
select ... lock in share mode
要设置IS锁;
select ... for update
要设置IX锁;
自增长锁
自增长锁(Auto-inc Locks)是一种特殊的表锁机制,提升并发插入性能。
对于这个锁有几个特点:
-
表级别锁
-
在 sql 执行完就释放锁,并不是事务执行完。
-
对于
insert...select
大数据量插入会影响插入性能,因为会阻塞另外一个事务执行。 -
自增算法可以配置。
在 MySQL5.1.2 版本之后,有了很多优化,可以根据不同的模式来进行调整自增加锁的方式。
锁的模式
最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
mysql> show variables like 'innodb_autoinc_lock_mode';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 1 |
+--------------------------+-------+
在 MySQL 中 innodb_autoinc_lock_mode 有3种配置模式:0、1、2,分别对应”传统模式”, “连续模式”, “交错模式”。
-
传统模式: 也就是我们最上面的使用表锁。
-
连续模式: 对于插入的时候可以确定行数的使用互斥量,对于不能确定行数的使用表锁的模式。
-
交错模式: 所有的都使用互斥量,为什么叫交错模式呢,有可能在批量插入时自增值不是连续的,当然一般来说如果不看重自增值连续一般选择这个模式,性能是最好的。
例子
- 默认环境
MySQL,InnoDB,默认的隔离级别(RR),假设有数据表:
t(id AUTO_INCREMENT, name);
数据表中有数据:
1, shenjian 2, zhangsan 3, lisi
- 测试场景
事务A先执行,还未提交:
insert into t(name) values(xxx);
事务B后执行:
insert into t(name) values(ooo);
问:事务B会不会被阻塞?
解析
如果不等待。
(1) 事务 A 先执行 insert,会得到一条 (4, xxx) 的记录,由于是自增列,故不用显示指定id为4,InnoDB会自动增长,注意此时事务并未提交;
(2) 事务 B 后执行 insert,假设不会被阻塞,那会得到一条 (5, ooo) 的记录;
此时,并未有什么不妥,但如果 A 继续添加
(3) 事务 A 继续 insert
insert into t(name) values(xxoo);
会得到一条(6, xxoo)的记录。
(4) 事务 A 再 select
select * from t where id > 3;
得到的结果是:
4, xxx
6, xxoo
画外音:不可能查询到 5 的记录,再RR的隔离级别下,不可能读取到还未提交事务生成的数据。
咦,这对于事务A来说,就很奇怪了,对于 AUTO_INCREMENT 的列,连续插入了两条记录,一条是 4,接下来一条变成了 6,就像莫名其妙的幻影。
其他场景
- 如果没有设置为自增主键,会怎么样?
如果没有设置自增长,参见 插入意向锁
InnoDB 锁算法
如何去使用这些锁,还是得靠锁算法。
记录锁(Record-Lock)
记录锁是锁住记录的,这里要说明的是这里锁住的是索引记录,而不是我们真正的数据记录。
-
如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住。
-
如果没有表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。
-
如果要锁的列没有索引,则会进行全表记录加锁。
实例
select * from t where id=1 for update;
它会在 id=1 的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行。
- snapshot read
select * from t where id=1;
是快照读,它并不加锁。
间隙锁(GAP Lock)
间隙锁顾名思义锁间隙,不锁记录。
锁间隙的意思就是锁定某一个范围,间隙锁又叫 gap 锁,其不会阻塞其他的 gap 锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。
| 1 | (1, 3] | 3 | (3, 5] | 5 |
| | |
| | |
记录锁 GAP 锁 GAP 锁
目的
依然是上面的例子,InnoDB,RR:
t(id PK, name KEY, sex, flag);
表中有四条记录:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
这个SQL语句
select * from t
where id between 8 and 15
for update;
会封锁区间,以阻止其他事务 id=10 的记录插入。
- 为什么要阻止id=10的记录插入?
如果能够插入成功,头一个事务执行相同的SQL语句,会发现结果集多出了一条记录,即幻影数据。
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致不可重复读。
- RC 失效
如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
临键锁(Next-key Lock)
这个锁本质是记录锁加上 gap 锁。
在RR隔离级别下( InnoDB 默认),Innodb 对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。
- 为什么呢?
因为唯一索引能确定行数,而其他索引不能确定行数,有可能在其他事务中会再次添加这个索引的数据会造成幻读。
这里也说明了为什么 Mysql 可以在 RR 级别下解决幻读。
- 范围
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。
例子
还是上面例子的数据。
PK上潜在的临键锁为:
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]
- 目的
临键锁的主要目的,也是为了避免幻读(Phantom Read)。
- 失效场景
如果把事务的隔离级别降级为RC,临键锁则也会失效。
插入意向锁
官方解释
插入意图锁定是在行插入之前通过插入操作设置的间隙锁定类型。
这个锁表示要以这样一种方式插入: 如果多个事务插入到相同的索引间隙中,如果它们不在间隙中的相同位置插入,则无需等待其他事务。
假设有值为4和7的索引记录。尝试分别插入值为5和6的独立事务,在获取插入行上的独占锁之前,每个事务都用insert intention locks锁定4和7之间的间隙,但不会因为行不冲突而互相阻塞。
- 理解
可以看出插入意向锁是在插入的时候产生的,在多个事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
这里要说明的是如果有间隙锁了,插入意向锁会被阻塞。
作用
对已有数据行的修改与删除,必须加强互斥锁X锁,那对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?
插入意向锁,孕育而生。
插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。
- 高并发插入
多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
例子
- 场景
在MySQL,InnoDB,RR下:
t(id unique PK, name);
数据表中有数据:
10, shenjian 20, zhangsan 30, lisi
事务A先执行,在10与20两条记录中插入了一行,还未提交:
insert into t values(11, xxx);
事务B后执行,也在10与20两条记录中插入了一行:
insert into t values(12, ooo);
- 问题
(1) 会使用什么锁?
(2) 事务 B 会不会被阻塞呢?
- 答案
回答:
虽然事务隔离级别是RR,虽然是同一个索引,虽然是同一个区间,但插入的记录并不冲突,故这里:
-
使用的是插入意向锁
-
并不会阻塞事务 B
MVCC
测试
准备工作
- mysql 版本号
$ mysql --version
/usr/local/mysql/bin/mysql Ver 14.14 Distrib 5.6.30, for osx10.11 (x86_64) using EditLine wrapper
- 建表
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`comment` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT = 0;
- 初始化数据
INSERT INTO `user`(id, name, comment) VALUES (1, '1', '1');
INSERT INTO `user`(id, name, comment) VALUES (5, '5', '5');
INSERT INTO `user`(id, name, comment) VALUES (9, '9', '9');
- 默认隔离级别
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
- 设置自动提交为 false
SET AUTOCOMMIT=0; -- 禁止自动提交
如果想恢复:
SET AUTOCOMMIT=1; -- 开启自动提交
实验一
测试过程
- 事务 A
begin;
select * from user where name = '5' for update;
- 事务 B
执行事务 A 后执行事务 B,
BEGIN;
INSERT INTO `user`(id, name, comment) VALUES (6, '6', '6');
COMMIT;
会发现事务 B 如下:
[2018-09-03 15:50:54] [41000][1205] Lock wait timeout exceeded; try restarting transaction
疑问
此处是对 name=’5’ 的元素进行加锁,为何插入新元素报错?
一探究竟
- 锁的信息
select * from information_schema.INNODB_LOCKS
查询结果
mysql> select * from information_schema.INNODB_LOCKS;
+------------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
+------------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 2898702:2752:4:4 | 2898702 | X,GAP | RECORD | `test`.`user` | index_name | 2752 | 4 | 4 | '9', 9 |
| 2898701:2752:4:4 | 2898701 | X,GAP | RECORD | `test`.`user` | index_name | 2752 | 4 | 4 | '9', 9 |
+------------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
- 原因
事务 A 给数据【5】加上了 next-key 锁。
事务 B 插入的时候会首先进行插入意向锁的插入,由于间隙锁和插入意向锁的冲突,导致了阻塞。
实验二
上面的 name 使用的是普通的非唯一索引,如果使用主键会怎么样呢?
测试过程
- 事务 A
begin;
select * from user where id = '5' for update;
- 事务 B
执行事务 A 后执行事务 B,
BEGIN;
INSERT INTO `user`(id, name, comment) VALUES (6, '6', '6');
COMMIT;
疑问
居然发现事务 B 并没有发生阻塞,why?
原因
执行事务 B 的时候,暂时不 commit。
可以看到对应的加锁情况如下:
mysql> select * from information_schema.INNODB_LOCKS;
+------------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
+------------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 2898667:2752:3:5 | 2898667 | S | RECORD | `test`.`user` | PRIMARY | 2752 | 3 | 5 | 6 |
| 2898663:2752:3:5 | 2898663 | X | RECORD | `test`.`user` | PRIMARY | 2752 | 3 | 5 | 6 |
+------------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
原来是因为唯一索引会降级记录锁,这么做的理由是:
非唯一索引加 next-key 锁由于不能确定明确的行数有可能其他事务在你查询的过程中,再次添加这个索引的数据,导致隔离性遭到破坏,也就是幻读。
唯一索引由于明确了唯一的数据行,所以不需要添加间隙锁解决幻读。
实验三
那如果是没有索引的普通字段呢?
测试过程
- 事务 A
begin;
select * from user where id = '5' for update;
- 事务 B
执行事务 A 后执行事务 B,
BEGIN;
INSERT INTO `user`(id, name, comment) VALUES (6, '6', '6');
COMMIT;
or
BEGIN;
INSERT INTO `user`(id, name, comment) VALUES (12, '12', '12');
COMMIT;
疑问
发现上面 2 个数据都无法插入。
咋不管是用实验1非间隙锁范围的数据,还是用间隙锁里面的数据都不行,难道是加了表锁吗?
的确,如果用没有索引的数据,其会对所有聚簇索引上都加上 next-key 锁。
所以大家平常开发的时候如果对查询条件没有索引的,一定进行一致性读,也就是加锁读,会导致全表加上索引,会导致其他事务全部阻塞,数据库基本会处于不可用状态。
死锁
概念
死锁: 是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。说明有等待才会有死锁,解决死锁可以通过去掉等待,比如回滚事务。
解决死锁的两个办法
等待超时:当某一个事务等待超时之后回滚该事务,另外一个事务就可以执行了,但是这样做效率较低,会出现等待时间,还有个问题是如果这个事务所占的权重较大,已经更新了很多数据了,但是被回滚了,就会导致资源浪费。
等待图(wait-for-graph): 等待图用来描述事务之间的等待关系,当这个图如果出现回路如下:
t1->t2->t3->t4->t1
就出现回滚,通常来说 InnoDB 会选择回滚权重较小的事务,也就是 undo 较小的事务。
问题复现
基本需要的基本功都有了,于是在自己的本地表中开始复现这个问题:
场景
时间点 | 事务A | 事务B |
---|---|---|
1 | begin; | begin; |
2 | delete from user where name = ‘777’; | delete from user where name = ‘666’; |
3 | insert user select 27,’777’,’777’; | insert user select 26,’666’,’666’; |
4 | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | Query OK, 1 row affected (14.32 sec) Records: 1 Duplicates: 0 Warnings: 0 |
解释
可以看见事务A出现被回滚了,而事务B成功执行。
具体每个时间点发生了什么呢?
- 时间点2
事务A删除name = ‘777’的数据,需要对777这个索引加上next-Key锁,但是其不存在,所以只对555-999之间加间隙锁,同理事务B也对555-999之间加间隙锁。间隙锁之间是兼容的。
- 时间点3
事务A,执行Insert操作,首先插入意向锁,但是555-999之间有间隙锁,由于插入意向锁和间隙锁冲突,事务A阻塞,等待事务B释放间隙锁。事务B同理,等待事务A释放间隙锁。于是出现了A->B,B->A回路等待。
- 时间点4
事务管理器选择回滚事务A,事务B插入操作执行成功。
修复BUG
这个问题总算是被找到了,就是因为间隙锁,现在需要解决这个问题,这个问题的原因是出现了间隙锁,那就来去掉他吧:
方案一: 隔离级别降级为RC,在RC级别下不会加入间隙锁,所以就不会出现毛病了,但是在RC级别下会出现幻读,可提交读都破坏隔离性的毛病,所以这个方案不行。
方案二: 隔离级别升级为可序列化,小明经过测试后发现不会出现这个问题,但是在可序列化级别下,性能会较低,会出现较多的锁等待,同样的也不考虑。
方案三: 修改代码逻辑,不要直接删,改成每个数据由业务逻辑去判断哪些是更新,哪些是删除,那些是添加,这个工作量稍大,小明写这个直接删除的逻辑就是为了不做这些复杂的事的,所以这个方案先不考虑。
方案四: 较少的修改代码逻辑,在删除之前,可以通过快照查询(不加锁),如果查询没有结果,则直接插入,如果有通过主键进行删除,在之前第三节实验2中,通过唯一索引会降级为记录锁,所以不存在间隙锁。
经过考明选择了第四种,马上进行了修复,然后上线观察验证,发现现在已经不会出现这个Bug了,这下总算能睡个安稳觉了。
拓展阅读
参考资料
- juejin
https://juejin.im/post/5b6c5be86fb9a04fb30a2bc7
- 58
- 大家
http://hedengcheng.com/?p=771#_Toc374698317
《Mysql技术内幕-InnoDB引擎》