乐趣区

关于mysql:MySQL行级锁解决消费队列中出现的唯一主键错误

前言:最近我的项目中的定时工作生产队列始终呈现一个反复的惟一主键谬误,是多个事务对同一行数据进行操作引起的。解决这个问题后,我便写了这篇博客的草稿,由 ctx 同学强势审核批改后,便有了这一版本的博客。

场景复现

出错办法:

private void handleAccountRisk(String accountUuid, float risk, RiskConfEtcdVo riskConf, Long currentTimeSeconds) {
    // 分布式锁
    String redisKey = RISK_UPDATE_LOCK_KEY.replace("${item}", accountUuid);
    String businessId = BusinessIdGeneratorUtil.businessId();
    redissonLockUtil.lock(redisKey,businessId, () -> {AccountRiskEntity accountRisk = accountRiskTplDao.getAccountRiskByUuid(accountUuid);
        // 如果 accountRisk 不存在则新增
        if (null == accountRisk) {
            // 实例化一个对象后填充...
            // insert
            accountRiskTplDao.save(target);
        // 如果存在则更新
        } else {
            ...
            accountRiskTplDao.updateById(accountRisk);
            ...
        }
    });
}

报错信息:

INSERT INTO account_risk  (create_time, update_time, risk, uuid)  VALUES  ( ?, ?, ?,
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'dc57ktzmtiwp' for key 'uuid'   

问题剖析

从 Duplicate entry 推断出反复插入。具体起因为:

多个事务在执行这个办法。
事务 A:依据 uuid 查不到数据,实例化一个实体,insert, 但还未提交事务
事务 B:依据 uuid 查不到数据,实例化一个实体
此时:A 提交了事务,B 执行 insert
事务 B:insert 中呈现报错:Duplicate entry 'dc57ktzmtiwp' for key 'uuid'

解决方案

依据下面的剖析,主须要对指定 uuid 对应的行数据加锁,不容许多个事务同时对该行记录进行操作。
能够通过应用 innodb 行级锁来解决。

getAccountRiskByUuid 办法的 sql 前面加上for update:

public AccountRiskEntity getAccountRiskByUuid(String uuid){QueryWrapper<AccountRiskEntity> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("uuid",uuid);
    // 乐观锁
    queryWrapper.last("FOR UPDATE");
    return baseMapper.selectOne(queryWrapper);
}

类似场景

DDIA 的第七章事务中,对这种状况有具体的举例形容。

对于 InnoDB 行锁

加锁形式

  • 共享锁(S):select * from table_name where … lock in share mode;
  • 排他锁(X):select * from table_name where … for update;

应用场景

如果遇到存在高并发并且对于数据的准确性很有要求的场景,是须要理解和应用 for update 的。比方波及到金钱、库存等。个别这些操作都是很长一串并且是开启事务的。如果库存刚开始读的时候是 1,而立马另一个过程进行了 update 将库存更新为 0 了,而事务还没有完结,会将错的数据始终执行上来,就会有问题。所以须要 for upate 进行数据加锁避免高并发时候数据出错。记住一个准则:一锁二判三更新

InnoDb 行锁的实现形式:

InnoDB 行锁是通过给索引项加锁来实现的,如果没有索引,InnoDB 将通过暗藏的聚簇索引来对记录桎梏。没有索引的话会进化成锁表!

幻读:指以后某个事务在读取某个范畴内的记录时,另外一个事务又在该范畴内插入了新的记录。当之前的事务再次读取该范畴的记录时,会产生幻行。—《高性能 Mysql》

集体了解:当咱们根据(某个事务在读取某个范畴内的记录)的后果作为判断条件决定后续的操作,在另一个事务(又在该范畴内插入了新的记录)后,之前判断条件的后果就可能会被扭转了。违反了这个判断条件,那么后续的操作就须要放弃疑难了?

退出移动版