首先复习一下加锁规则:
原则1:加锁的基本单位是next-key lock,是一个前开后闭区间;
原则2:查找过程中访问到的对象才会加锁;
优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁;
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁;
一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
接下来的讨论基于下表t:
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`)) ENGINE=InnoDB;insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
不等号条件里的等值查询
等值查询和遍历有什么区别?为什么当where条件是不等号,这个过程也有等值查询?
begin;select * from t where id>9 and id<12 order by id desc for update;
利用加锁规则,这个语句的加锁范围是主键索引上的(0,5]、(5,10]、(10,15)。id=15
没有加上行锁是因为用到了优化2,退化为了间隙锁。
但是查询语句里where条件不是等号,这里的等值查询又是从哪来的呢?
分析索引id的示意图:

由于语义是order by id desc,要拿到满足条件的所有行,优化器必须先找到第一个
id<12
的值;该过程需要搜索索引树找到
id=12
的值,但最终没找到,只找到(10,15)的间隙;然后向左遍历,该遍历过程不是等值查询,会扫描到
id=5
这一行,会加一个(0,5]。
也就是说,在执行过程中,通过树搜索方式定位记录时用的是等值查询的方法。
等值查询的过程
下面这个语句的加锁范围是什么呢?
begin;select id from t where c in(5,20,10) lock in share mode;
先看语句的explain结果:

该语句使用了索引c并且rows=3
,说明这三个值都是通过B+树搜索定位的。
在查找c=5
时,先锁住了(0,5],但因为c不是唯一索引,为了确认还有没有其他c=5
的记录,需要向右遍历,直到c=10
才确认没有,该过程满足优化2,所以加间隙锁(5,10)。
同样的,执行c=10
的时候,加锁的范围是(5,10]和(10,15);执行c=20
的时候,加锁的范围是(15,20]和(20,25)。
这些锁是在执行过程中一个一个加的,而不是一次性加上去的。
假设同时有另外一个语句:
select id from t where c in(5,20,10) order by c desc for update;
间隙锁不互锁,但这两条语句都会在索引c上的c=5、10、20三行记录上加记录锁。
由于两条语句要加锁相同的资源,但加锁顺序相反,当这两条语句并发执行的时候,就可能出现死锁。
关于死锁的信息,MySQL只保留了最后一个死锁的现场,但这个现场还是不完备的。接下来就分析上面例子的死锁现场。
怎么看死锁?
出现死锁后,执行show engine innodb status
命令能输出很多信息,其中有一节LATESTDETECTED DEADLOCK,就是记录的最后一次死锁信息。

该结果分为三部分:
(1) TRANSACTION:是第一个事务的信息;
(2) TRANSACTION:是第二个事务的信息;
WE ROLL BACK TRANSACTION (1):是最终处理结果,表示回滚了第一个事务。
第一个事务的信息中:
WAITING FOR THIS LOCK TO BE GRANTED:表示这个事务在等待的锁信息;
index c of table
test
.t
:说明在等的是表t的索引c上面的锁;lock mode S waiting:表示这个语句要自己加一个读锁,当前的状态是等待中;
Record lock:说明这是一个记录锁;
n_fields 2:表示这个记录是两列,也就是字段c和主键字段id;
0: len 4; hex 0000000a; asc ;;:是第一个字段c。值是十六进制a,也就是10;
1: len 4; hex 0000000a; asc ;;:是第二个字段,也就是主键id,值也是10;
这两行里面的asc表示的是,接下来要打印出值里面的“可打印字符”,但10不是可打印字符,因此就显示空格;
第一个事务信息就只显示出了等锁的状态,在等待(c=10,id=10)这一行的锁;
第二个事务的信息中:
“ HOLDS THE LOCK(S)”用来显示这个事务持有哪些锁;
index c of table
test
.t
表示锁是在表t的索引c上;hex 0000000a和hex 00000014表示这个事务持有c=10和c=20这两个记录锁;
WAITING FOR THIS LOCK TO BE GRANTED,表示在等(c=5,id=5)这个记录锁。
从上面这些信息中能知道:
lock in share mode这条语句,持有
c=5
的记录锁,在等c=10
的锁;for update这个语句,持有
c=20
和c=10
的记录锁,在等c=5
的记录锁。
因此导致死锁,由此得到结论:
由于锁是一个个加的,要避免死锁,对同一组资源要按照尽量相同的顺序访问;
在发生死锁的时刻,for update语句占用的资源更多,回滚成本更大,因此InnoDB选择了回滚成本更小的lock in share mode语句来回滚。
怎么看锁等待?
看完死锁,再看一个锁等待的例子。

由于session A并没有锁住c=10
,所以session B删除这一行是可以的,但之后再想insert这一行回去就不行了。
此时执行show engine innodb status
,锁信息是在TRANSACTIONS这一节:

index PRIMARY of table
test
.t
:表示这个语句被锁住是因为表t主键上的某个锁;lock_mode X locks gap before rec insert intention waiting:
insert intention:表示当前线程准备插入一个记录,这是一个插入意向锁。可以认为它就是这个插入动作本身;
gap before rec:表示这是一个间隙锁,而不是记录锁。这个gap是在哪个记录之前的呢?接下来的0~4这5行的内容就是这个记录的信息;
n_fields 5表示这一个记录有5列:
0: len 4; hex 0000000f; asc ;; 第一列是主键id字段,这个间隙是
id=15
之前的,因为id=10
已经不存在了,它表示的就是(5,15);1: len 6; hex 000000000513; asc ;; 第二列是长度为6字节的事务id,表示最后修改这一行的是trx id为1299的事务;
2: len 7; hex b0000001250134; asc % 4;; 第三列长度为7字节的回滚段信息。acs后面有显示内容 (% 和 4),这是因为刚好这个字节是可打印字符。后面两列是c和d的值,都是15。
由此可知,delete操作删除了id=10
的行,原来的间隙(5,10)、(10,15)变成了(5,15)。
有个结论:所谓间隙,其实是由“这个间隙右边的记录”定义的。
update的例子
再看一个update语句的案例:

session A的加锁范围是索引c上的(5,10]、(10,15]、(15,20]、(20,25]、(25,supremum]。
session B第一个语句要把c=5
改为c=1
,可以理解为两步:
插入(c=1,id=5);
删除(c=5,id=5)。
根据上面的结论,间隙是由间隙右边的记录定义,此时session A加锁范围变为:

session B的第二个语句拆成两步:
插入(c=5,id=5);
删除(c=1,id=5)。
第一步试图在间隙锁(1,10)插入数据,被堵住。