最近有同事在项目上遇到一个场景,定时任务在往MySql插入一条数据超时了,而排查其余SQL,没有锁表的动做。排查到最后,发现是分区表致使id不惟一,加上Gap锁致使的。下面简单分析一下。html
1.场景重现spa
1.1 没有分区的场景code
先建一个没有分区的表htm
1 CREATE TABLE student ( 2 `id` INT NOT NULL PRIMARY KEY, 3 `name` VARCHAR (128) DEFAULT NULL, 4 `country` VARCHAR(64) 5 ) ENGINE = INNODB DEFAULT charset = utf8;
插入一些数据blog
1 INSERT INTO STUDENT (`id`, `name`, `country`) 2 VALUES 3 (1, 'name1', 'CHINA'), 4 (3, 'name3', 'JAPAN'), 5 (5, 'name5', 'USA'), 6 (7, 'name7', 'JAPAN'), 7 (9, 'name9', 'CHINA');
此时开启一个事务(称为事务1),把id=5的数据改成changed索引
事务1未提交,同时在另一个窗口,再开一个事务(称为事务2),插入id为4的数据;事务
1 insert into student(`id`,`name`,`country`) values (4,'name4','USA');
成功插入,两个事务分别提交,能够看到数据已经写入。get
这很好理解,两条语句操做的id不同,不会互相影响。但是加了分区,就不同了。it
1.2 分区的场景io
1 CREATE TABLE student ( 2 `id` INT NOT NULL, 3 `name` VARCHAR (128) DEFAULT NULL, 4 `country` VARCHAR(64) NOT NULL, 5 PRIMARY KEY(id,country) 6 ) ENGINE = INNODB DEFAULT charset = utf8 partition BY KEY (country) PARTITIONS 4;
注意,主键已经不仅是id了,而是id+country。若是不把country加入到主键中,会报错。
[Err] 1503 - A PRIMARY KEY must include all columns in the table's partitioning function
而这致使了经过id并不能惟一确认一条数据(虽然从程序的逻辑上是惟一的),进而致使更新时会锁住不止一行,影响了其余事务的操做。
插入数据,此时country值是同样的(后面解释)
1 INSERT INTO STUDENT (`id`, `name`, `country`) 2 VALUES 3 (1, 'name1', 'CHINA'), 4 (3, 'name3', 'CHINA'), 5 (5, 'name5', 'CHINA'), 6 (7, 'name7', 'CHINA'), 7 (9, 'name9', 'CHINA');
和上面同样,开一个事务1,update id=5的数据,暂不提交。
在事务2,前后插入id=8和id=4的数据,能够看到,第一条顺利插入,第二条被阻塞,直到事务超时。
2. 缘由简析
参考美团技术团队关于MySql锁的分析:https://tech.meituan.com/2014/08/20/innodb-lock.html
简单来讲,就是MySql的默认事务级别是RR,在这个级别是解决了幻读问题的。不可重复读和幻读的关注点不同,前者是update/delete,后者是insert.
为了防止幻读,MySql用Gap锁把id=5的数据先后都锁住了,即(3,5] 和(5,7],因此插入id=4或6的数据会被阻塞,而插入8不会。
那么为何明明有ID,还要分区呢?这是由于有时候数据量大,利用分区能够访问快一点,例如以记录的create time做为分区依据。但这样就要求分区字段做为主键的一部分,破坏了原主键的惟一性。
上面的例子,是基于country=CHINA的分析,由于经过实验,这个锁对于一样分区的才生效。若是把其余数据的country,改成USA或JAPAN,结果又不同了。
好比把7改成JAPAN, 这时(5,'CHINA') 和 (7,'JAPAN') 并不在同个分区,那么Gap锁锁住的是(3,5]和(5,9],插入id=8, country=CHINA一样阻塞(固然插入id=8, country=JAPAN不会阻塞,不在同个分区!)。
因此实际上是 分区->ID+分区字段才能组成主键->误觉得ID是主键,但实际并不惟一,致使了Gap锁->多锁了几行
还有其余的场景,参考这个例子:https://www.zhihu.com/question/51390849/answer/294352412
事务2若是是update,而且改变了索引字段的值,也就改变了索引的位置,那么更新后的数据可能落入gap区间,形成阻塞。
若是不改变索引字段的值,不会有这样的问题。