致青春-Redisson分布式锁使用不当,留下了血的教训

1,251 阅读3分钟

最近在做一个业务需求,发现了之前的一个Bug, 这次的业务需求是做一个音乐伴奏高潮部分的识别功能,要把几十万的存量数据数据调用三方能力平台进行识别(匿了,我是API调用侠), 简单来说就是:

未命名文件.png

受限对方服务的处理性能,前前后后差不多持续了半个月才识别完成,测试同学对识别结果进行抽取验证,发现只有几百条来个部分数据 是对方识别成功,但通过运营平台上查看并没有识别结果,纳尼?第一反应就是对方是不是识别了没做回调喔,对方反馈识别没啥问题,也回调成功了的,秀了一下证据,好吧,那只有在看看内部是不是有问题。

直接拿一条数据进行手动触发回调接口,发现数据更新返回成功,缓存也更新了,运营后台还是显示没识别结果,那奇怪了。然后用对应的内容和版权ID查询一下,哇哦,竟然发现有两条记录,看时间,原始数据入库时间基本是同一时刻。

bc61861bcf0482ceeed683a2109fb38.jpg

('223482', '版权ID', '内容ID', '1', NULL, '2099-12-31 00:00:00', NULL, NULL, '歌曲名', '3', 'SHENGSHENGMAN', 'SSM', NULL, '[\"108\"]', '1', '$1420018489', 'MATERI', 'mtv', NULL, '2022-02-25 17:36:32', '2022-10-22 21:50:11', '149', '192', '0.7805');
('223483', '版权ID', '内容ID', '1', NULL, '2099-12-31 00:00:00', NULL, NULL, '歌曲名', '3', 'SHENGSHENGMAN', 'SSM', NULL, '[\"108\"]', '1', '$1420018489', 'MATERI', 'mtv', NULL, '2022-02-25 17:36:32', '2022-02-25 17:36:32', '-1', '-1', '0');

原始数据来源外部核心数据服务的分发,内部使用MQ异步消费入库,入库是一个比较优秀的同学写的,做了一套比较nice的封装,同学的代码写的很优雅,虽然入库的并发量不是很高,但使用了Redisson做了分布式锁控制,避免产生重复数据,入库部分的主要逻辑是这样的,看起来是没什么大毛病:

public int addOrUpdate(SendObj sendObj) {
	//加分布式锁
	String key = RedisKeys.LOCK_KEY + sendObj.getContentId();
	RLock rLock = RedissonLockUtil.tryLock(key);
	if (rLock == null){
            return 1;
	}
	try {
		// 其它业务逻辑
		//获取产品信息
		Resource old = query(sendObj.getCopyrightId(),sendObj.getContentId());
		if (old == null){
                    //新增
                    return insert();
		}
		//修改
		return update();
	}catch (BadSqlGrammarException e){
            //不用回滚
            return 1;
	}finally {
            rLock.unlock();
	}
}

5年老码农看这段代码确实没看出什么大毛病(各位觉得有啥问题没有),那为什么会几乎在同一时刻入库两条数据呢?百思不得解,既然这样就看看调用逻辑吧:

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = RollbackException.class)
public int addOrUpdate(SendObj sendObj) {
	int i = service1.addOrUpdate(sendObj);
	if (i < 1){
            log.error("数据库处理产品信息失败,data:{}",sendObj);
            throw new RollbackException(CommonError.SYSTEM_ERROR,"数据库处理产品信息失败");
	}
	//获取产品资源
	i = service2.addOrUpdate(sendObj);
	if (i < 1){
            log.error("数据库处理产品文件资源信息失败,data:{}",sendObj);
            throw new RollbackException(CommonError.SYSTEM_ERROR,"数据库处理产品文件资源信息失败");
	}
	return 1;
}

各位看出猫腻了没,这个接口是有标记@Transactional注解,即我们执行完第一步的入库之后,其实数据库并没有执行commit事务提交,这个时候如果资源又被请求过来,通过检测数据库中是不存在该资源的,又执行一次入库操作,这样就就可能会造成同一条数据记录多次的情况,所以对方分发了两条数据过来,或者说MQ消费时可能存在重复消费问题。

那到底是不是这样的呢?由于是概率性事件,再加上是现网数据不好操作,就抱着这个想法写了一个demo来测试一下吧:

@Autowired
private TestTableMapper testTableMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = RollbackException.class)
@Override
public void save() {
    doSave();
    // 休息一会儿
    try {
        Thread.sleep(50000);
    } catch (Exception e) {
    }
}

public int doSave() {
    TestTable testTable = new TestTable();
    testTable.setName("test");
    String key = "lock:" + testTable.getName();
    RLock rLock = RedissonLockUtil.lock(key);
    if (rLock == null) {
        log.info("rlock fail");
        return 1;
    }
    try {
        QueryWrapper<TestTable> query = new QueryWrapper<>();
        query.setEntity(testTable);
        TestTable db = testTableMapper.selectOne(query);
        if (null == db) {
            return testTableMapper.insert(testTable);
        }
        return testTableMapper.update(testTable, query);
    } catch (Exception e) {
        return 1;
    } finally {
        rLock.unlock();
    }
}

起了一个简易接口调用:

@RequestMapping("/save")
public String httpSave() {
    new Thread(() -> testService.save(), "test").start();
    new Thread(() -> testService.save(), "test1").start();
    return "success";
}

来看一下结果,果然数据库里面name均为test的两条数据:

image.png

作为核心基础数据,这个异常数据对很多服务影响都不小的,不单单是这个资源数据,基于该封装框架下的其它类型数据也存在同样问题,所以我们立马启动修复、验证上线工作,问题才算告一段落,所以,到这里,事就是这么个事儿,在工作中,无论是负责核心业务还是普通业务,使用到三方组件,一定要需要好好我们结合具体的业务场景,多分析,多验证,才能让系统更稳健运行。

微信图片_20221031231321.jpg

我不是大佬,我只是在学习路上,最近研究了一下公众号,哈哈,挺有意思。

扫码_搜索联合传播样式-标准色版.png