一. 事故概述
春节期间, 生产系统多次出现假死不可用现象, 导致绝大部分业务无法进行. 主要表现现象为接口无法访问.
背景为900W+客户表和近实时ES, 以及春节期间疫情导致的普通卖菜场景近似秒杀等.
二. 排查过程
优先排查了info, error, catalina日志, 发现以下异常:
主要的异常信息为:
Cause: java.sql.SQLException: Lock wait timeout exceeded;
try restarting transaction
引起该异常通常是由高频行锁或表锁同时操作数据库导致. 上一次事务还未完成, 新的事务尝试获取锁等待超时.
但根本原因是业务涉及缺陷或代码不合理导致的事务无法正常提交或事务体执行时间过长. 引起获取锁等待超时.
参考异常中的业务代码, 如1图中的库存扣减逻辑, 和2图中的购物车逻辑.
- 排查代码时发现库存扣减存在业务上设计的不合理, 原业务为在支付回调时扣减库存, 造成超卖现象很严重(甲方允许超卖), 扣减在update语句中实现, 库存字段为unsigned, 所以负数抛出异常导致事务无法正常提交.
- 在创建订单时的大事务体中包含购物车删除逻辑, 有很多查询, 创建, 删除操作数据库的逻辑, 在近秒杀场景中, 高频商品的记录被阻塞在事务中, 最终导致其他线程获取锁超时.
三. 解决方案
比较主流的处理方案是查询以下几张表, 删除事务表中无法正常释放的事务锁记录
information_schema.INNODB_TRX;
information_schema.INNODB_LOCKs;
information_schema.INNODB_LOCK_waits;
但基于事故发生的时间和严重程度, 该方案只能治标, 不能治本.
所以根据异常中两处不同的业务代码入手
发现共同点: 均存在大事务代码体, 个别业务存在事务期间刷新ES操作.
解决方案如下:
- 异步化刷新ES.
- 降低事务体大小和数据库操作, 将非强一致性要求的操作移到事务外.
- 酌情去除事务.
- 优化畸形代码(特指历史遗留或未经review的脏代码)
- 查询类接口考虑读写分离
- 利用缓存合理规避高频业务对数据库带来的压力
- 对于代码提交做把控, 有效的review可以减少脏代码的产生.