问题描述
开发的项目是 Spring+Hibernate+达梦数据库,debug 发现存在一处历史代码,使用数据库表作为序号池,生成序号:
public Integer findNumberByKeyAndUpdateNumberByKey(String key) {
String sql = "select id as id,key,MAX(number_value) as number_value,create_by as create_by,update_by as update_by,create_time as create_time,update_time as update_time,tenant as tenant,is_void as is_void,company_code as company_code,version as version,department_code as department_code from SCM_NUMBER_GENERATION where key = ?1";
List parm = new ArrayList();
parm.add(key);
List<ScmNumberGeneration> scmNumberGeneration = scmNumberGenerationRepository.findByNativeSQL(ScmNumberGeneration.class, sql, parm);
if (scmNumberGeneration.size()<1 || StringUtil.isEmpty(scmNumberGeneration.get(0).getNumberValue())){
ScmNumberGeneration scmNumberGeneration1 = new ScmNumberGeneration();
scmNumberGeneration1.setKey(key);
scmNumberGeneration1.setNumberValue(1);
scmNumberGenerationRepository.save(scmNumberGeneration1);
return 1;
}else{
ScmNumberGeneration scmNumberGeneration1 = scmNumberGeneration.get(0);
Integer num = scmNumberGeneration1.getNumberValue() + 1;
scmNumberGeneration1.setNumberValue(num);
scmNumberGenerationRepository.save(scmNumberGeneration1);
return num;
}
}
在这个项目里,所有的 JPA 实体类都使用 version 字段作为乐观锁,因此这里通过 Hibernate 保存序号池的代码,如果存在并发就会报错:
scmNumberGenerationRepository.save(scmNumberGeneration1);
可以通过测试用例用多线程进行模拟验证:
public void test() {
// 使用多线程模拟并发进行测试
final int threadNum = 5;
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
executorService.execute(() -> {
try {
TenantUtil.runTaskByTenant(TenantUtil.JTKJ_TENANT_ID, () -> {
Integer scmPayReqH = scmNumberGenerationService.findNumberByKeyAndUpdateNumberByKey("test");
System.out.println(scmPayReqH);
});
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
与一般业务代码不同,序号池生成需要保证一定程度的并发,当然更好的做法是使用 Redis 实现序号池生成,但如果并发要求不高,也可以有限度地改造现有代码。
解决方案
这里简单地使用异常捕获-重试的方式简单解决:
public Integer findNumberByKeyAndUpdateNumberByKey(String key) {
Integer number = retryFindNumberByKeyAndUpdateNumberByKey(key, 5);
log.debug("序号池 SCM_NUMBER_GENERATION 获取序号[{}-{}]成功", key, number);
return number;
}
private Integer retryFindNumberByKeyAndUpdateNumberByKey(String key, int retryCount) {
if (retryCount <= 0) {
log.error("序号池 SCM_NUMBER_GENERATION 生成序号[{}]失败,重试次数已用完", key);
throw new GlobalBizException("序号生成出错");
}
log.debug("序号池 SCM_NUMBER_GENERATION 开始尝试获取序号[{}],剩余重试次数{}", key, retryCount);
retryCount--;
String sql = "select id as id,key,MAX(number_value) as number_value,create_by as create_by,update_by as update_by,create_time as create_time,update_time as update_time,tenant as tenant,is_void as is_void,company_code as company_code,version as version,department_code as department_code from SCM_NUMBER_GENERATION where key = ?1";
List parm = new ArrayList();
parm.add(key);
List<ScmNumberGeneration> scmNumberGeneration = scmNumberGenerationRepository.findByNativeSQL(ScmNumberGeneration.class, sql, parm);
if (scmNumberGeneration.size() < 1 || StringUtil.isEmpty(scmNumberGeneration.get(0).getNumberValue())) {
ScmNumberGeneration scmNumberGeneration1 = new ScmNumberGeneration();
scmNumberGeneration1.setKey(key);
scmNumberGeneration1.setNumberValue(1);
try {
scmNumberGenerationRepository.save(scmNumberGeneration1);
return 1;
} catch (ObjectOptimisticLockingFailureException e) {
return retryFindNumberByKeyAndUpdateNumberByKey(key, retryCount);
}
} else {
ScmNumberGeneration scmNumberGeneration1 = scmNumberGeneration.get(0);
Integer num = scmNumberGeneration1.getNumberValue() + 1;
scmNumberGeneration1.setNumberValue(num);
try {
scmNumberGenerationRepository.save(scmNumberGeneration1);
return num;
} catch (ObjectOptimisticLockingFailureException e) {
return retryFindNumberByKeyAndUpdateNumberByKey(key, retryCount);
}
}
}

文章评论