mysql死锁查询

本文从一起线上数据库死锁问题出发,详细分析了死锁产生的原因,涉及事务、并发控制、隔离级别、InnoDB锁机制等方面。通过分析死锁日志,发现由于update操作未使用索引导致的死锁。解决方法是为update条件添加联合索引,提高并发性能。此外,文章还介绍了MySQL锁相关知识,包括事务隔离级别、行锁类型和避免死锁的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

查监控发现线上时不时出现财务报表数据更新失败情况,追查日志发现,更新失败全部由于数据库死锁导致,日志报错“Deadlock found when trying to get lock”,经DBA查线上数据库日志,发现线上其他库表也存在死锁情况。 为了更系统的分析问题,将从问题定位、死锁成因分析,mysql锁知识这几个方面来展开讨论。

引起死锁的case

【定位问题】通过异常监控查看对应时间点目标系统的日志,发现不同时间的报错点都是由于死锁导致。 找到代码中的报错的位置,查看代码逻辑,发现发生死锁的代码处有使用事务(代码中没有明确指定事务隔离级别,默认RR),事务中有两处DML操作,如下:

#同一事物
#操作一:
insert into finance_report_info(order_type,sale_channel_no,sale_channel_name,exhibition_channel_no..)
values("q","a","a","cc");

#操作二:
UPDATE finance_report_info
SET is_show = "NO"
WHERE order_no = ? and warranty_no = ? and insurant_id = ? and acceptance_date > ?

 

这时候找到DBA帮忙查询死锁的日志,部分日志如下;

 

日志没有截全,只显示了发生死锁时事务1的日志。从日志中可以分析出,死锁时事务A正在等待锁的sql是“update finance_report_info SET is_show = "NO" WHERE order_no = 'RS_YGUA201901301506582205c' and warranty_no = '8024990030961928' and insurant_id = 63943 and acceptance_date > '2019-02-22 04:04:21'”, 事务A正在等待主索引的X锁。 事务B的日志和事务A一致,事务B正在等待锁的sql是“UPDATE finance_report_info SET is_show = "NO" WHERE order_no = 'RS_YGUA201901291711532907b' and warranty_no = '8024990030959368' and insurant_id = 62393 and acceptance_date > '2019-02-22 04:04:21'“”。

【死锁日志分析】根据死锁记录的结果,可以看出确实是这两个语句发生了死锁,且锁冲突发生在主键索引上。那么,为什么两个sql语句会存在锁冲突呢?

查看线上数据库的finance_report_info表结构发现,发生死锁冲突的update语句的匹配条件没有加索引,这时候如果执行更新操作的话,走的是锁表(InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁)。 这时候综合代码分析,当多个事务并发执行的时候,在执行insert的操作时不会出现问题,这时候两个事务分别给finance_report_info相关索引项加锁,之后两个事务在执行update操作时,就会互相等待对方事务对该表的锁(等待i其他事物insert操作加的锁),引起死锁。

【解决办法】找到问题之后,决定提sql给update的匹配关键字加联合索引,这样的话update操作就不会锁表了,死锁问题解决。 线上创建索引后,观察几天监控发现,监控正常,没有死锁线上出现。

【故障review】故障review后,查资料发现,在事务隔离级别选用RR(可重复读)时,数据库为了尽量避免幻读现象,如果更新操作走非唯一索引或者范围查询时,mysql加锁过程首先会先给满足条件的记录对应的普通索引项(查询时使用的索引)加X锁,并给加锁索引项间隙加gap锁,然后找到聚簇索引,将主键索引项也加上X锁。 回过头来分析,上面给的case中,如果两个事务并发执行,事务A和事务B都执行完了insert操作,事务A执行update操作时,由于查询索引是普通索引而且还是范围查询,很多的索引项间隙需要加gap锁,如果事务B执行insert的记录恰好在事务A加锁索引项间隙,又会出现互相等待锁,引起死锁。

【故障改进】在仔细研究后,决定将代码中的update操作分为两个步骤,先select查询出满足条件的记录,再根据id对记录进行更新。 这时候查询是因为只会走主键索引,索引只会给满足条件的记录对应的主键索引加X锁。

 

mysql 锁相关知识

在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,即S锁)。当数据对象被加上排它锁时,其他的事务不能对它读取和修改。加了共享锁的数据对象可以被其他事务读取,但不能修改。数据库利用这两种基本的锁类型来对数据库的事务进行并发控制。 为了避免死锁情况的发生,我们应该在特定的业务场景下写出最优的SQL和合理使用mysql锁机制。这就需要我们掌握mysql锁相关的一些知识。 因为咱们公司统一使用mysql InnoDB存储引擎,下文提到的锁相关知识都是mysql IndoDB存储引擎为准的。

事务

  • 原子性(Actomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

  • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以操持完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。

  • 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。(在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。)

  • 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发带来的问题

  • 脏读(Dirty Reads):一个事务处理过程里读取了另一个事务中未提交的的数据。一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做“脏读”。

  • 不可重复读(Non-Repeatable Reads):一个事务范围内多次使用相同查询条件查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。

  • 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

    小结:不可重复读的和幻读很容易混淆,幻读和不可重复读都是读取了另一条已经提交的事务。不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

事务隔离级别

  • 读未提交(read uncommitted)

    可以读取未提交的事务。此隔离级别,基本不会使用。

  • 读已提交(read committed)

    可以读取到已经提交的数据

  • 可重复读(repeatable read)

    可以保证同一事务可重复读。

  • 串行化(serializable)

    从MVCC并发控制退化为基于锁的并发控制,不会区分快照读与当前读。读加读锁 (S锁),写加写锁 (X锁)。读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。

事务隔离级别

脏读

不可重复读

幻读

读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

 

Innodb的行锁类型

  • 共享锁(S): 又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。与读锁兼容,写锁不兼容。

  • 排它锁(X): 又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。与读写锁均不兼容。

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。

意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

锁粒度

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

一致性非锁定读操作(快照读)

一致性非锁定读操作是指InnoDB存储引擎通过多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) 来读取当前执行时间数据库中的行数据。 如果读取的行数据正在执行delete/update操作 ,这时读取操作不会因此等待行上锁的释放,相反,InnoDB存储引擎会去读取行的一个快照数据。 读不加锁,读写不冲突。 非锁定读机制提高了数据库读取的并发性。

在RC和RR隔离级别下,InnoDB存储引擎会使用非锁定的一致性读机制。在RC事务隔离级别下,对于快照数据,非一致性锁定读总是读取被锁定行的最新一份快照数据。在RR事务隔离级别下,对于快找数据,一致性非锁定读总是读取事务开始时的行数据版本。

算法:

Recordlock锁(锁数据,不锁Gap)

Gap锁,不锁记录,仅仅记录前面的Gap

next KeyLocks锁,同时锁住记录(数据),并且锁住记录前面的Gap

所以其实 Next-KeyLocks=Gap锁+ Recordlock锁

常见死锁场景

e.g.

create table msg(
  id bigint,
   token varchar(20),
   message varchar(20),
    primary key (`id`),
    key `idx_token` (`token`)
);

 

场景一:操作不同表的相同记录,引起行锁冲突

事务A和事务B并发操作两张表,出现循环等待锁情况。

 

场景二:操作相同表记录,引起行锁冲突。

并发事务批量更新时,处理数据的顺序不一样引起死锁。

 

场景三:不同索引锁冲突

 

场景四:gap锁冲突

innodb在RR级别下,如下的情况也会产生死锁,比较隐晦。

 

避免死锁

1.以固定的顺序访问表和行。

2.在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。 可以使用lock in share mode 或者for update显示的加锁

3.使用尽可能低的隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁

4.表添加合理的索引。使加锁更精确,从而减少锁冲突的机会。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大

5.大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小

死锁分析

1. 查看mysql死锁日志

2. sql分析。对发生死锁的事务中的sql语句进行分析(需要掌握对mysql事务各个隔离级别和使用锁的知识)

3. 解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值