MySQL死锁天天闹?事务阻塞业务停摆,规范锁序+缩短事务秒救场

简介: MySQL死锁频发?别再半夜被报警叫醒!本文用大白话讲透死锁本质——就是“互相卡住不放手”。聚焦两大主因:锁顺序混乱(48.7%)和事务过长,并给出8大实操方案:统一加锁顺序、拆分事务、建好索引、改用READ COMMITTED隔离级等,附真实电商场景解决方案,看完即用,稳如老狗!

做开发、搞运维的朋友,估计都被MySQL死锁坑过吧?本来好好的业务,一到高峰期就突然卡住,用户投诉铺天盖地,后台日志全是“Deadlock found when trying to get lock”的报错,查来查去发现是死锁搞的鬼。更头疼的是,有时候死锁偶发,测试环境复现不了,线上却频繁爆发,轻则业务卡顿、订单丢失,重则整个服务崩掉,损失真的扛不住。
360截图20260408165053418.jpg

我见过很多团队,遇到死锁就慌了神,要么盲目重启数据库,要么乱改SQL,结果死锁越改越频繁,甚至把正常业务都搞出问题。其实MySQL死锁不是什么“不治之症”,绝大多数频繁死锁的问题,根源就两个:一是锁顺序混乱,二是事务太长。只要把这两个核心问题解决,再配合一些实用技巧,就能让数据库稳如老狗,再也不用被死锁半夜叫起来加班。

今天就用最口语化的方式,把MySQL死锁的来龙去脉、高频坑点、实操解决方案,一次性讲透。不管你是开发新手,还是资深运维,看完这篇,都能轻松搞定死锁问题,让业务不再被事务阻塞拖后腿。全程不搞晦涩难懂的理论,全是实战干货,结合真实业务场景,看完就能上手操作,适合百度收录,也能直接用到工作里。

一、先搞懂:MySQL死锁到底是什么?别被专业术语吓住

很多人一听到“死锁”,就觉得是很高深的技术问题,其实说白了,死锁就是“互相僵持”,跟两个人在狭窄的巷子里迎面走来,谁都不肯让路,最后两个人都走不了,是一个道理。

放在MySQL里,就是两个或多个事务,互相持有对方需要的锁资源,同时又在等待对方释放锁,形成一个循环等待的死循环,谁都无法继续执行,也无法主动释放自己的锁,最后只能靠MySQL的死锁检测机制,强制终止其中一个事务,才能打破这个僵局。

举个最通俗的例子,大家一看就懂:

假设我们有两个事务,事务A和事务B,还有一张用户余额表(user_balance),里面有两个用户的余额记录,用户1和用户2。

事务A的操作:先给用户1的余额减100元(需要给用户1的记录加排他锁),然后再给用户2的余额加100元(需要给用户2的记录加排他锁)。

事务B的操作:先给用户2的余额减100元(需要给用户2的记录加排他锁),然后再给用户1的余额加100元(需要给用户1的记录加排他锁)。

高峰期的时候,两个事务同时执行:事务A先拿到了用户1的锁,准备拿用户2的锁;而事务B刚好先拿到了用户2的锁,准备拿用户1的锁。这时候,事务A等着事务B释放用户2的锁,事务B等着事务A释放用户1的锁,两个人互相等,谁也不让谁,死锁就产生了。

这里要注意一个误区:很多人以为“并发高就一定会产生死锁”,其实不是这样的。并发高只是死锁的“催化剂”,不是根本原因。哪怕并发不高,只要锁顺序乱了、事务太长,一样会出现死锁;反之,只要锁顺序规范、事务足够短,哪怕并发再高,也能有效避免死锁。

还有一个关键点:MySQL的InnoDB引擎(现在绝大多数业务都用这个引擎),有自动死锁检测机制,默认会每隔一段时间(毫秒级)检测一次是否有死锁。一旦检测到死锁,就会选择“代价最小”的那个事务,强制回滚,释放锁资源,让另一个事务继续执行。这就是为什么有时候我们会发现,某个事务突然回滚了,但没有报错(或者只报了死锁提示),其实就是MySQL在自动处理死锁。

但问题在于,MySQL的自动处理,只是“亡羊补牢”,不能从根本上解决问题。频繁的死锁检测和事务回滚,会导致业务卡顿、数据不一致,甚至引发连锁反应,比如订单重复创建、余额计算错误等。所以,我们要做的,是从根源上避免死锁,而不是依赖MySQL的自动处理。

二、必看:死锁频繁爆发的5大核心原因,90%的人都踩过坑

要解决死锁,首先得找到死锁的根源。根据我多年的实战经验,MySQL死锁频繁爆发,主要有5个核心原因,尤其是前两个,几乎是所有死锁问题的“重灾区”,大家可以对照自己的业务,一一排查。

(一)最常见:锁顺序混乱(占比48.7%,腾讯云实测数据)

这是死锁最主要的原因,没有之一。很多开发人员写SQL的时候,根本不考虑锁顺序,同一个业务场景,不同的事务,对同一批资源的加锁顺序不一样,一旦并发执行,就很容易形成循环等待,触发死锁。

除了前面说的用户余额转账的例子,再给大家举一个实际业务中最常出现的场景——订单创建+库存扣减,这也是电商、外卖等业务的核心场景,也是死锁的重灾区。

场景:用户下单,需要做两个操作:1. 创建订单记录(操作order表,加排他锁);2. 扣减商品库存(操作stock表,加排他锁)。

开发A写的代码:先创建订单(order表),再扣减库存(stock表);

开发B写的代码:先扣减库存(stock表),再创建订单(order表);

这两个代码单独运行,都没有问题,但一旦高峰期并发,就会出现死锁:

事务1(开发A的代码):先拿到order表的锁,准备拿stock表的锁;

事务2(开发B的代码):先拿到stock表的锁,准备拿order表的锁;

两个人互相等待,死锁就爆发了。更可怕的是,这种死锁不是偶发的,只要并发达到一定程度,就会频繁出现,而且很难排查,因为代码单独运行都正常,只有并发的时候才会出问题。

还有一种更隐蔽的情况:单表多记录加锁顺序混乱。比如,批量更新用户信息,有的事务按用户ID升序加锁(先更ID=1,再更ID=2),有的事务按用户ID降序加锁(先更ID=2,再更ID=1),这样也会形成循环等待,触发死锁。

这里提醒大家:锁顺序混乱,看似是“代码写法问题”,本质上是“开发规范问题”。很多团队没有统一的加锁规范,每个开发都按自己的习惯写SQL,最后必然会出现死锁。

(二)最致命:事务太长,锁持有时间太久

如果说锁顺序混乱是死锁的“导火索”,那长事务就是死锁的“温床”。事务越长,锁持有时间就越久,并发情况下,其他事务等待锁的时间就越长,发生死锁的概率就会呈指数级上升。

很多开发人员都有一个坏习惯:把很多无关的操作,都放在同一个事务里,比如“查询数据+业务逻辑计算+调用第三方接口+更新数据库+日志记录”,全塞在一个事务里。这样一来,事务的执行时间会变得很长,锁会被一直持有,直到整个事务结束才释放。

举个真实的反面案例:某电商平台的订单支付流程,开发人员把“查询订单信息→调用支付接口→更新订单状态→扣减库存→记录支付日志→通知物流”,所有操作都放在同一个事务里。

大家可以想想,这个事务的执行时间会有多长?调用支付接口可能需要几百毫秒,通知物流可能需要几十毫秒,再加上业务逻辑计算,整个事务可能需要1-2秒,甚至更久。在这1-2秒里,订单表、库存表的锁会被一直持有,其他并发的支付、下单事务,只能排队等待,一旦有多个这样的长事务,就很容易形成死锁。

更坑的是,很多长事务还会出现“隐性长事务”——开发人员忘记提交事务,或者代码报错后没有及时回滚事务,导致事务一直处于“未提交”状态,锁一直被持有,直到数据库连接断开,或者被DBA手动kill掉。这种隐性长事务,比显性长事务更可怕,因为它隐蔽性强,很难被发现,一旦出现,会导致大量锁阻塞,甚至引发大面积死锁。

像618同城网www.tiancebbs.cn这样的高并发平台,在处理大量用户请求、数据交互时,也曾经遇到过长事务导致的死锁问题,后来通过缩短事务、规范锁顺序,成功将死锁发生率降到了极低水平,保障了平台的稳定运行。

(三)最容易忽略:索引缺失,导致锁升级

很多人不知道,MySQL的锁粒度,和索引有直接关系。InnoDB引擎默认是行级锁,但如果查询语句没有命中索引,就会从行级锁升级为表级锁——也就是说,本来只想锁一行数据,结果把整个表都锁了,这样一来,并发情况下,所有操作该表的事务,都会被阻塞,很容易引发死锁。

举个例子:有一张商品表(product),主键是id,还有一个字段是category_id(商品分类ID),但category_id没有建索引。现在有一个事务,执行“UPDATE product SET stock = stock - 1 WHERE category_id = 5”,因为category_id没有索引,MySQL会进行全表扫描,为了保证数据一致性,会给全表的每一行都加行锁,最后升级为表级锁。

这时候,其他事务再操作这张商品表,不管是更新、删除,还是查询(加锁查询),都会被阻塞。如果有多个这样的事务,就会互相等待表级锁,触发死锁。

还有一种情况:范围查询没有命中索引,会触发间隙锁(Gap Lock),扩大锁的范围,也会增加死锁的概率。比如“SELECT * FROM product WHERE price > 100 FOR UPDATE”,如果price没有建索引,MySQL会扫描所有price>100的记录,同时给这些记录之间的“间隙”也加锁,导致其他事务无法插入、更新这个间隙内的记录,进而引发锁冲突和死锁。

这里提醒大家:索引不仅能提升查询效率,还能控制锁的粒度,避免锁升级。所以,写SQL的时候,一定要确保查询语句能命中索引,避免全表扫描。

(四)最隐蔽:锁类型冲突,共享锁与排他锁混用

MySQL的锁,主要分为共享锁(S锁,读锁)和排他锁(X锁,写锁),这两种锁的兼容性很差:

  1. 多个事务可以同时持有同一个资源的共享锁(比如多个人同时查询同一条数据);

  2. 一个事务持有排他锁后,其他事务既不能持有排他锁,也不能持有共享锁(比如一个人在修改数据,其他人不能修改,也不能查询加锁的数据);

  3. 一个事务持有共享锁后,其他事务可以持有共享锁,但不能持有排他锁(比如多个人在查询数据,其他人不能修改这条数据)。

很多开发人员在写SQL的时候,会不小心混用共享锁和排他锁,导致锁冲突,进而引发死锁。比如:

事务A:执行“SELECT * FROM order WHERE id = 1 FOR SHARE”(加共享锁,用于查询);

事务B:执行“SELECT * FROM order WHERE id = 2 FOR SHARE”(加共享锁,用于查询);

然后,事务A想更新id=2的订单(需要加排他锁),但事务B持有id=2的共享锁,所以事务A需要等待;

同时,事务B想更新id=1的订单(需要加排他锁),但事务A持有id=1的共享锁,所以事务B也需要等待;

这样一来,两个事务互相等待,死锁就产生了。

这种情况,在一些需要“先查询、再更新”的场景中,非常常见。很多开发人员为了防止“幻读”,会给查询语句加共享锁,但没有考虑到后续的更新操作会需要排他锁,进而引发锁冲突。

(五)最容易踩坑:触发器、外键引发的隐性锁

除了上面4种情况,还有一种非常隐蔽的死锁原因——触发器和外键。很多开发人员在使用触发器、外键的时候,根本没有意识到,它们会在后台自动加锁,而且加锁顺序是不可控的,很容易和其他事务的锁顺序冲突,引发死锁。

比如,有一张订单表(order),和一张订单日志表(order_log),给订单表加了一个触发器:当订单状态更新时,自动向订单日志表插入一条日志记录。

事务A:更新订单表的状态(加排他锁),触发器自动插入订单日志表(加排他锁);

事务B:先插入一条订单日志(加排他锁),然后更新对应的订单状态(加排他锁);

这时候,事务A的加锁顺序是“order表→order_log表”,事务B的加锁顺序是“order_log表→order表”,和前面说的锁顺序混乱一样,很容易引发死锁。

外键也是一样的道理:当向子表插入数据时,MySQL会自动检查父表的外键约束,给父表的对应记录加共享锁。如果有其他事务正在更新父表的这条记录(加排他锁),就会导致锁冲突;如果多个事务同时操作子表和父表,加锁顺序不一致,就会引发死锁。

所以,除非必要,尽量不要使用触发器和外键,尤其是在高并发场景下。如果必须使用,一定要仔细分析它们的加锁逻辑,确保和其他事务的加锁顺序一致。

三、实操:解决死锁的8大方案,规范锁序+缩短事务是核心

找到了死锁的核心原因,接下来就是最关键的部分——如何解决死锁,而且是从根源上解决,不是临时补救。结合前面的原因分析,我整理了8个实操方案,其中“规范锁顺序”和“缩短事务”是核心,只要做好这两个,就能解决90%的死锁问题,剩下的方案,作为补充和优化,进一步降低死锁概率。

(一)核心方案1:规范锁顺序,从根源杜绝循环等待

规范锁顺序,本质上就是“约定所有事务,对同一批资源的加锁顺序,必须完全一致”,这样就不会出现“你等我、我等你”的循环等待,从根源上避免死锁。这是解决死锁最有效、最根本的方案,没有之一。

具体怎么操作?给大家3个可落地的规范,直接照搬就能用:

  1. 多表操作,统一加锁顺序

只要涉及到多表更新、删除、加锁查询,所有事务的加锁顺序,必须完全一致。比如,涉及order表和stock表的操作,所有事务都必须先操作order表,再操作stock表;涉及user表和order表的操作,所有事务都必须先操作user表,再操作order表。

怎么统一?建议团队内部制定一份“加锁顺序规范文档”,把所有常用的表,按字典序(比如按表名首字母排序)或者业务优先级,确定加锁顺序,所有开发人员必须严格遵守。比如:user表 → order表 → stock表 → order_log表,所有事务涉及这些表的操作,都必须按这个顺序加锁。

举个例子,之前的订单创建+库存扣减场景,统一规定:先操作order表(创建订单),再操作stock表(扣减库存),不管哪个开发写代码,都必须按这个顺序,这样就不会出现锁顺序混乱的问题,死锁自然就不会发生。

这里提醒大家:不要依赖SQL的书写顺序,因为MySQL的执行顺序,可能和SQL的书写顺序不一样。最好的方式是,在代码层面,明确先执行哪个表的操作,再执行哪个表的操作,确保加锁顺序一致。

  1. 单表多记录操作,按固定顺序加锁

如果是单表多记录的更新、删除、加锁查询,比如批量更新多个用户的余额、批量删除多个订单,所有事务都必须按同一字段的固定顺序(比如主键ID升序、降序)加锁,不能有的按升序,有的按降序。

最推荐的方式是按主键ID升序加锁,因为主键是唯一的,而且InnoDB的主键索引是聚簇索引,按主键升序加锁,效率最高,也最容易统一。

比如,批量更新用户余额,事务A和事务B,都必须按用户ID升序,先更新ID=1,再更新ID=2,再更新ID=3,这样就不会出现循环等待。如果事务A按升序,事务B按降序,就很容易出现死锁。

具体操作:写SQL的时候,加上ORDER BY主键ID ASC,确保加锁顺序一致。比如:

UPDATE user_balance SET balance = balance - 100 WHERE id IN (1,2,3) ORDER BY id ASC;

这样,MySQL会按id升序,依次给id=1、id=2、id=3的记录加锁,所有事务都按这个顺序,就不会出现锁顺序混乱。

  1. 避免隐性锁顺序混乱(触发器、外键、INSERT ... SELECT)

前面提到,触发器、外键、INSERT ... SELECT这类操作,会隐性加锁,而且加锁顺序不可控,很容易破坏我们约定的锁顺序。所以,我们要尽量避免使用这些操作;如果必须使用,一定要仔细分析其加锁逻辑,确保和我们约定的锁顺序一致。

比如,触发器会自动操作其他表,我们要确保触发器的操作顺序,和我们约定的加锁顺序一致。如果触发器的操作顺序和约定的不一致,就需要修改触发器逻辑,或者调整约定的加锁顺序。

再比如,INSERT ... SELECT语句,会同时给查询的表加共享锁,给插入的表加排他锁,加锁顺序是“查询表→插入表”,我们要确保这个顺序,和我们约定的加锁顺序一致,否则就会引发死锁。

(二)核心方案2:缩短事务,减少锁持有时间

如果说规范锁顺序是“从根源上避免死锁”,那么缩短事务,就是“减少死锁发生的概率”。事务越短,锁持有时间就越短,其他事务等待锁的时间就越短,发生死锁的概率就越低。

很多人觉得“缩短事务”很简单,其实不然,很多开发人员都不知道,哪些操作可以放在事务里,哪些操作不能放在事务里。这里给大家4个可落地的技巧,轻松缩短事务,而且不影响业务逻辑。

  1. 事务里只放“核心数据库操作”,无关操作全部剥离

这是缩短事务的核心原则:事务的唯一作用,就是保证数据库操作的原子性(要么全成,要么全败),所以,只有数据库操作(增删改查),才能放在事务里;其他无关操作,比如业务逻辑计算、调用第三方接口、日志记录、文件写入等,全部剥离到事务外面。

还是以订单支付流程为例,正确的做法是:

① 先在事务外,查询订单信息、调用支付接口(第三方接口)、进行业务逻辑计算;

② 只有当支付接口调用成功后,再开启事务,执行核心数据库操作:更新订单状态、扣减库存;

③ 事务提交后,再在事务外,记录支付日志、通知物流。

这样一来,事务里就只有两个核心数据库操作,执行时间可能只有几毫秒,锁持有时间极短,发生死锁的概率就会大大降低。

反之,如果把所有操作都放在事务里,事务执行时间会很长,锁持有时间也很长,很容易引发死锁。

  1. 避免“隐性长事务”,确保事务及时提交/回滚

隐性长事务,比显性长事务更可怕,因为它隐蔽性强,很难被发现。所以,我们要做好两点,避免隐性长事务:

① 代码层面,确保每个事务都有明确的提交(COMMIT)和回滚(ROLLBACK)逻辑,尤其是在try-catch块里,一定要在catch块中回滚事务,避免代码报错后,事务一直处于未提交状态。

比如,用Java开发,正确的事务写法是:

try {

开启事务;

执行数据库操作;

提交事务;

} catch (Exception e) {

回滚事务;

记录日志;

}

很多开发人员,会忘记在catch块里回滚事务,导致代码报错后,事务一直未提交,锁一直被持有,引发锁阻塞和死锁。

② 数据库层面,开启长事务监控,及时发现并终止未提交的长事务。

可以通过查询MySQL的information_schema.INNODB_TRX表,查看当前所有未提交的事务,重点关注trx_started(事务开始时间)超过阈值(比如60秒)的事务,手动kill掉,释放锁资源。

查询语句:SELECT * FROM information_schema.INNODB_TRX WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;

kill事务语句:KILL 事务ID(trx_id);

也可以通过修改MySQL配置,设置事务超时时间,让MySQL自动终止超时的事务:

innodb_lock_wait_timeout = 60; (单位:秒,默认是50秒,可根据业务调整)

这里提醒大家:事务超时时间不要设置太短,否则会导致正常的长事务被终止;也不要设置太长,否则无法及时释放锁资源。建议根据自己的业务,设置合理的阈值,比如60-120秒。

  1. 拆分大事务,将“一揽子操作”拆分为多个小事务

如果业务逻辑确实需要执行多个数据库操作,而且这些操作无法剥离到事务外,就可以将大事务拆分为多个小事务,每个小事务只执行一个核心操作,执行完立即提交,减少锁持有时间。

举个例子:批量导入1000条用户数据,很多开发人员会把1000条数据的插入操作,放在一个事务里,这样事务执行时间会很长,锁持有时间也很长。正确的做法是,拆分为100个小事务,每个事务插入10条数据,插入完立即提交,这样每个事务的执行时间很短,锁持有时间也很短,大大降低死锁概率。

再比如,批量更新1000条订单状态,拆分为100个小事务,每个事务更新10条订单,执行完立即提交,避免一个大事务持有锁太久。

这里要注意:拆分事务后,要考虑数据一致性的问题。如果某个小事务执行失败,需要有对应的补偿机制(比如回滚该小事务的操作,或者记录失败日志,后续手动处理),确保数据一致性。

  1. 开启自动提交,避免隐式事务过长

MySQL默认是开启自动提交(autocommit=1)的,也就是说,每一条SQL语句,都是一个独立的事务,执行完自动提交,锁会立即释放。但很多开发人员,为了方便,会手动关闭自动提交(autocommit=0),然后手动开启事务,执行多条SQL语句后,再手动提交。

这种做法,很容易导致事务过长,因为一旦关闭自动提交,所有SQL语句都会处于同一个事务里,直到手动提交或回滚。如果开发人员忘记手动提交,就会导致事务一直未提交,锁一直被持有。

所以,建议大家:除非必要,尽量不要关闭自动提交;如果需要手动事务,一定要确保事务执行完后,及时手动提交或回滚,不要拖延。

(三)补充方案1:合理设计索引,避免锁升级

前面提到,索引缺失会导致锁升级(行锁→表锁),扩大锁的范围,引发死锁。所以,合理设计索引,确保查询语句能命中索引,是避免死锁的重要补充。

具体怎么做?给大家3个实操技巧:

  1. 给所有查询条件、更新条件、删除条件的字段,建立索引

只要SQL语句中,WHERE子句里的字段,用于查询、更新、删除,就必须给这个字段建立索引,确保MySQL能通过索引定位到具体的行,避免全表扫描,从而避免锁升级。

比如,前面提到的“UPDATE product SET stock = stock - 1 WHERE category_id = 5”,如果category_id没有索引,就会全表扫描,触发锁升级;给category_id建立索引后,MySQL会通过索引定位到category_id=5的所有行,只给这些行加行锁,不会升级为表锁。

这里提醒大家:不要盲目建索引,索引太多会影响插入、更新的效率,要根据业务场景,建立必要的索引,优先建立主键索引、唯一索引、常用查询字段的索引。

  1. 避免范围查询无索引,减少间隙锁

范围查询(比如>、<、BETWEEN、IN)如果没有命中索引,会触发间隙锁,扩大锁的范围,增加死锁的概率。所以,范围查询的字段,一定要建立索引。

比如,“SELECT * FROM product WHERE price > 100 FOR UPDATE”,如果price没有索引,会触发间隙锁,给所有price>100的记录和间隙加锁;给price建立索引后,只会给price>100的记录加行锁,不会触发间隙锁(MySQL 8.0+,READ COMMITTED隔离级别下)。

  1. 避免使用“SELECT * FOR UPDATE”,尽量用“行级锁”替代“表级锁”

很多开发人员,为了防止“幻读”,会使用“SELECT * FOR UPDATE”给查询的记录加排他锁,但如果查询语句没有命中索引,就会升级为表级锁,引发锁阻塞和死锁。

所以,尽量避免使用“SELECT * FOR UPDATE”,如果必须加锁查询,建议明确指定查询的字段,并且确保查询语句能命中索引,只给需要的记录加锁,避免锁范围过大。

比如,不要写“SELECT * FROM order WHERE user_id = 1 FOR UPDATE”,而是写“SELECT id, order_no FROM order WHERE user_id = 1 FOR UPDATE”,并且给user_id建立索引,确保只给user_id=1的订单记录加锁。

(四)补充方案2:避免共享锁与排他锁混用,减少锁冲突

共享锁(S锁)和排他锁(X锁)的兼容性很差,混用很容易引发锁冲突,进而导致死锁。所以,我们要尽量避免混用这两种锁,具体做法如下:

  1. 非必要,不使用共享锁(FOR SHARE)

很多开发人员,会用共享锁来防止“幻读”,但实际上,在大多数业务场景下,不需要防止幻读,用普通的查询(不加锁)就可以满足需求。普通查询(SELECT)在InnoDB引擎下,默认是“快照读”,不会加锁,也不会影响其他事务的写操作,既能保证查询效率,又能减少锁冲突。

只有在少数需要“读取数据后,立即更新,并且不允许其他事务修改该数据”的场景下,才需要使用共享锁。比如,查询用户余额后,立即扣减余额,这时候可以用共享锁,防止其他事务同时修改该用户的余额。

  1. 如果必须使用共享锁,尽量缩短共享锁的持有时间

如果确实需要使用共享锁,一定要确保共享锁的持有时间极短,查询完数据后,立即执行更新操作,然后提交事务,释放共享锁,避免其他事务长时间等待。

比如,查询用户余额(加共享锁)→ 扣减余额(加排他锁)→ 提交事务,整个过程要尽量快速,不要在中间加入其他无关操作。

  1. 避免“先加共享锁,再加排他锁”的场景

如果一个事务先加共享锁,再试图加排他锁,而此时有其他事务持有该资源的共享锁,就会导致该事务等待,进而引发锁冲突。所以,尽量避免这种场景,要么直接加排他锁,要么先查询(不加锁),再加排他锁。

(五)补充方案3:调整事务隔离级别,减少锁的范围

MySQL的事务隔离级别,会影响锁的行为和范围,不同的隔离级别,锁的粒度和持有时间也不同。默认的事务隔离级别是“可重复读”(REPEATABLE READ),在这个级别下,会触发间隙锁,扩大锁的范围,增加死锁的概率。

所以,对于高并发场景,我们可以适当降低事务隔离级别,减少锁的范围,降低死锁概率。具体建议如下:

  1. 优先使用“读已提交”(READ COMMITTED)隔离级别

“读已提交”隔离级别,是大多数高并发业务的首选,它的特点是:

① 普通查询(快照读)不加锁,不会影响其他事务的写操作;

② 更新、删除、加锁查询,只锁命中的行,不会触发间隙锁(MySQL 8.0+);

③ 能避免“脏读”,但无法避免“不可重复读”,但大多数业务场景,“不可重复读”是可以接受的。

比如,电商、外卖、社交等业务,用户查询订单信息、商品信息,即使两次查询的结果不一致(比如订单状态从“待支付”变成“已支付”),也不会影响业务逻辑,这种场景下,“读已提交”隔离级别完全适用。

修改事务隔离级别的方法:

① 全局修改(重启MySQL生效):在my.cnf配置文件中,添加“transaction_isolation = READ-COMMITTED”,然后重启MySQL;

② 会话级修改(当前会话生效):执行“SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED”;

③ 事务级修改(当前事务生效):执行“SET TRANSACTION ISOLATION LEVEL READ COMMITTED”,然后开启事务。

  1. 避免使用“序列化”(SERIALIZABLE)隔离级别

“序列化”隔离级别,是最高的隔离级别,它会给所有查询的记录加锁,甚至会给间隙加锁,锁的范围极大,并发性能极差,很容易引发死锁。除非是对数据一致性要求极高,且并发极低的场景(比如财务对账),否则不要使用这个隔离级别。

(六)补充方案4:使用乐观锁,替代悲观锁,减少锁冲突

前面我们讲的,都是“悲观锁”(比如排他锁、共享锁),悲观锁的核心是“先加锁,再操作”,虽然能保证数据一致性,但会导致锁冲突,增加死锁的概率。对于并发量高、写操作少、读操作多的场景,我们可以使用“乐观锁”,替代悲观锁,减少锁冲突。

乐观锁的核心是“先操作,再校验”,不需要加锁,而是通过版本号(version)或时间戳(timestamp),来判断数据是否被其他事务修改过,如果没有被修改,就提交事务;如果被修改,就回滚事务,重新尝试。

举个例子,乐观锁的实现步骤:

  1. 在表中添加一个版本号字段(version),默认值为1;

  2. 事务A查询数据时,获取数据的版本号(比如version=1);

  3. 事务A修改数据时,同时校验版本号,只有当表中的版本号等于查询到的版本号时,才执行更新操作,并将版本号加1;

  4. 如果此时有事务B修改了这条数据,表中的版本号会变成2,事务A的校验就会失败,回滚事务,重新查询数据,再次尝试更新。

具体SQL示例:

-- 查询数据,获取版本号

SELECT id, balance, version FROM user_balance WHERE id = 1;

-- 更新数据,校验版本号

UPDATE user_balance SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1;

-- 判断更新是否成功,如果影响行数为0,说明数据被其他事务修改,回滚重试

乐观锁的优势:不需要加锁,避免了锁冲突,不会引发死锁,并发性能更高;劣势:需要额外添加版本号字段,而且在写操作频繁的场景下,会出现大量重试,影响业务效率。

所以,乐观锁适合“读多写少”的场景,比如商品详情查询、用户信息查询等;悲观锁适合“写多读少”的场景,比如订单支付、库存扣减等。

(七)补充方案5:开启死锁检测,及时排查死锁问题

虽然我们做了很多预防措施,但难免还是会出现死锁。所以,开启死锁检测,及时排查死锁问题,避免死锁频繁爆发,也是非常重要的。

具体怎么做?给大家3个实操方法:

  1. 开启死锁日志持久化,方便后续分析

MySQL默认会将死锁信息记录到内存中,但重启MySQL后,死锁日志会丢失。所以,我们需要开启死锁日志持久化,将死锁信息记录到错误日志中,方便后续排查。

修改MySQL配置文件(my.cnf),添加以下配置:

innodb_print_all_deadlocks = 1 # 开启所有死锁日志打印

log_error = /var/log/mysql/error.log # 错误日志路径(可自定义)

配置完成后,重启MySQL,后续所有死锁信息,都会被记录到error.log中,我们可以通过查看日志,定位死锁的事务、SQL语句、锁资源等。

  1. 实时查询死锁信息,快速定位问题

死锁发生后,我们可以通过以下SQL语句,实时查询死锁信息,快速定位问题:

① 查看最新死锁详情(最核心):

SHOW ENGINE INNODB STATUS;

执行这条语句后,会输出很多信息,重点关注“LATEST DETECTED DEADLOCK”模块,里面包含了死锁的所有信息:死锁的事务ID、持有的锁、等待的锁、触发死锁的SQL语句等。

② 查看当前所有事务:

SELECT * FROM information_schema.INNODB_TRX;

重点关注trx_id(事务ID)、trx_started(事务开始时间)、trx_sql_state(事务状态)、trx_query(事务执行的SQL语句)。

③ 查看当前锁持有和等待关系(MySQL 8.0+):

SELECT * FROM performance_schema.data_locks;

SELECT * FROM performance_schema.data_lock_waits;

通过这两条语句,可以查看当前所有持有锁的事务、等待锁的事务,以及锁的资源信息,快速定位锁冲突的原因。

  1. 定期分析死锁日志,优化业务逻辑

定期查看死锁日志,统计死锁的高频场景、高频SQL语句,分析死锁的原因,然后优化业务逻辑、SQL语句、加锁顺序等,从根源上解决死锁问题。

比如,通过死锁日志发现,某条批量更新SQL经常引发死锁,就可以优化这条SQL,拆分事务、添加索引、规范加锁顺序;如果发现某类业务场景经常引发死锁,就可以优化该业务的逻辑,避免锁顺序混乱、长事务等问题。

(八)补充方案6:分库分表,分散并发压力

如果业务并发量非常高,单库单表的压力很大,即使做好了前面所有的优化,死锁还是频繁爆发,这时候就可以考虑分库分表,将数据分散到多个数据库、多个表中,分散并发压力,减少锁冲突和死锁的概率。

分库分表的核心思路:将一张大表,拆分为多个小表,将一个大数据库,拆分为多个小数据库,每个小表、小数据库,只处理一部分数据,这样一来,并发压力就会分散,每个小表的锁冲突会大大减少,死锁的概率也会降低。

比如,电商平台的订单表,数据量非常大,并发量也很高,可以按用户ID哈希分表,将不同用户的订单,分散到不同的订单表中,这样,不同用户的订单操作,不会互相影响,锁冲突会大大减少,死锁的概率也会降低。

这里提醒大家:分库分表是一种“终极解决方案”,实施成本较高,需要修改业务代码、调整数据库架构,所以,只有在并发量极高、前面的优化方案无法解决死锁问题时,才考虑分库分表。

四、实战:不同业务场景的死锁解决方案,直接照搬就能用

前面讲了通用的解决方案,但不同的业务场景,死锁的原因和解决方案也有所不同。下面结合几个常见的业务场景,给大家提供针对性的死锁解决方案,直接照搬就能用到工作中。

(一)电商场景:订单创建+库存扣减(高频死锁场景)

核心痛点:并发下单时,多事务同时操作订单表和库存表,锁顺序混乱、长事务,导致死锁频繁爆发,订单丢失、库存错乱。

解决方案:

  1. 规范锁顺序:所有事务,统一先操作订单表(创建订单),再操作库存表(扣减库存),不允许反过来操作;

  2. 缩短事务:事务里只放“创建订单”和“扣减库存”两个核心数据库操作,调用支付接口、记录日志等操作,全部剥离到事务外;

  3. 给订单表的user_id、order_no建立索引,给库存表的product_id建立索引,避免锁升级;

  4. 拆分大事务:批量下单时,拆分为多个小事务,每个事务处理1-10个订单,执行完立即提交;

  5. 开启乐观锁:库存扣减时,使用乐观锁(版本号),替代悲观锁,减少锁冲突;

  6. 调整事务隔离级别:将事务隔离级别改为“读已提交”,减少间隙锁,降低死锁概率。

相关文章
|
13天前
|
运维 Kubernetes 应用服务中间件
CI/CD流水线镜像拉取耗时从47分钟降到2分钟,我做了这几件事
换镜像加速源,CI/CD构建从47分钟骤降至2分钟!非代码/硬件优化,仅切换为毫秒镜像(1ms.run)——全源加速(Docker Hub、GHCR、k8s.gcr等),30台服务器10分钟批量配置,失败率归零,凌晨发布成功率100%。
109 16
|
4天前
|
人工智能 API 网络安全
阿里云百炼Coding Plan 套餐价格与额度说明及使用指南及OpenClaw部署教程
阿里云Coding Plan以固定月费+额度模式大幅降低开发者使用大模型的成本,Lite版适合个人日常开发,Pro版适合高频与团队场景。搭配阿里云轻量服务器+OpenClaw,可快速搭建稳定、合规、低成本的AI编码助手,全程部署简单、模型丰富、稳定性高,是2026年个人与小型团队实现AI编码效率提升的优选方案。
454 2
|
12天前
|
人工智能 供应链 API
阿里云Coding Plan是什么?总是售罄怎么办?附解决攻略
阿里云Coding Plan因高性价比与顶级模型支持成开发者焦点,但Pro版每日9:30限量补货、秒罄难求。本文解析其价值,提供抢购技巧,并推荐百炼平台替代方案:免抢购、赠100万Tokens、按量付费,灵活适配各类需求。
294 5
|
22天前
|
存储 人工智能 前端开发
使用Spring AI Alibaba构建智能体Agent
本文详解使用Spring AI Alibaba框架,结合DashScope和React Agent,通过两个实战案例(基础测试+生产级应用),教你快速构建Java AI智能体
845 7
|
Java Linux iOS开发
又一款 IDEA 全家桶 神器 ja-netfilter-all 插件
又一款 IDEA 全家桶 神器 ja-netfilter-all 插件
9418 0
又一款 IDEA 全家桶 神器 ja-netfilter-all 插件
|
1月前
|
Arthas 人工智能 Java
我们做了比你更懂 Java 的 AI-Agent -- Arthas Agent
Arthas Agent 是基于阿里开源Java诊断工具Arthas的AI智能助手,支持自然语言提问,自动匹配排障技能、生成安全可控命令、循证推进并输出结构化报告,大幅降低线上问题定位门槛。
1078 64
我们做了比你更懂 Java 的 AI-Agent -- Arthas Agent
|
1月前
|
人工智能 安全 前端开发
阿里开源 Team 版 OpenClaw,5分钟完成本地安装
HiClaw 是 OpenClaw 的升级版,通过引入 Manager Agent 架构和分布式设计,解决了 OpenClaw 在安全性、多任务协作、移动端体验、记忆管理等方面的核心痛点。
1945 60
阿里开源 Team 版 OpenClaw,5分钟完成本地安装
|
20天前
|
人工智能 算法 知识图谱
算法对齐还是实战突围?解构GEO优化中方法论与实践的权重博弈
在AIGC重塑信息检索的当下,GEO(生成式引擎优化)已成为品牌流量增长新基座。专家于磊提出“人性化GEO”理念,首创“两大核心+四轮驱动”方法论,融合语义对齐、EEAT原则与结构化知识图谱,兼顾算法可信度与实践适应性,推动AI搜索从技术博弈回归用户价值。(239字)
109 18
|
22天前
|
存储 JSON vr&ar
什么是 glb/glTF 格式,为什么它们对 3D 设计师如此重要?
glTF(GL传输格式)是Khronos Group推出的开放、高效、跨平台的3D模型标准,支持网格、材质、动画等全要素,JSON+二进制(glb)双格式。轻量、易传输,广泛用于Web、VR、游戏与3D打印,兼容Blender、Unity等主流工具。(239字)
|
14天前
|
人工智能 Python
别再乱问AI了!掌握这3个“指令性提示词”,效率提升300%
别再乱问AI了!掌握这3个“指令性提示词”,效率提升300%
191 5