先说一个最简单的锁优化方案,MySQL的锁是依赖索引机制来实现的,如果查询没有使用索引,就会使用表锁,那么显然最简单的方案就是给这种查询创建一个索引,避免使用表锁。关键词是缺索引
早期发现我们的业务有一个神奇的性能问题,就是响应时间偶尔会突然延长,后来经过排查,确认响应时间是因为数据库查询变慢引起的。但是那些变长的查询,SQL完全没有问题,而且用EXPLAIN去分析,都很正常,也走了索引。
直到后面我们去排查业务代码的提交记录,才发现新加的功能会执行一个SQL,但是这个SQL本身不会命中任何索引,于是数据库就会使用表锁,偏偏这个SQL本身没有命中索引,又很慢,导致表锁一直得不到释放。结果其他正常的SQL反而被他拖累了。最终我们重新优化了这个使用表锁的SQL,让它走了一个索引,就解决了这个问题。
这个方案还是比较简单,还有两个稍微复杂的方案:
在一个业务中,有一个场景是先从数据库中查询数据并锁住。如果这个数据不存在,那么就需要执行一段逻辑,计算出一个数据,然后插入。如果已经有数据了,就把原始数据取出来,再利用这个数据执行一段逻辑,计算出一个结果,执行更新。
因为两端运算逻辑不同,所以不能简单地使用INSERT ON DUPLICATE的语句来取代。
以没有数据的逻辑来看,在计算之后插入新数据,伪代码如下:
BEGIN;
SELECT * FROM biz WHERE id = ? FOR UPDATE
// 中间有很多业务操作
INSERT INTO biz(id, data) VALUE(?, ?);
COMMIT;
事实上,这个地方会引起死锁。
假如现在数据库中ID最大的值是78,那么如果两个业务进来,同时执行这个逻辑,一个准备插入id=79的数据,一个准备插入id=80的数据,执行时序如下图
[40001][1213] Deadlock found when trying to get lock; try restarting transaction
造成死锁的原因是:在线程1执行SELECT FOR UPDATE的时候,因为id是79的数据不存在,所以数据库会产生一个(78,supremum]的临键锁;类似的,线程2也会产生一个(78,supremum]的临键锁。当线程1想要执行插入的时候,他想要获得79的行锁;当线程2想要执行插入的时候,它想要获得id=80的行锁,这个时候就会出现死锁,因为线程1和线程2同时还在等着对方释放掉持有的间隙锁。