死锁问题是生产上比较常见的问题, 异常信息是
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:Deadlockfoundwhentryingtogetlock;tryrestartingtransaction
1 更新条件为主键或者有索引->加行锁
2 更新条件无索引-> 加表锁
1 大事务化为小事务:
大事务执行时间长,锁的占用时间就长, 其他线程等待锁时间也越长, 发生死锁的概率也就越高
2 更新条件加索引:
如果不加索引, 锁的是整张表 , 针对这张表的任何更新操作都要等待锁释放,所以不要因为表数据量小就不加索引, 针对于频繁更新的字段更要加索引, 降低锁的粒度
3 按照顺序更新 :
行锁产生的死锁是生产最常见原因, 核心问题就是两边线程更新顺序不一致,
针对于同一张表不同行的死锁可以按照 id 顺序更新
针对于不同表的死锁可以在代码中约定更新表的顺序
wait-for graph原理
MySQL 每个事务执行都有对应的事务 id,记录这持有锁,等待锁信息, 当发生锁等待时就会触发wait-for graph算法
比如,事务1给A加锁,事务2给B加锁,同时事务1给B加锁(等待),事务2给A加锁就发生了死锁。发生死锁, 发生死锁后 MySQL会默认回滚最小的事务(更新行数最少)
11-16晚, 在 pps 定时任务刷新库存缓存入库时,发生了数据库死锁
继续剖析得到产生死锁的方法和参数
com.weidiango.pps.stock.impl.ProductSkuStockServiceImpl#updateSkuSellStockCore
定位出 更新 t_product 表 id=674987 发生了死锁
该方法为更新库存通用方法,调用端如下
* 调用方: * 1 erp推送修改库存 * 2 下单缓冲区定时入库 * 3 订单发货前修改sku/售后退货/换货扣减sku库存 * 4 下单库存为0->非0 或者 非0->0 时 * 5 平台产品列表手动更新 * 6 添加预售/现货补货计划增加sku库存 * 7 商家后台手动更新
具体哪两个调用端并发调用导致了死锁呢? 继续查看 loghub 日志排查
死锁一般都是并发操作导致的, 同时更新 t_product 表 id=674987 在该秒内有两处
1 更新 erp 推送的库存 (正常执行,未回滚)
2 定时任务执行更新缓冲区库存入库 (死锁, 抛出异常回滚)
通过步骤 3 推测出两个调用端分别为
1 更新 erp 推送的库存 (事务 2)
2 定时任务执行更新缓冲区库存入库 ( 事务 1)
这里主要看下事务 1,2 的持有锁和等待锁
持有锁: 证明当前事务更新过改行, 但是事务还未提交,锁还处于持有状态
等待锁 : 其他事务中持有锁,其他事务暂未提交, 当前事务获取不到锁, 处于等待状态
事务 1 的等待锁 : t_product , 证明在其他事务中有对 t_product表的修改,还未提交
事务 2 的等待锁 : t_item , 说明其他事务中有对 t_item 表的修改,还未提交
事务 2 的持有锁 : t_product, 说明在事务 2 中更新过 t_product 表, 还未进行提交
com.weidiango.pps.stock.impl.ProductSkuStockServiceImpl#updateSkuSellStockCore
该接口为更新 sku 库存接口, 在此接口中会判断库存是否存在缓冲区,存在缓冲区会
更新 t_item表,与更新 t_product 表 两处更新存在间隔, 其他线程真对同一 productId先更新 t_product 后更新 t_item 时 会产生死锁
经过分析: 该方法更新库存时更新缓冲区销量本身逻辑存在不合理的情况, 产生了一定的耦合性, 所以吧更新t_item 操作统一放在定时任务批量操作,不再耦合在更新库存方法中, 这样就解决了 两张表不按照顺序更新时产生的死锁
当两个线程并发操作换货操作, a->b, b->a 时, 会发生死锁
互换死锁是未按照固定的顺序更新导致的, 解决方案是将 a,b sku 根据 id 排序, 按照固定的顺序更新库存
参考链接
Mysql 死锁和死锁的解决方案_三3三的博客-CSDN博客_mysql 表死锁
mysql死锁问题分析 - chengchao - 博客园