干我们这行的,谁还没被数据库锁表坑过几回?半夜三点,手机突然震得像触电一样,群里炸了锅——“系统卡死了!”“订单全堵住了!”“老板在咆哮!”你迷迷糊糊爬起来,打开监控一看,数据库里一堆锁在排队,跟早高峰堵车似的。那一刻,你恨不得把键盘摔了,但还得冷静下来,找问题、解锁、恢复服务。锁表这事儿,说白了就是个“资源争夺战”,谁都想用同一行数据,结果谁也不让谁,大家一起死锁。今天咱就聊聊怎么治这个毛病,不说那些虚头巴脑的理论,全是实战经验。

锁表的本质其实没那么玄乎。数据库就像个大仓库,每个表就是货架,每行数据就是货架上的箱子。当你的程序要改某个箱子里的东西时,它会先给这个箱子贴个“正在使用”的标签,这叫行锁。但有些程序比较粗暴,直接给整排货架贴标签,甚至把整个仓库门锁上,这就叫表锁。比如 MySQL 里的 MyISAM 引擎,写操作时直接锁整张表,读操作都得等着。而 InnoDB 引擎虽然支持行锁,但如果你没走索引,它也会升级成表锁——这就像本来只想锁一个箱子,结果因为找不到钥匙,干脆把整栋楼都封了。所以第一步,先搞清楚你的数据库引擎是什么,锁的粒度有多粗,这是解决问题的前提。
实战中,最常见的锁表现就是“慢查询+高并发”。比如电商大促时,用户疯狂下单,你的程序可能先查库存,再扣库存,写订单。如果库存表用的是行锁,两个用户同时抢同一个商品,第一个用户还没提交事务,第二个用户就得等着。这时候,如果第一个用户的事务里还夹杂了其他慢查询,比如查了几百万行的日志表,锁就迟迟不释放,后面的请求全堵死。我见过最夸张的一次,一个事务里跑了 5 分钟的全表扫描,结果那 5 分钟里,整个订单系统直接瘫痪。解决思路很简单:把事务做小,把慢查询干掉。比如库存扣减,可以单独拆成一个短事务,查库存和扣库存放在一起,别跟其他无关操作混在同一个事务里。
还有一种情况是程序写得太糙,忘了释放锁。比如用 SELECT FOR UPDATE 加行锁,结果业务逻辑报错,事务既没提交也没回滚,锁就这么挂着。这时候你查 INFORMATIONSCHEMA.INNODBTRX 表,能看到一堆 “RUNNING” 状态的事务,有的已经跑了几小时。怎么处理?要么手动杀掉这些线程,用 SHOW PROCESSLIST 找到对应的 ID,然后 KILL 掉。但要注意,KILL 只是终止线程,事务可能还会回滚,回滚本身也很耗时。更聪明的做法是给事务设置超时时间,比如把 innodblockwaittimeout 设成 30 秒,超过就自动回滚。别指望程序员永远不犯错,用机制兜底才是正道。
索引设计也是锁表的隐形杀手。很多人觉得建了索引就万事大吉,但索引建得不对,反而会导致锁升级。比如你的表有个联合索引 (a, b),但查询条件只用了 b,没用到 a,索引就失效,数据库只能全表扫描,锁自然会升级为表锁。再比如查询里用了函数,像 WHERE DATE(createtime) = '2024-01-01',这也会导致索引失效。所以,定期检查慢查询日志,看看哪些查询走了全表扫描,优化相应的索引,锁表的概率能降一半。我有个朋友,他们系统每次月底结算就锁表,后来发现是一个统计查询没走索引,每次扫描几千万行,把表锁得死死的。加了复合索引后,问题再没出现过。
业务层面也有不少技巧可以避免锁表。比如把大表拆成小表,按用户 ID 哈希分表,每个用户的数据分散到不同物理表里,锁冲突自然减少。或者用消息队列削峰,用户下单时先丢到队列,后台慢慢处理,这样数据库的压力会平滑,不会瞬间爆发。还有一招是“乐观锁”,不依赖数据库的行锁,而是用版本号字段。每次更新时检查版本号是否一致,不一致就重试。这适合高并发但冲突少的场景,比如点赞、浏览计数。但要注意,乐观锁在冲突频繁时会加重数据库负担,因为重试次数多,所以需要根据业务特性选用。
别把锅全甩给 DBA。很多时候,锁表是产品设计的问题。比如一个页面要查询 10 张表的数据,每张表还带关联查询,这本身就在给数据库添堵。产品经理拍脑袋定需求,程序员闷头实现,结果上线后锁表成常态。作为技术人,得学会跟产品说“不”,或者用缓存、预计算等手段把压力前置。比如排行榜数据,别每次都查数据库,用 Redis 的 Sorted Set 存一份,更新时异步写库,锁表风险直接归零。记住,数据库是一道防线,能不动它就别动。
说到底,解决锁表没有银弹,关键在于“拆”。拆事务、拆查询、拆表、拆业务。每次遇到锁表,先别急着骂人,冷静分析是哪环节出了问题。是索引缺失?是事务太大?还是并发太高?找到根因,对症下药。我见过最狠的一个团队,直接把所有写操作改成异步,数据库只做简单的插入,更新操作全丢到消息队列,锁表从此成了历史。当然,这么做也有代价,数据一致性会变弱,需要额外的补偿机制。但有时候,为了系统的可用性,妥协是必要的。毕竟,比起半夜被电话吵醒,少点一致性也不是不能接受,对吧?


