最近在做一个业务需求,发现了之前的一个Bug, 这次的业务需求是做一个音乐伴奏高潮部分的识别功能,要把几十万的存量数据数据调用三方能力平台进行识别(匿了,我是API调用侠), 简单来说就是:
受限对方服务的处理性能,前前后后差不多持续了半个月才识别完成,测试同学对识别结果进行抽取验证,发现只有几百条来个部分数据 是对方识别成功,但通过运营平台上查看并没有识别结果,纳尼?第一反应就是对方是不是识别了没做回调喔,对方反馈识别没啥问题,也回调成功了的,秀了一下证据,好吧,那只有在看看内部是不是有问题。
直接拿一条数据进行手动触发回调接口,发现数据更新返回成功,缓存也更新了,运营后台还是显示没识别结果,那奇怪了。然后用对应的内容和版权ID查询一下,哇哦,竟然发现有两条记录,看时间,原始数据入库时间基本是同一时刻。
('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的两条数据:
作为核心基础数据,这个异常数据对很多服务影响都不小的,不单单是这个资源数据,基于该封装框架下的其它类型数据也存在同样问题,所以我们立马启动修复、验证上线工作,问题才算告一段落,所以,到这里,事就是这么个事儿,在工作中,无论是负责核心业务还是普通业务,使用到三方组件,一定要需要好好我们结合具体的业务场景,多分析,多验证,才能让系统更稳健运行。
我不是大佬,我只是在学习路上,最近研究了一下公众号,哈哈,挺有意思。