目录
InnoDB 锁的基本类型
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
官网把InnoDB的锁分成了8 类。我们把前面的两个行级别的锁(Shared and Exclusive Locks),和两个表级别的锁(Intention Locks)称为锁的基本模式。
后面三个Record Locks、Gap Locks、Next-Key Locks,我们把它们叫做锁的算法,也就是在什么情况下会锁定什么范围。
锁的基本模式
InnoDB 里面既有行级别的锁,又有表级别的锁。
表锁与行锁的区别:
锁定粒度 | 表锁 > 行锁 |
加锁效率 | 表锁 > 行锁 |
冲突概率 | 表锁 > 行锁 |
并发性能 | 表锁 < 行锁 |
共享锁(Shared Locks )
共享锁,又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享同一把锁,都能访问到数据,但是只能读不能修改和删除。
加锁释锁方式:
select * from user where id=1 LOCK IN SHARE MODE;
commit/rollback;
排它锁(Exclusive Locks)
排它锁,又称为写锁,简称X锁,排他锁不能与其他锁并存,如一个事务获取 了一个数据行的排他锁,其他事务就不能再获取该行的锁(包括共享锁、排他 锁),只有该获取了排他锁的事务是可以对该数据行进行读取和修改操作的。
加锁释锁方式:
自动:delete / update / insert 默认加上X锁;
手动:select * from student where id=1 FOR UPDATE;
释放:commit/rollback
意向锁
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁 。
意向共享锁(Intention Shared Lock,简称IS锁)
表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(Intention Exclusive Lock,简称IX锁)
表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前 必须先取得该表的IX锁。
意向锁的作用:
1.意向锁是一个表锁,这样在InnoDB里面就可以支持更多粒度的锁。
2.提高加锁的效率
如果没有意向锁,当我们准备给一张表加上表锁的时候,我们就需要去判断有没其他的事务锁定了其中了某些行,如果有的话,肯定不能加上表锁。那么这个扫描整张表去判断是否能成功加上表锁的操作是非常耗时且效率低下的。
有了意向锁之后就可以解决这个问题了。只需要判断这张表上有没有意向锁,如果有意向锁,就直接返回加表锁失败。如果没有,就代表可以加锁成功。
锁的原理
情形1:没有索引
#没有索引的表t1
CREATE TABLE `t1` (
`id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t1` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t1` (`id`, `name`) VALUES (2, '2');
INSERT INTO `t1` (`id`, `name`) VALUES (3, '3');
INSERT INTO `t1` (`id`, `name`) VALUES (4, '4');
#事务1中使用独占锁查询id=1
begin;
SELECT * FROM t1 WHERE id=1 for update;
#事务2中使用独占锁查询id=3
select * from t1 where id=3 for update;
#事务3中插入id=5
INSERT INTO `t1` (`id`, `name`) VALUES(5, '5');
说明没有索引查询会锁住整张表
情形2:主键索引
#有索引的表t2(主键索引id)
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t2` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t2` (`id`, `name`) VALUES (5, '5');
INSERT INTO `t2` (`id`, `name`) VALUES (9, '9');
INSERT INTO `t2` (`id`, `name`) VALUES (14, '14');
#事务1中使用独占锁查询id=1
begin;
SELECT * FROM t2 WHERE id=1 for update;
#事务2中使用独占锁查询id=1
SELECT * FROM t2 WHERE id=1 for update;
#事务3中使用独占锁查询id=5
SELECT * FROM t2 WHERE id=5 for update;
这说明对于有索引的表使用相同的id值去加锁,会冲突;使用不同的id加锁,可以加锁成功(这跟我们平时关于锁的认知也是一致的)
情形3:辅助索引
#主键索引id,唯一索引name
CREATE TABLE `t3` (
`id` int(11) ,
`name` varchar(255) ,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t3` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t3` (`id`, `name`) VALUES (5, '5');
INSERT INTO `t3` (`id`, `name`) VALUES (9, '9');
INSERT INTO `t3` (`id`, `name`) VALUES (14, '14');
#事务1中使用独占锁查询name=5
begin;
SELECT * FROM t3 WHERE name=5 for update;
#事务2中使用独占锁查询name=5
SELECT * FROM t3 WHERE name=5 for update;
#事务3中使用独占锁查询id=5
SELECT * FROM t3 WHERE id=5 for update;
InnoDB的行锁,是通过锁住索引来实现的
如果锁住的是索引,一张表没有索引怎么办?
前面关于索引的博客里说到过:
1)如果我们定义了主键(PRIMARYKEY),那么 InnoDB 会选择主键作为聚集索引。
2)如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。
3)如果也没有这样的唯一索引,InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行记录的写入而递增。
所以如果查询没有使用到索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了,也就是锁表。这也是为什么要求我们建立索引,一个是提高查询效率,一个是提高并发性。
对于辅助索引,在InnoDB中辅助索引树里会存储二级索引和主键的值。所以我们通过辅助索引锁定一行数据的时候,其锁定的步骤跟我们检索数据的步骤是一样的,会通过辅助索引找到主键索引,然后也锁定 (这跟我们通常理解的锁定数据的概念也是一样的)所以在InnoDB中,一旦锁定了某个索引,可以理解为该记录行都已经被锁住
锁的算法
InnoDB里有三种锁的算法:Record Lock,Gap Lock,Next-Key Lock
#有索引的表t2(主键索引id)
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t2` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t2` (`id`, `name`) VALUES (5, '5');
INSERT INTO `t2` (`id`, `name`) VALUES (9, '9');
INSERT INTO `t2` (`id`, `name`) VALUES (14, '14');
先说明下三种范围的概念:因为t2只有主键索引,所以这里的划分标准就是当前主键索引的值
我们把它数据库里面存在的主键值,叫做Record---记录,这里就有4个Record。
根据存在的Record,将整个区域隔开的一个个区间,对于数据不存在的区间,称之为 Gap--间隙,是一个左开右开的区间。
将间隙(Gap)连同它左边的记录(Record),一起称之为Next-Key,即临键的区间,这是一个左开右闭的区间。
记录锁
对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,如果能够精确匹配到一条记录,这个时候使用的就是记录锁
前面我们使用不同的id去查询加锁的时候就验证过了,此时查询不同的id并给其加排他锁不会发生冲突,说明只锁住这个record
间隙锁
如果我们查询的记录不存在,没有命中任何一个record,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁,会锁住一个间隙区间。间隙锁定是对索引记录之间的间隙的锁定,或者是对第一个或最后一个索引记录之间的间隙的锁定
InnoDB会将索引id的区间进行划分,分为:(-infinite,1),(1,5),(5,9),(9,14),(14,+infinite)五个区间;这里锁住的是(5,9)区间
如果其他事务想在(5,9)区间内插入新的数据记录,将会阻塞,如上图
间隙锁主要是阻塞插入insert。相同的间隙锁之间不冲突,如下图:
猜测是因为Gap锁出现的条件是没有命中任何一个record,由于已经将整个间隙锁住,那么相同的间隙锁的查询结果都会和之前的一致,也就没必要产生冲突
Gap间隙锁锁定范围
假设SQL里where中范围查找中的最小值、最大值分别为sMin和sMax,则锁定区间(lMin,lMax)满足如下条件:
- lMin的值为表索引值中小于or等于sMin中的最大值,对于sMin=6的情况,lMin=5。
- lMax的值为表索引值中大于or等于sMax中的最小值,对于sMax=6的情况,lMax=9.
Gap Lock 只在 RR 中存在。如果要关闭间隙锁,就需要把事务隔离级别设置成RC或者把innodb_locks_unsafe_for_binlog设置为ON(现已弃用)。这种情况下除了外键约束和唯一性检查会加间隙锁,其他情况都不会使用间隙锁。
临键锁
如果我们使用了范围查询,不仅仅命中了Record记录,还包含了 Gap间隙,在这种情况下我们使用的就是临键锁,它是MySQL里面默认的行锁算法,相当于记录锁加上间隙锁,。
其他两种退化的情况:
如果使用唯一性索引等值查询匹配到一条记录的时候,会退化成记录锁。
如果没有匹配到任何记录的时候,会退化成间隙锁。
会锁住命中的当前Next-Key区间和下一个Next-Key区间,这里是(5,9],(9,14]
试验不难发现,在其他事务下尝试在Next-Key Lock锁住的区间内插入数据会被阻塞,如下图
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t2 values (8,'8');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t2 values (12,'12');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
但是在其他事务查询Next-Key Lock锁住区间内不存在的记录不会被阻塞,但是查询存在的记录会被阻塞
我的理解:对于区间内不存在的记录,由于插入会被阻塞,所以其他事务不可能插入,也就是说查询的时候一定不会查询到数据,也就不用担心会出现幻读的问题 ;没必要去阻塞; 但是对于存在的记录,在事务1内可能发生变化,所以不能被其他事务锁定,这跟间隙锁阻塞插入insert,但是相同的间隙锁之间不冲突的道理是一样的,都是为了提高SQL的并发性。执行结果如下图:
mysql> select * from t2 where id = 8 for update;
Empty set (0.00 sec)
mysql> select * from t2 where id = 13 for update;
Empty set (0.00 sec)
mysql> select * from t2 where id = 15 for update;
Empty set (0.00 sec)
mysql> select * from t2 where id = 14 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
注意区间范围 : 这里Next-Key Lock锁住的区间会包含最大命中的记录的下一个区间
看下官网的描述
A next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
这里说临键锁会锁住对于的记录和记录前面的gap,防止其他事务在此区间内进行插入
When InnoDB scans an index, it can also lock the gap after the last record in the index
select * from t2 where id >6 and id<=9 for update ;
//锁住的是(5,9],(9,14] 这里命中的最大记录是9,所以锁定区间还会包含(9,14]
select * from t2 where id >6 and id<=14 for update;
//锁住的是(5,9],(9,14] ,(14,正无穷) 这里命中的最大记录是14,所以锁定区间还会包含(14,正无穷)
总结 下临键锁: Next-Key Lock锁定范围会包含sql命中的最大记录的下一个区间(这是这里的Next-Key的含义);当在事务1使用临键锁锁定范围时,在区间范围内其他事务都不可能插入记录,但是可以使用select ... for update查询区间范围内不存在的记录且不会阻塞
总结
![](https://i-blog.csdnimg.cn/blog_migrate/612e582211ba84c7f17d5d264a961b4f.png)
Read Uncommited
RU隔离级别:不加锁。
Serializable
Serializable 所有的 select 语句都会被隐式的转化为select ... in share mode,会和update、delete互斥。
Repeatable Read
RR隔离级别下,普通的select使用快照读(snapshotread),底层使用MVCC来实现。
加锁的 select(select ... in share mode / select ... for update)以及更新操作update, delete 等语句使用当前读(current read),底层使用记录锁、或者间隙锁、临键锁。
Read Commited
RC隔离级别下,普通的select都是快照读,使用MVCC实现。加锁的select都使用记录锁,因为没有Gap Lock。
除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间
事务隔离级别的选择
RU 和 Serializable 肯定不能用。我们来看看RR和RC的区别
RC和RR主要有几个区别:
1、 RR的间隙锁会导致锁定范围扩大。
2、 条件列如果未使用到索引,RR会锁表,RC只会锁行(RC中没有Gap锁,只有Record锁)。
3、在一致性读方面的区别:
RC隔离级别时,事务中的每一条select语句会读取到该SQL执行时已经提交了的记录,也就是每一条select都有自己的一致性读ReadView ;
而RR隔离级别时,事务中的一致性读的ReadView是以第一条select语句的运行时间,作为本事务的一致性读snapshot的建立时间点的。只能读取该时间点之前已经提交的数据。
4. RC 隔离级别,通过 where 条件过滤之后,不符合条件的记录上的行锁,会释放掉;但是RR隔离级别,即使不符合where条件的记录,也不会释放行锁和gap lock
5、 RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。
A type of read operation used for UPDATE statements, that is a combination of read committed and consistent read. When an UPDATE statement examines a row that is already locked, InnoDB returns the latest committed version to MySQL so that MySQL can determine whether the row matches the WHERE condition of the UPDATE. If the row matches (must be updated), MySQL reads the row again, and this time InnoDB either locks it or waits for a lock on it. This type of read operation can only happen when the transaction has the read committed isolation level, or when the innodb_locks_unsafe_for_binlog option is enabled.
简单来说,semi-consistent read是read committed与consistent read两者的结合。一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本的数据是否满足 update的where条件。若满足(也就是需要更新已加锁的记录),则MySQL会重新发起一次读操作,此时会读取行数据的最新版本并加锁。
semi-consistent read只会发生在read committed隔离级别下,或者是参数innodb_locks_unsafe_for_binlog被设置为true(该参数即将被废弃)。
对比RR隔离级别,update语句会使用当前读,如果其中的一行被锁定了,那么就会被阻塞,等待其他事务释放锁;而不会读取最新的提交版本,然后来判断是否符合where条件。
死锁
MySQL中使用锁来实现事务隔离级别,那么不可避免的会出现死锁问题
#事务1 按照step的顺序执行SQL
begin;
select *from t2 where id = 1 for update; -- step1
delete from t2 where id = 5 ; -- step4
#事务2
begin;
delete from t2 where id = 5 ; -- step2
delete from t2 where id = 1 ; -- step3
发现在执行SQL的时候,系统会检测到死锁然后主动释放,必不会等待获取锁超时
因为死锁的发生需要满足一定的条件,所以在发生死锁时,InnoDB一般都能通过算法(wait-for graph)自动检测到,然后立即释放锁,而不需要多余的等待
死锁的产生条件:
- 互斥条件
- 同一时刻只能有一个事务持有这把锁
- 其他的事务需要在这个事务释放锁之后才能获取锁,而不可以强行剥夺
- 当多个事务形成等待环路的时候,即发生死锁。
查看锁日志
show status like 'innodb_row_lock_%';
SHOW命令可以查询锁的概要信息。InnoDB还提供了三张表来分析事务与锁的情况:
select * from information_schema.INNODB_TRX ; -- 当前运行的所有事务 ,还有具体的语句
select * from information_schema.INNODB_LOCKS; -- 当前出现的锁
select * from information_schema.INNODB_LOCK_WAITS; -- 锁等待的对应关系
如果一个事务长时间持有锁不释放,我们就可以可以 kill 事务对应的线程 ID(INNODB_TRX表中的trx_mysql_thread_id)来强行释放锁
死锁的避免
- 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路);
- 批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路);
- 申请足够级别的锁,如果要操作数据,就直接申请排它锁;
- 尽量使用索引访问数据,避免没有where条件的操作,防止锁表;
- 尽量使用等值查询而不是范围查询查询数据,避免间隙锁对并发的影响。