最近一段时间处理了较多锁的问题,包括锁等待导致业务连接堆积或超时,死锁导致业务失败等,这类问题对业务可能会造成严重的影响,没有处理经验的用户往往无从下手。下面将从整个数据库设计,开发,运维阶段介绍如何避免锁问题的发生,提供一些最佳实践供读者参考。

设计阶段

在数据库设计阶段,引擎选择和索引设计不当可能导致后期业务上线后出现较为严重的锁或者死锁问题。

1. 表引擎选择使用myisam,引发table level lock wait。

从5.5版本开始,MySQL官方就把默认引擎由myisam转为innodb,这两种引擎的主要区别:

由于myisam引擎只支持table lock,在使用myisam引擎表过程中,当数据库中出现执行时间较长的查询后就会堵塞该表上的更新动作,所以经常会碰到线程会话处于表级锁等待(Waiting for table level lock)的情况,严重的情况下会出现由于实例连接数被占满而应用无法正常连接的情况

  1. CREATE TABLE `t_myisam` (
  2. `id` int(11) DEFAULT NULL
  3. ) ENGINE=MyISAM DEFAULT CHARSET=utf8
  4. Query |111 | User sleep | select id,sleep(100) from t_myisam |
  5. Query |108 | Waiting for table level lock | update t_myisam set id=2 where id=1|
  6. 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操作,该操作同样会阻塞该表上的所有请求。

2. 表索引设计不当,导致数据库出现死锁。

索引设计是数据库设计非常重要的一环,不仅仅关系到后续业务的性能,如果设计不当还可导致业务上的死锁。下面的一则案例就出现在线上系统,数据库在并发更新的时候出现了死锁,通过排查定位于update更新使用了两个索引导致,死锁信息如下:

  1. *** (1) TRANSACTION:
  2. TRANSACTION 29285454235, ACTIVE 0.001 sec fetching rows
  3. mysql tables in use 3, locked 3
  4. LOCK WAIT 6 lock struct(s), heap size 1184, 4 row lock(s)
  5. MySQL thread id 6641616, OS thread handle 0x2b165c4b1700, query id 28190427937 10.103.180.86 test_ebs Searching rows for update
  6. UPDATE test SET is_deleted = 1 WHERE group_id = 1332577 and test_id = 4580605
  7. *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
  8. 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
  9. Record lock, heap no 179 PHYSICAL RECORD: n_fields 8; compact format; info bits 0
  10. *** (2) TRANSACTION:
  11. TRANSACTION 29285454237, ACTIVE 0.001 sec fetching rows, thread declared inside InnoDB 4980
  12. mysql tables in use 3, locked 3 5 lock struct(s), heap size 1184, 3 row lock(s)
  13. MySQL thread id 6639213, OS thread handle 0x2b1694cc2700, query id 28190427939 10.103.180.113 test_ebs Searching rows for update
  14. UPDATE test SET is_deleted = 1 WHERE group_id = 1332577 and test_id = 4212859
  15. *** (2) HOLDS THE LOCK(S):
  16. 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
  17. Record lock, heap no 179 PHYSICAL RECORD: n_fields 8; compact format;

表结构:

  1. CREATE TABLE `test` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
  3. `test_id` bigint(20) DEFAULT NULL,
  4. `group_id` bigint(20) DEFAULT NULL COMMENT Id,对应test_group.id’,
  5. `gmt_created` datetime DEFAULT NULL COMMENT ‘创建时间’,
  6. `gmt_modified` datetime DEFAULT NULL COMMENT ‘修改时间’,
  7. `is_deleted` tinyint(4) DEFAULT 0 COMMENT ‘删除。’,
  8. PRIMARY KEY (`id`),
  9. KEY `idx_testid` (`test_id`),
  10. KEY `idx_groupid` (`group_id`)
  11. ) ENGINE=InnoDB AUTO_INCREMENT=7429111 ;

SQL执行计划:

  1. mysql>explain UPDATE test SET is_deleted = 1 WHERE group_id = 1332577 and test_id = 4212859
  2. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  3. | 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,解决方法有两种:

  • 加test_id + group_id的组合索引,这样就可以避免掉index merge;
  • 将优化器的index merge优化关闭。

开发阶段

事务处理时间过长,导致并发出现锁等待。

并发事务处理在数据库中经常看到的应用场景,在这种场景下,需要避免大事务,长事务,复杂事务导致事务在数据库中的运行时间加长,事务时间变长则导致事务中锁的持有时间变长,影响整体的数据库吞吐量。下面的一则案例中,用户的业务数据库中出现大量的update等待,导致大量的业务超时报错:

问题排查

1. 通过show processlist确定出现锁等待的线程以及SQL信息:
  1. |Query|37|updating|UPDATE test_warning SET ... WHERE test_id = '46a9b'
2. 通过innodb的information_schema数据库中的锁等待以及事务试图,查出相关的锁信息:
  1. select r.trx_mysql_thread_id waiting_thread,
  2. r.trx_id waiting_trx_id,
  3. r.trx_query waiting_query,
  4. b.trx_id blocking_trx_id,
  5. b.trx_query blocking_query,
  6. b.trx_mysql_thread_id blocking_thread,
  7. b.trx_ started,
  8. b.trx_wait_started
  9. 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
  10.  
  11. waiting_thread: 318984063
  12. waiting_trx_id: 26432631
  13. waiting_query: UPDATE test_warning SET ........ WHERE test_id = '46a9b'
  14. blocking_trx_id: 26432630
  15. blocking_query: NULL
  16. blocking_thread: 235202017
  17. trx_started: 2016-03-01 13:54:39

从述的锁等待信息中发现,事务26432631被26432630阻塞了,那么我们就可以从general log中去排查一下事务26432630做了哪些操作。

3. 从提前打开数据库的general log中查找到上述被blcok 的update语句在日志中的位置,发现了update被blcok的根本原因: 日志中有两条不同的SQL同时并发更新同一条记录,所以后更新的SQL会等待前更新的SQL,如果SQL1所在的事务1直没有提交,那么事务2将会一直等待,这样就出现上述updating的状态
  1. 235202017 Query UPDATE test_warning ..... WHERE test_id = '46a9b'
  2. 318984063 Query UPDATE test_warning ..... task_order_id = '' WHERE test_id = '46a9b'
所以我们就可以在看一下这个事务的上下文:

thread id=235202017 的SQL上下文:

  1. 235202017 Query SET autocommit=0
  2. 235202017 (13:54:39) Query UPDATE test_warning SET .... WHERE test_id = '46a9b'
  3. 235202017 Query commit

thread id=318984063 的SQL上下文:

  1. 318984063 Query SET autocommit=1
  2. 318984063 (13:54:39)Query SELECT .... FROM test_waybill WHERE (test_id IN ('46a9b'))
  3. 318984063 Query SELECT......FROM test_waybill WHERE test_id = '46a9b'
  4. 318984063 Query UPDATE test_warning SET ..... WHERE test_id = '46a9b'
  5. 318984063 (13:55:31)Query UPDATE test_waybill_current t ..... WHERE t.test_id IN ('46a9b')
  6. 318984063 Query SET autocommit=0

可以看到事务1 从13:54:39开始,直到13:55:30结束,事务2 中有更新事务1中的同一条记录,所以直到事务1 提交后,事务2才得以执行完毕,有了这样的日志,将此信息发给用户很快就找到了问题,在事务1中由于还存在其他的业务逻辑,导致事务1的提交迟迟没有完成,进而导致了其他业务锁的发生。

维护阶段

DDL操作被大查询block。

当应用上线进入维护阶段,则开始会有较多的数据库变更操作,比如:添加字段,添加索引等操作,这一类操作导致的锁故障也是非常频繁的,下面将会介绍一则案例,一个DDL操作被查询block,导致数据库连接堆积:

  1. Query |6 | User sleep | select id ,sleep(50) from t
  2. Query |4 | Waiting for table metadata lock | alter table t add column gmt_create datetime
  3. Query |2 | Waiting for table metadata lock | select * from t where id=1
  4. Query |1 | Waiting for table metadata lock | select * from t where id=2
  5. 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的操作全部被阻塞,前端源源不断的请求到达数据库,这个时候数据库的连接很容易被打满,那我们来分析一下为什么有这这些锁等待:

  • alter 操作的锁等待:由于在表t上做了一个添加字段的操作,该操作会在结束前对表获取一个metadata lock,但是该表上已经有一个查询一直未结束,导致metadata lock一直获取不到,所以添加字段操作只能等待查询结束,这就解释了alter操作为什么状态为Waiting for table metadata lock。
  • 查询和更新的锁等待:由于前面进行的alter操作已经在T表上试图获取metadata lock,所以后续对表T的的查询和更新操作在获取metadata lock的时候会被alter操作所阻塞,进而导致这些线程状态为Waiting for table metadata lock。

解决办法则是将线程6 kill 掉即可,更加友好的方式为:控制session会话等待meta data lock的超时时间,执行DDL操作前,set session lock_wait_timeout = 10 //可根据需要设置,即使改session获取不到meta data lock 锁,最多也就阻塞数据库10秒钟,10秒钟之后,会话将自动超时退出,而后面的DML将能够继续得到执行,从而有效的降低了因为meta data lock而导致的数据库表的死锁的风险。

总结

锁问题是非常常见的问题,需要我们在数据库开发、设计、管理的各个阶段都需要注意,防范未然,做到心中有数。

设计开发阶段:
  1. 表设计要避免使用myisam存储引擎,改用innodb引擎;
  2. 为SQL创建合适的索引,避免多个单列索引执行出错;
  3. 避免大事务,长事务,复杂事务导致事务在数据库中的运行时间加长。
管理运维阶段:
  1. 在业务低峰期执行上述操作,比如创建删除索引;
  2. 在结构变更前,观察数据库中是否存在长时间运行的SQL,未提交的事务;
  3. 结构变更期间,监控数据库的线程状态是否存在lock wait。

【MySQL】锁问题最佳实践的更多相关文章

  1. MySQL · 答疑解惑 · MySQL 锁问题最佳实践

    http://mysql.taobao.org/monthly/2016/03/10/ 前言 最近一段时间处理了较多锁的问题,包括锁等待导致业务连接堆积或超时,死锁导致业务失败等,这类问题对业务可能会 ...

  2. MySQL锁问题最佳实践

    最近一段时间处理了较多锁的问题,包括锁等待导致业务连接堆积或超时,死锁导致业务失败等,这类问题对业务可能会造成严重的影响,没有处理经验的用户往往无从下手.下面将从整个数据库设计,开发,运维阶段介绍如何 ...

  3. MySQL安装脚本最佳实践

    MySQL安装脚本最佳实践 2018年9月19日 17:01 #!/bin/bash   export host_ip=192.168.7.206   echo '#1.取master主机ip的后三位 ...

  4. [转帖]12条用于Linux的MySQL/MariaDB安全最佳实践

    12条用于Linux的MySQL/MariaDB安全最佳实践 2018-01-04 11:05:56作者:凉凉_,soaring稿源:开源中国社区 https://ywnz.com/linuxysjk ...

  5. (转)Amazon Aurora MySQL 数据库配置最佳实践

    转自:https://zhuanlan.zhihu.com/p/165047153 Amazon Aurora MySQL 数据库配置最佳实践 AWS云计算 ​ 已认证的官方帐号 1 人赞同了该文章 ...

  6. MySQL性能优化最佳实践20条

    今天,数据库的操作越来越成为整个应用的性能瓶颈了,这点对于Web应用尤其明显.关于数据库的性能,这并不只是DBA才需要担心的事,而这更是我们程序员需要去关注的事情.当我们去设计数据库表结构,对操作数据 ...

  7. 解开发者之痛:中国移动MySQL数据库优化最佳实践(转)

    开源数据库MySQL比较容易碰到性能瓶颈,为此经常需要对MySQL数据库进行优化,而MySQL数据库优化需要运维DBA与相关开发共同参与,其中MySQL参数及服务器配置优化主要由运维DBA完成,开发则 ...

  8. MySQL主从同步最佳实践

    #!/bin/bash export master_ip=192.168.7.206 export slave_ip=192.168.7.207 export root_passwd=123456   ...

  9. 中国移动MySQL数据库优化最佳实践

    原创 2016-08-12 章颖 DBAplus社群 本文根据DBAplus社群第69期线上分享整理而成,文末还有书送哦~ 讲师介绍章颖 数据研发工程师 现任中国移动杭州研发中心数据研发工程师,擅长M ...

随机推荐

  1. Direct3D 10学习笔记(三)——文本输出

    本篇将简单整理Direct3D 10的文本输出的实现,具体内容参照< Introduction to 3D Game Programming with DirectX 10>(中文版有汤毅 ...

  2. SqlServer 数据库 引擎优化使用(六)

    一:新建模板 二:选择跟踪的语句类型: 使用模板: 把范文的行为进行记录: 开启引擎: 选择文件和数据库: 选择选项: 分析的结果:

  3. iOS 宏条件编译

    在工程的 build settings 中的 preprocessor macros 下, debug: DEBUG=1 TestApi=1 #if !defined (TestApi) config ...

  4. 参数*args和**args区别

    #*args(元组)和**args(字典)的区别 def tuple_test(*args): for i in args: print 'hello'+i s=('xuexi','mili') tu ...

  5. mysqldump-info

    其实很多东西都能在info里面找到非常详细的说明,只是,我们太忙了,只想要一个答案,而无心去看而已,所以呢,就把用得到的都看一下来记录吧. 命令模式:mysqldump [options] [db_n ...

  6. LintCode Minimum Path Sum

    Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which ...

  7. C#编程语言与面向对象——继承

    现实生活中的事物都归属于一定的类别,比如,狮子是一种(IS_A)动物,为了在计算机中模拟这种关系,面向对象的语言引入了继承(inherit)特性. 构成继承关系的两个类中,Animal称为父类(par ...

  8. Ibator的配置和使用

    1.     Ibator介绍 Ibator是iBATIS的代码发生器,其原名叫abator,后来更名为Ibator,同时代码结构也做了相应的一些修改,所以两者的配置也有所不同.Ibator可以生成一 ...

  9. 机械表小案例之transform的应用

    这个小案例主要是对transform的应用. 时钟的3个表针分别是3个png图片,通过setInterval来让图片转动.时,分,秒的转动角度分别是30,6,6度. 首先,通过new Date函数获取 ...

  10. python的or操作符连接多个字符串含义是什么?

    研究odoo代码的时候发现其中有一行语句让人费解 self.rcfile = os.path.abspath( self.config_file or opt.config or os.environ ...