云服务器数据库锁死如何处理?
- 来源:纵横数据
- 作者:中横科技
- 时间:2026/6/8 11:25:11
- 类别:新闻资讯
我印象最深的一次技术事故,不是服务器崩溃,也不是数据丢失,而是数据库“锁死”。那是一个工作日的下午,业务高峰时段,公司的订单系统突然大面积超时。用户下单点不动,后台订单刷不出来,仓库那边也没法发货。技术群里炸了锅,各种“数据库连接超时”“死锁检测”的报错刷屏。
我登录数据库一看,show processlist的结果让我倒吸一口凉气。几十个连接挤在那里,状态清一色都是“Waiting for table metadata lock”。整个数据库就像被施了定身法,谁也别想动弹。那次事故的处理过程相当狼狈,但事后也让我对数据库锁死这个问题有了极其深刻的理解。
今天我就把这几年遇到的各类数据库锁死案例和处理经验整理一下。不敢说面面俱到,但至少下次你遇到类似情况,不至于手忙脚乱。
先搞清楚:数据库锁到底是个什么东西
很多人听到“锁死”两个字就觉得复杂,其实说白了,锁就是数据库为了保证数据一致性而设置的一道关卡。你改数据的时候加个锁,不让别人同时改,这很正常。但问题出在,有时候这把锁该解开的时候没解开,或者两个事务互相等对方放开锁,就出现了僵局。
锁死的情况五花八门,但根据我这些年遇到的案例,大致可以分成这么几类。
第一类是行锁等待超时。一个事务锁住了一行数据正在修改,另一个事务也要改同一行,只能等。等的时间太长了,应用就报超时错误。这种不算真正的死锁,但用户感知上就是“卡住了”。
第二类是死锁。两个事务互相持有对方需要的锁,谁都不放手,数据库的死锁检测机制会选中其中一个事务回滚,让另一个继续。这种情况数据库通常会自动处理,但如果频繁发生,说明业务逻辑有问题。
第三类是表锁或者元数据锁。这类锁最让人头疼,因为它会波及到整张表。只要你有一个查询或者事务没结束,后面所有跟这张表有关的操作都可能被堵住。我那天的故障就是这种。
第四类是间隙锁。这是MySQL的Repeatable Read隔离级别下特有的锁机制,会在一个范围内加锁,防止幻读。但如果范围太大,可能锁住大量本不需要锁的行,导致并发能力急剧下降。
不同类型,处理方式完全不同。咱们一个一个说。
案例一:元数据锁MDL,一个没结束的查询堵了所有人
先详细说说那天下午的事故。起因其实很小,有个运营同事要导出一张大表的数据做分析,在数据库管理工具里跑了一个select查询。那张表有几千万行数据,查询跑了很久没结束。与此同时,研发团队正在做一次上线,需要给这张表加一个新字段。执行alter table语句的时候,它需要获取这张表的元数据锁。
问题就出在这里了。那个慢查询还拿着这张表的元数据锁没有释放,alter table语句拿不到锁,只能排队等待。而alter table语句本身也会持有锁,它后面的所有增删改查操作,都得等这个alter table完成。就这样,一个慢查询引发了一场多米诺骨牌式的连锁反应,整张表的所有操作全部被堵住了。
找到根源之后,处理就简单了。我用show processlist找到了那个执行时间最长的慢查询连接,记下了它的Id,然后用kill命令把它终止掉。那个连接一断开,元数据锁就释放了,alter table语句立刻拿到了锁并执行完成,后面排队的操作也陆陆续续恢复了。整个过程大概持续了二十多分钟,但这二十分钟里,订单系统基本处于瘫痪状态。
这次事故之后,我们定了几条规矩。第一,在业务高峰期绝对不允许对大表执行alter table这样的结构变更操作。所有表结构变更必须经过评审,安排在业务低峰期执行。第二,对大表做select查询的时候,必须设置合理的超时时间,避免一个查询无限期地占用资源。第三,建立了元数据锁的监控告警,当等待时间超过阈值时,自动报警,甚至自动kill掉引发问题的会话。
案例二:行锁等待,一个忘记提交的事务成了钉子户
还有一种锁死,不严重,但特别隐蔽。有一次客户反馈,他们的积分系统里,用户积分扣除操作有时候会卡住,转圈很久之后报超时。这个问题不是每次都出现,偶尔来一下,排查起来很费劲。
后来我们在数据库层面开启了慢查询日志和锁等待监控。终于抓到了一个现场。有一个事务已经运行了将近一个小时,状态是Sleep,说明这个事务早就执行完了,但是应用程序没有提交也没有回滚,就这么悬在那里。更关键的是,这个事务持有了积分表里某几行数据的行锁。
有了这个钉子户,后面任何需要修改这几行数据的操作,都得等它释放锁。而这个事务永远也不会自己释放,因为应用程序那边连接已经超时关闭了,但数据库这边的事务还活着。这种僵尸事务是行锁等待最常见的原因之一。
处理方法其实很简单,找到那个长时间未提交的事务,确认没有重要操作之后,直接在数据库里kill掉对应的连接。事务会回滚,持有的锁也就释放了。但更重要的是,要找到应用层面的根因。后来我们查代码发现,积分扣除的接口里,有一处异常处理的逻辑写错了,发生异常的时候直接return,没有执行事务的提交或者回滚操作。这就导致事务一直悬在那里,直到数据库连接超时。
解决之后我们还做了一件事,在数据库的配置里设置了两个参数。一个是innodb_lock_wait_timeout,控制行锁等待的超时时间,超过这个时间就直接报错,不让应用程序一直等下去。另一个是设置了一个脚本,定期检查运行时间超过一定阈值的事务,自动发出告警,让运维人员介入处理。
案例三:死锁,两个事务互相掐架
死锁这个事,听起来很高深,其实道理很简单。事务A锁住了资源1,等着资源2。事务B锁住了资源2,等着资源1。两个都等对方放手,就僵住了。好在MySQL的InnoDB引擎有死锁检测机制,会主动介入,选择一个代价较小的事务回滚掉,让另一个继续执行。
虽然数据库会自动处理死锁,但如果你的业务里频繁出现死锁,那肯定是有问题的。我之前遇到过一家做库存管理的公司,他们的系统中频繁出现死锁告警,每次死锁都会导致一部分用户的请求失败,用户体验很差。
我帮他们分析了一下死锁日志,发现了一个规律。所有的死锁都发生在同一个场景下,多个用户同时扣减同一个商品的库存。事务A和事务B几乎同时读到库存数量是10,然后各自扣减1,都要把库存更新成9。因为两个事务在更新之前都加了行锁,第一个更新成功的会锁住这行,第二个只能等。但如果第一个事务在更新之后还需要读取其他数据,锁没有立即释放,第二个事务在等待的时候又发起了一个新的更新请求,就有可能触发死锁。
解决的方法有好几种,我们选了比较彻底的一种,在扣减库存的操作上,使用了乐观锁的机制。每次更新的时候带上原来的库存数量作为条件,update语句返回的影响行数如果是0,说明期间库存被别人改了,就重试整个操作。这样虽然在高并发下可能会有几次重试,但彻底避免了死锁的可能性。另外一个做法是把分散的事务合并成一个,减少锁持有的时间,也能显著降低死锁的概率。
案例四:间隙锁,一个范围查锁住了整片区域
间隙锁这个问题,很多人第一次遇到的时候都会觉得莫名其妙。明明是修改一行数据,怎么把附近根本不存在的行也锁住了。
有一个做抢购活动的客户遇到过。他们在商品表中做了一个批量更新的操作,把某个价格区间的商品状态改为下架。执行的SQL类似update products set status=off where price between 100 and 200。这条语句执行完之后,同一张表上的其他插入操作就卡住了,尤其是插入price在100到200之间的新商品时,会一直等待。
原因就在于间隙锁。在MySQL的Repeatable Read隔离级别下,为了防止幻读,innodb不仅会锁住符合条件的现有行,还会锁住这些行之间的间隙。这样其他事务就无法在这个间隙里插入新的记录,保证了同一个查询在同一个事务内多次执行得到的结果是一致的。但副作用就是,并发插入的能力大大降低了。
对于这个客户的场景,其实没有必要使用Repeatable Read级别,因为抢购活动的业务逻辑里并不需要防止幻读。我们把数据库的事务隔离级别调整成了Read Committed,并且把事务的尺寸尽量缩小,只在一个update语句的范围内持有锁。这个调整做下去之后,间隙锁的问题就不再出现了,并发插入也恢复正常了。
还有一个更直接的方法,就是尽量使用唯一索引来做条件过滤。如果你的update或者delete语句使用的条件是唯一索引,innodb只会锁住那一条记录,不会产生间隙锁。这是最理想的情况。
第五步:处理锁死问题的通用工具箱
讲完了案例,我想分享一套处理数据库锁死问题的通用方法。这套方法我自己用过很多次,每次都管用。
当你发现数据库疑似锁死的时候,第一步要做的是查看当前的进程列表。在MySQL里执行show full processlist,看看哪些连接的状态不是Sleep,而是像Updating、Sending data、Waiting for table metadata lock这类状态。观察每个连接运行了多长时间,如果发现有运行了几分钟甚至几十分钟的查询,那很可能就是问题的源头。
第二步,如果是想找元数据锁或者行锁等待的具体关系,有几张系统表很有用。information_schema下的INNODB_TRX可以看到当前所有正在运行的事务,包括它们已经运行了多长时间、是否持有锁。INNODB_LOCKS展示了当前持有的锁和等待的锁。INNODB_LOCK_WAITS则直接告诉你谁在等谁。把这几张表关联起来查询,就能画出一张清晰的锁等待关系图。
第三步,找到那个阻塞了别人的会话之后,确认它是不是一个可以终止的会话。通常来说,运行时间很长而且没有在做实际数据修改的查询,比如慢select,是可以直接kill的。但如果是一个大型数据更新操作,kill掉可能会导致已经执行的部分需要回滚,这可能需要很长时间,有时候甚至比等它执行完还要久。这时候需要权衡一下,到底是等还是杀。
第四步,如果kill掉会话之后问题依然没有解决,或者锁死的情况反复出现,那就要考虑是不是数据库参数配置的问题了。检查一下innodb_lock_wait_timeout的值是不是设得太大了,导致应用程序等待太久才超时。检查一下事务隔离级别是不是设成了Serializable,这种级别下锁的粒度非常粗,很容易引发锁等待。
第五步,从应用层面做根本性的优化。上面所有的案例最终都指向了一个共同的方向,数据库锁死很大程度上是应用设计的问题。事务是不是开得太大了,能不能拆分成多个小事务。索引是不是用对了,没有索引的更新操作会锁住整张表。更新的并发度是不是太高了,能不能通过排队或者限流来控制。这些问题的解决,比在数据库层面亡羊补牢要有效得多。
预防,比处理更重要
数据库锁死这件事,处理起来不算太难,但它发生的时机往往很不凑巧,总是在业务最忙的时候。这也就意味着,预防比处理更重要。
我后来给自己定了几条原则,每次做数据库相关的设计或者代码Review的时候都会过一遍。
第一,所有的事务都遵循一个原则,能短则短。事务里只放必要的操作,那些耗时的计算、外部接口调用、文件读写,统统放在事务外面。事务持有锁的时间越短,发生锁冲突的概率就越低。
第二,多个资源的访问顺序要固定。如果多个事务都需要访问同一组表或者同一组行,尽量按照相同的顺序来访问。比如先更新订单表,再更新库存表,所有事务都遵循这个顺序,就能大大降低死锁的概率。
第三,索引一定要建对。更新和删除操作的where条件必须走索引,否则会导致行锁升级为表锁,那就不只是锁死的问题了,整张表都别想动了。
第四,建立完善的锁监控体系。不仅是告警,还要把锁等待的日志定期收集起来,分析哪些表和哪些SQL语句是锁冲突的高发区,有针对性地优化。
第五,定期做数据库的压力测试,模拟高并发场景下的锁冲突情况。在测试环境里把问题暴露出来,总比在生产环境里措手不及要好得多。
从慌到稳,只需要一套清晰的思路
那次下午的事故,处理完之后我坐在工位上发了好一会儿呆。数据库锁死这个问题,其实技术上并不复杂,但它考验的是你在压力下的判断力。该不该kill这个会话,杀了之后会不会有副作用,有没有更好的处理方式,这些决策需要在很短的时间内做出。
后来我养成了一个习惯,每次遇到新的故障类型,处理完之后都会整理成一份简单的文档,把现象、排查步骤、处理命令、注意事项都写清楚。隔一段时间就翻出来看一看,温习一遍。等到下次真的再遇到的时候,脑子里的反应就变成了条件反射,不会慌了。




使用微信扫一扫
扫一扫关注官方微信 

