最近一段时间处理了较多锁的问题,包括锁等待致使业务链接堆积或超时,死锁致使业务失败等,这类问题对业务可能会形成严重的影响,没有处理经验的用户每每无从下手。下面将从整个数据库设计,开发,运维阶段介绍如何避免锁问题的发生,提供一些最佳实践供读者参考。php
在数据库设计阶段,引擎选择和索引设计不当可能致使后期业务上线后出现较为严重的锁或者死锁问题。前端
从5.5版本开始,MySQL官方就把默认引擎由myisam转为innodb,这两种引擎的主要区别:mysql
因为myisam引擎只支持table lock,在使用myisam引擎表过程当中,当数据库中出现执行时间较长的查询后就会堵塞该表上的更新动做,因此常常会碰到线程会话处于表级锁等待(Waiting for table level lock)的状况,严重的状况下会出现因为实例链接数被占满而应用没法正常链接的状况sql
CREATE TABLE `t_myisam` ( `id` int(11) DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=utf8; Query |111 | User sleep | select id,sleep(100) from t_myisam | Query |108 | Waiting for table level lock | update t_myisam set id=2 where id=1| Query | 3 | Waiting for table level lock | update t_myisam set id=2 where id=1|
从上述的案例中能够看到,t_myisam表为myisam存储引擎,当该表上有执行时间较长的查询语句在执行的时候,该表上其余的更新全被堵塞住了,这个时候应用或者数据库的链接很快耗完,致使应用请求失败。这个时候快速的恢复方法为将线程id:111 kill掉便可(能够执行show processlist查看到当前数据库全部链接状态)。另外myisam存储引擎的表索引在实例异常关闭的状况下会致使索引损坏,这个时候必需要对表进行repair操做,该操做一样会阻塞该表上的全部请求。数据库
索引设计是数据库设计很是重要的一环,不只仅关系到后续业务的性能,若是设计不当还可致使业务上的死锁。下面的一则案例就出如今线上系统,数据库在并发更新的时候出现了死锁,经过排查定位于update更新使用了两个索引致使,死锁信息以下:session
*** (1) TRANSACTION: TRANSACTION 29285454235, ACTIVE 0.001 sec fetching rows mysql tables in use 3, locked 3 LOCK WAIT 6 lock struct(s), heap size 1184, 4 row lock(s) MySQL thread id 6641616, OS thread handle 0x2b165c4b1700, query id 28190427937 10.103.180.86 test_ebs Searching rows for update UPDATE test SET is_deleted = 1 WHERE group_id = 1332577 and test_id = 4580605 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 132 page no 37122 n bits 352 index `PRIMARY` of table `testdriver`.`test` trx id 29285454235 lock_mode X locks rec but not gap waiting Record lock, heap no 179 PHYSICAL RECORD: n_fields 8; compact format; info bits 0 *** (2) TRANSACTION: TRANSACTION 29285454237, ACTIVE 0.001 sec fetching rows, thread declared inside InnoDB 4980 mysql tables in use 3, locked 3 5 lock struct(s), heap size 1184, 3 row lock(s) MySQL thread id 6639213, OS thread handle 0x2b1694cc2700, query id 28190427939 10.103.180.113 test_ebs Searching rows for update UPDATE test SET is_deleted = 1 WHERE group_id = 1332577 and test_id = 4212859 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 132 page no 37122 n bits 352 index `PRIMARY` of table `testdriver`.`test` trx id 29285454237 lock_mode X locks rec but not gap Record lock, heap no 179 PHYSICAL RECORD: n_fields 8; compact format;
表结构:并发
CREATE TABLE `test` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, `test_id` bigint(20) DEFAULT NULL, `group_id` bigint(20) DEFAULT NULL COMMENT ‘Id,对应test_group.id’, `gmt_created` datetime DEFAULT NULL COMMENT ‘建立时间’, `gmt_modified` datetime DEFAULT NULL COMMENT ‘修改时间’, `is_deleted` tinyint(4) DEFAULT ‘0’ COMMENT ‘删除。’, PRIMARY KEY (`id`), KEY `idx_testid` (`test_id`), KEY `idx_groupid` (`group_id`) ) ENGINE=InnoDB AUTO_INCREMENT=7429111 ;
SQL执行计划:运维
mysql>explain UPDATE test SET is_deleted = 1 WHERE group_id = 1332577 and test_id = 4212859 | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | | 1 | SIMPLE | test | index_merge | idx_testid,idx_groupid | idx_testid,idx_groupid | 9,9 | | 1 | Using intersect(idx_testid,idx_groupid); Using where; Using temporary |
因此第一个事务先根据group_id索引,已经锁住primary id,而后再根据test_id索引,锁定primary id;第二个事务先根据test_id索引,已经锁住primary id,而后再根据group_id索引,去锁primary id;因此这样并发更新就可能出现死索引。数据库设计
MySQL官方也已经确认了此bug:77209,解决方法有两种:ide
事务处理时间过长,致使并发出现锁等待。
并发事务处理在数据库中常常看到的应用场景,在这种场景下,须要避免大事务,长事务,复琐事务致使事务在数据库中的运行时间加长,事务时间变长则致使事务中锁的持有时间变长,影响总体的数据库吞吐量。下面的一则案例中,用户的业务数据库中出现大量的update等待,致使大量的业务超时报错:
问题排查
|Query|37|updating|UPDATE test_warning SET ... WHERE test_id = '46a9b'
select r.trx_mysql_thread_id waiting_thread, r.trx_id waiting_trx_id, r.trx_query waiting_query, b.trx_id blocking_trx_id, b.trx_query blocking_query, b.trx_mysql_thread_id blocking_thread, b.trx_ started, b.trx_wait_started from information_schema.innodb_lock_waits w inner join information_schema.innodb_trx b on b.trx_id= w.blocking_trx_id inner join information_schema.innodb_trx r on r.trx_id= w.requesting_trx_id \G waiting_thread: 318984063 waiting_trx_id: 26432631 waiting_query: UPDATE test_warning SET ........ WHERE test_id = '46a9b' blocking_trx_id: 26432630 blocking_query: NULL blocking_thread: 235202017 trx_started: 2016-03-01 13:54:39
从述的锁等待信息中发现,事务26432631被26432630阻塞了,那么咱们就能够从general log中去排查一下事务26432630作了哪些操做。
235202017 Query UPDATE test_warning ..... WHERE test_id = '46a9b' 318984063 Query UPDATE test_warning ..... task_order_id = '' WHERE test_id = '46a9b'
thread id=235202017 的SQL上下文:
235202017 Query SET autocommit=0 235202017 (13:54:39) Query UPDATE test_warning SET .... WHERE test_id = '46a9b' 235202017 Query commit
thread id=318984063 的SQL上下文:
318984063 Query SET autocommit=1 318984063 (13:54:39)Query SELECT .... FROM test_waybill WHERE (test_id IN ('46a9b')) 318984063 Query SELECT......FROM test_waybill WHERE test_id = '46a9b' 318984063 Query UPDATE test_warning SET ..... WHERE test_id = '46a9b' 318984063 (13:55:31)Query UPDATE test_waybill_current t ..... WHERE t.test_id IN ('46a9b') 318984063 Query SET autocommit=0
能够看到事务1 从13:54:39开始,直到13:55:30结束,事务2 中有更新事务1中的同一条记录,因此直到事务1 提交后,事务2才得以执行完毕,有了这样的日志,将此信息发给用户很快就找到了问题,在事务1中因为还存在其余的业务逻辑,致使事务1的提交迟迟没有完成,进而致使了其余业务锁的发生。
DDL操做被大查询block。
当应用上线进入维护阶段,则开始会有较多的数据库变动操做,好比:添加字段,添加索引等操做,这一类操做致使的锁故障也是很是频繁的,下面将会介绍一则案例,一个DDL操做被查询block,致使数据库链接堆积:
Query |6 | User sleep | select id ,sleep(50) from t Query |4 | Waiting for table metadata lock | alter table t add column gmt_create datetime Query |2 | Waiting for table metadata lock | select * from t where id=1 Query |1 | Waiting for table metadata lock | select * from t where id=2 Query |1 | Waiting for table metadata lock | update t set id =2 where id=1
Metadata lock wait 的含义:为了在并发环境下维护表元数据的数据一致性,在表上有活动事务(显式或隐式)的时候,不能够对元数据进行写入操做。所以 MySQL 引入了 metadata lock ,来保护表的元数据信息。所以在对表进行上述操做时,若是表上有活动事务(未提交或回滚),请求写入的会话会等待在 Metadata lock wait。
致使 Metadata lock wait 等待的常见因素包括:活动事务,当前有对表的长时间查询,显示或者隐式开启事务后未提交或回滚,好比查询完成后未提交或者回滚,表上有失败的查询事务等。
上述案例中,查询,更新和DDL操做的线程状态都为Waiting for table metadata lock,对表t的操做所有被阻塞,前端源源不断的请求到达数据库,这个时候数据库的链接很容易被打满,那咱们来分析一下为何有这这些锁等待:
解决办法则是将线程6 kill 掉便可,更加友好的方式为:控制session会话等待meta data lock的超时时间,执行DDL操做前,set session lock_wait_timeout = 10 //可根据须要设置,即便改session获取不到meta data lock 锁,最多也就阻塞数据库10秒钟,10秒钟以后,会话将自动超时退出,然后面的DML将可以继续获得执行,从而有效的下降了由于meta data lock而致使的数据库表的死锁的风险。
锁问题是很是常见的问题,须要咱们在数据库开发、设计、管理的各个阶段都须要注意,防范未然,作到心中有数。