背景知识:
MySQL有三种锁的级别:页级、表级、行级。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
行级锁并不是直接锁记录,而是锁索引,如果一条SQL语句用到了主键索引,mysql会锁住主键索引;如果一条语句操作了非主键索引,mysql会先锁住非主键索引,再锁定主键索引。
问题现象:
线上出现错误日志:mysql死锁问题。
排查过程:
请DBA协助查看mysql错误日志,发现确实存在死锁问题。
发生死锁的就是以上两条sql,两次请求时间仅间隔1毫秒。
涉及到的数据库表如下:
DBA又查看mysql的binlog,发现queue_id为283410的记录,确实正常执行过SQL(1),那55机器为什么还要再次执行SQL(1)呢?
又查看开放平台日志,54机器线程启动时间为:
2016-09-25 11:12:40,463] [dbMsgConsumer-3] [INFO] [taskLogger] [] - [QueueConsumeTask started]
55机器线程启动时间为:
2016-09-25 11:12:40,583] [dbMsgConsumer-2] [INFO] [taskLogger] [] - [QueueConsumeTask started]
当54机器开启事务,执行SQL(1)时,事务还未提交,55机器也执行SQL(1),此时55机器查询到的记录是更新前的,54机器提交事务,再去执行SQL(2),此时由于SQL(1)是范围查询,SQL(2)是主键查询,SQL(2)的执行时间要远远少于SQL(1),会造成54机器执行SQL(2)时,55机器还未执行完成SQL(1),造成两条机器互相抢占资源,造成死锁。54和55两台机器执行示意图如下:
解决办法:
修改SQL(1)为两步操作,首先通过条件查询出符合条件的记录,然后根据查询出的结果的主键id再进行update操作。
修改后sql,先执行查询操作:
再执行修改操作,使用获取到的主键ID
代码修改如下:
经验教训:
电商无论前台后台的程序,都不应该存在仅根据非主键的几个字段一查就要update/delete的场景。即使有,也应该改为先把要更新的记录查出来然后逐条按主键id更新。
MySQL有三种锁的级别:页级、表级、行级。
MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
MySQL这3种锁的特性可大致归纳如下:
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
行级锁并不是直接锁记录,而是锁索引,如果一条SQL语句用到了主键索引,mysql会锁住主键索引;如果一条语句操作了非主键索引,mysql会先锁住非主键索引,再锁定主键索引。
问题现象:
线上出现错误日志:mysql死锁问题。
排查过程:
请DBA协助查看mysql错误日志,发现确实存在死锁问题。
发生死锁的就是以上两条sql,两次请求时间仅间隔1毫秒。
涉及到的数据库表如下:
根据背景知识里的红色描述:如果用到了主键索引,mysql会锁定主键索引,如果用到了非主键索引,msyql会先锁定非主键索引,再锁定主键索引。因此,在SQL(1)中先锁定了next_consume_time这个非主键索引,还需要锁定主键索引,此时SQL(2)直接锁定了主键索引,而其update语句中set使用了next_consume_time,同时还需要next_consume_time这个非主键索引。因此两条SQL就出现了对索引资源的竞争,造成死锁。
DBA又查看mysql的binlog,发现queue_id为283410的记录,确实正常执行过SQL(1),那55机器为什么还要再次执行SQL(1)呢?
又查看开放平台日志,54机器线程启动时间为:
2016-09-25 11:12:40,463] [dbMsgConsumer-3] [INFO] [taskLogger] [] - [QueueConsumeTask started]
55机器线程启动时间为:
2016-09-25 11:12:40,583] [dbMsgConsumer-2] [INFO] [taskLogger] [] - [QueueConsumeTask started]
两者仅相差120毫秒。
当54机器开启事务,执行SQL(1)时,事务还未提交,55机器也执行SQL(1),此时55机器查询到的记录是更新前的,54机器提交事务,再去执行SQL(2),此时由于SQL(1)是范围查询,SQL(2)是主键查询,SQL(2)的执行时间要远远少于SQL(1),会造成54机器执行SQL(2)时,55机器还未执行完成SQL(1),造成两条机器互相抢占资源,造成死锁。54和55两台机器执行示意图如下:
解决办法:
修改SQL(1)为两步操作,首先通过条件查询出符合条件的记录,然后根据查询出的结果的主键id再进行update操作。
修改后sql,先执行查询操作:
再执行修改操作,使用获取到的主键ID
代码修改如下:
经验教训:
电商无论前台后台的程序,都不应该存在仅根据非主键的几个字段一查就要update/delete的场景。即使有,也应该改为先把要更新的记录查出来然后逐条按主键id更新。