半夜两点,手机嗡嗡震个不停。值班同事发来消息:“线上订单表锁死了,所有写入都堵住了,赶紧起来看看!”这种场景,干过数据库运维的朋友估计都熟。MySQL锁表这事儿,就像老小区的水管,平时不觉得,一旦堵了,楼上楼下全遭殃。网上搜“mysql数据库锁表了怎么解决”,答案倒是不少,但多数是复制粘贴的官方文档,看得人一头雾水。今天咱不整那些虚的,直接上实战经验,把五种真正管用的解决方案掰开揉碎了讲清楚。

先搞清楚锁表到底卡在哪。MySQL的锁分两类,读锁和写锁。读锁是共享的,大家都能读,互不干扰;写锁是排他的,你占了别人就得等着。问题往往出在写锁上——一个事务迟迟不提交,或者查询跑了半天没结束,行锁升级成表锁,整个表就像被按了暂停键。我见过最夸张的一次,开发同事在测试环境跑了个全表UPDATE,忘了加WHERE条件,直接锁了半小时,生产环境跟着遭殃。所以说,锁表不是MySQL的bug,是使用姿势出了问题。
第一种解决方案,最简单粗暴——直接杀进程。SHOW PROCESSLIST;这条命令,能把当前所有连接列出来,找到State列显示“Locked”或者“Waiting for table lock”的,记下Id,然后KILL掉。这招适合紧急情况,比如线上业务已经挂了,没时间分析原因,先止血再说。但有个坑得提醒你,如果你杀的是个长事务,回滚也需要时间,期间锁可能还占着。所以杀之前,最好确认下这个连接是不是真的卡死了,别误杀了正在跑的正常SQL。
第二种方案,调大锁等待时间。MySQL有个参数叫innodblockwait_timeout,默认50秒。也就是说,一个事务等锁超过50秒,直接报错放弃。你可以把这个值调大,比如改成120秒,给慢查询争取更多时间。但说实话,这只是缓兵之计。调太大,用户那边一直转圈,体验更差;调太小,稍微有点并发就报死锁错误。我建议,除非业务场景特殊,否则别动这个参数,治标不治本。真遇到频繁锁等待,得往下面几种方案上想。
第三种,也是我平时最推荐的——优化SQL,减少锁的粒度。很多锁表事故,根源就是一条烂SQL。比如,UPDATE语句的WHERE条件没走索引,MySQL只能全表扫描,把每条记录都锁一遍,哪怕只改一行。解决办法很简单:给WHERE条件字段加上索引,让UPDATE精准定位到目标行,锁的粒度就从表级降到了行级。还有个细节,事务里别做太多无关操作,比如先查个数据,再睡两秒,再更新,这期间锁一直攥在手里,别人只能干等。把事务缩短,锁的时间自然就短了。
第四种方案,拆分大事务,化整为零。有些业务逻辑,比如批量更新几十万条数据,你一个事务全包了,锁表时间肯定长。这时候,把它拆成一千条一批,每批单独提交,效果立竿见影。我接手过一个库存系统,原来每晚清账要跑二十分钟,期间整个库存表锁死,其他业务全堵车。后来改成每五千条提交一次,总耗时没变,但锁表时间缩短到几秒,其他业务几乎无感。这招的本质,是把“一次性占用资源”改成“分时占用”,对数据库和业务都友好。
第五种方案,上读写分离,把压力卸掉。如果锁表是因为读操作太多,拖慢了写事务,那就考虑把读请求分流到从库,主库专心处理写操作。MySQL的主从复制已经很成熟,稍微配置一下就能实现。但注意,读写分离有延迟,如果你对数据一致性要求极高,比如金融交易,那得谨慎。另外,从库也得监控,别从库也锁了,那就白搭了。这方案适合读多写少的业务,能极大缓解主库压力。
说到这儿,有朋友可能会问,能不能直接禁用表锁?MySQL里有个参数,叫autocommit,把它设为1,每条语句自动提交,锁的持有时间就短了。但这也意味着,你失去了事务的原子性,多步操作没法回滚。所以,别一刀切,得根据具体业务场景权衡。还有,定期检查慢查询日志,把那些经常出现的慢SQL揪出来优化,比出了事再救火强得多。
回到标题那句——五种实用解决方案,其实核心就一句话:锁表不可怕,可怕的是不知道怎么解。杀进程是急诊,调参是调理,优化SQL是根治,拆分事务是手术,读写分离是预防。你手里有这五把刀,遇到锁表心里就有底了。下次再有人半夜喊你起来处理锁表,先深呼吸,打开命令行,按这个顺序排查,大概率十分钟内搞定。要是搞不定……那就再读一遍这篇文章,顺便把慢查询日志调出来看看。数据库这东西,你对它好,它就不给你添乱。


