记一次项目上Mysql死锁问题的定位

最近项目上接了一个需求,处理系统中出现的慢SQL,排查过程中通过druid的监控看到了系统中不少死锁的报错提示,即

Deadlock found when trying to get lock; try restarting transaction

  1. 首先根据报错信息行数定位到具体代码报错位置,固定到service层,可以看到如下代码
  2. CTReportDaily dbCTReportDaily = ctReportDailyDao.getByCtAndShopAndDay(bloc, project,
            collectionTerminalNo, "-", tradeBalanceTime);
        if (dbCTReportDaily == null) {
          CTReportDaily ctReportDaily = new CTReportDaily();
          ctReportDaily.setUuid(UUID.randomUUID().toString());
          ctReportDaily.setBloc(bloc);
          ctReportDaily.setProject(project);
          ...
          ctReportDailyDao.insert(ctReportDaily);
          LogUtil.info(logger,
              MainEvent.fetchMainEvent(bloc, project, collectionTerminalNo, EventStage.T41));
        } else {
          dbCTReportDaily.setInitTradeCount(initTradeCount);
          ctReportDailyDao.updateyTicketData(dbCTReportDaily);
          LogUtil.info(logger,
              MainEvent.fetchMainEvent(bloc, project, collectionTerminalNo, EventStage.T41));
        }

    简单理解为按条件查询 CTReportDaily表记录,查询不到新增,查询到则更新。此处getByCtAndShopAndDay()查询已走唯一键索引,具体报错在 ctReportDaily.insert(ctReportDaily);这一行报死锁错误

  3. 开始简单的以为查询没有锁住行,导致并发更新该行而死锁,于是在 getByCtAndShopAndDay()查询中增加 for update,使其由快照读变成当前读,即加上一个排它锁,其他事务不允许读不允许写。但是加上后经过压测发现死锁问题并没有解决。。。
  4. 仔细研究日志后发现,另外一处报死锁的日志,定位到代码如下
  5. CTReportDaily dbCTReportDaily = ctReportDailyDao.getByCtAndShopAndDay(bloc, project,
              collectionTerminalNo, "-", printDate);
          if (dbCTReportDaily == null) {
            CTReportDaily ctReportDaily = new CTReportDaily();
            ctReportDaily.setUuid(UUID.randomUUID().toString());
            ctReportDaily.setBloc(bloc);
            ctReportDaily.setProject(project);
            ...
            ctReportDailyDao.insert(ctReportDaily);
            LogUtil.info(logger,
                MainEvent.fetchMainEvent(bloc, project, collectionTerminalNo, EventStage.T40));
          } else {
            if (cTReportTicket.getTicketCount() != null) {
              dbCTReportDaily.setTicketCount(cTReportTicket.getTicketCou
### MySQL 行级的使用场景及案例分析 #### 1. **行级的特点** 行级是一种细粒度的机制,主要用于提高并发性能。它只定满足条件的具体行数据,而不影响其他不相关的行[^4]。这种特性使得多个事务可以在同一张表的不同行上同时进行修改操作。 #### 2. **行级的应用场景** ##### (1)高并发环境下的更新操作 在高并发环境中,尤其是涉及频繁更新特定行的操作时,行级能够有效减少冲突的发生概率。例如,在电商系统的订单处理模块中,当用户下单后需要扣减库存数量时,可以通过行级确保每次仅对指定商品的库存录加,从而避免因全表而导致的性能下降[^3]。 ```sql UPDATE products SET stock = stock - 1 WHERE product_id = 10 AND stock > 0; ``` 此查询只会定 `product_id = 10` 的那一条录,并允许其他线程访问或修改表中的其他行[^4]。 --- ##### (2)范围查询引发的潜在问题 尽管行级理论上只作用于具体行,但在实际应用中需要注意其局限性。例如,当执行带有范围条件 (`WHERE`) 的 SQL 查询时,MySQL 可能会对符合条件的所有索引区间施加重叠的间隙 (Gap Lock),进而阻止新录插入到这些被定的范围内[^3]。 考虑如下例子: - 原始数据仅有两条录:`(id=1, money='10')`, `(id=7, money='20')`。 - Session A 执行了一次范围更新操作: ```sql UPDATE test SET money = CAST(money AS UNSIGNED) * 10 WHERE id BETWEEN 1 AND 6; ``` - 此时,Session B 尝试向表中插入一条新的录 `(id=2)`,但由于 Session A 对 `[1, 6]` 范围内的所有可能键值都加上了间隙,因此 Session B 需要等待直到 Session A 提交或回滚交易才能继续。 --- ##### (3)防止幻读现象 为了保障事务的一致性和隔离级别要求,InnoDB 默认采用可重复读 (Repeatable Read) 模式运行。在此模式下,除了普通的外还会附加额外类型的(如 Next-Key ),以杜绝所谓的“幻影读取”问题[^1]^。这意味着即便某个事务尚未显式请求某一行的数据变更权限,只要存在对该区域扫描的可能性,就有可能触发相应的预防措施。 举例来说,假设有两个会话分别按照下面的方式交互操作同一个表格里的信息: - Sesssion C 开启了一个 RR 级别的事务并检索当前状态; - 接着另一个独立进程成功写入若干新增项目至目标集合内部; - 如果没有恰当控制手段介入,则前者再次调用相同命令时很可能会看到之前未预料的结果集扩展部分 —— 即所谓‘幽灵’条目显现出来;然而借助合理配置好的 NXK-locking strategy 则完全可以规避此类风险状况发生。 --- #### 3. **注意事项与优化建议** - **索引的重要性**: 行级依赖于有效的索引来定位定的目标行。如果没有适当建立索引结构,可能导致整张表退化成类似表级定的行为,严重影响效率[^4]。 - **死锁检测与解决**: 多个事务竞争不同顺序上的资源容易引起循环等待局面形成死锁事件。为此系统内置有专门算法用于快速识别并主动牺牲其中一个参与者来打破僵局恢复正常流程运转. - **时间戳管理策略调整**: 根据业务需求灵活选用适合自己的版本号标方案有助于进一步提升整体吞吐量表现水平. --- ### 结论 综上所述可以看出,正确理解和运用好MYSQL所提供的各种层次级别的封功能对于构建高效稳定的关系型数据库至关重要。特别是在面对复杂多变的实际应用场景之时更是如此。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值