请看下列代码,你能发现有什么问题吗?
public List<YearConf> queryYearConf(Integer year) throws Exception {
//1.查询该年是否存在配置数据,如果无数据则初始化
if (yearConfDao.queryIntegerWithSql(new SQL().select("count(id)").from(YearConf.class).where(YearConf::getYear, year)) == 0) {
List<YearConf> yearConfs = new ArrayList<>();
Date now = new Date();
String loginUser = SubjectUtils.loginUserId();
BizConstants.YEAR_CONF_MAP.forEach((s, s2) -> {
YearConf yearConf = new YearConf();
yearConf.setId(IdUtil.fastSimpleUUID());
yearConf.setCompany(s);
yearConf.setYear(year);
yearConf.setCreateTime(now);
yearConf.setUpdateTime(now);
yearConf.setCreateUser(loginUser);
yearConf.setUpdateUser(loginUser);
yearConfs.add(yearConf);
});
//2.初始化该年的配置数据
yearConfDao.saveAll(yearConfs);
}
//3.查询数据
SQL sql = new SQL().select("`year`,company,budgetGoal,jan,feb,mar,apr,may,june,july,aug,sept,oct,nov,dece").from(YearConf.class).where(YearConf::getYear, year);
return yearConfDao.queryWithSql(YearConf.class, sql).queryList();
}
正如文章标题所示,上面的代码存在着并发问题。
请想象一下: 假设两个线程A、B同时进入queryYearConf方法且year参数一样,如果线程A在执行yearConfDao.savell(yearConfs)方法之前,此时线程B进行yearConfDao.queryIntegerWithSqlxxx进行查询,那么线程B由于查询不到该年份数据,同样也会再次对同样年份配置数据进行一次初始化保存,这与代码要求实现的业务逻辑相悖。
总结一下,如果你在项目里写过类似的代码,模板如下,那么这些代码将会存在着并发问题!
-
- 先查询初始化数据是否存在
-
- 不存在则初始化并保存
-
- 查询数据
public List<xxx> queryAndInit(Integer param){
//1.查询param参数对应的数据是否存在,不存在,则初始化
if(xxxDao.countByParam(param)==0){
List<xxx> xxxs = new ArrayList();
//2.初始化xxxs
initXxxs();
//3.保存xxxs
xxxDao.saveAll(xxxs);
}
//4.查询数据
return xxxDao.query(param);
}
public List<xxx> queryAndInit(){
//1.查询param参数对应的数据是否存在,不存在,则初始化
if(xxxDao.count()==0){
List<xxx> xxxs = new ArrayList();
//2.初始化xxxs
initXxxs();
//3.保存xxxs
xxxDao.saveAll(xxxs);
}
//4.查询数据
return xxxDao.query();
}
如何解决?
1.代码块同步加锁,注意锁的粒度,
//只需对参数对象加锁即可
public List<xxx> queryAndInit(Integer param) {
synchronized ((param + "").intern()) {
//1.查询param参数对应的数据是否存在,不存在,则初始化
if (xxxDao.countByParam(param) == 0) {
List<xxx> xxxs = new ArrayList();
//2.初始化xxxs
initXxxs();
//3.保存xxxs
xxxDao.saveAll(xxxs);
}
//4.查询数据
return xxxDao.query(param);
}
}
//没有参数则对这个方法加锁
public synchronized List<xxx> queryAndInit(){
//1.查询param参数对应的数据是否存在,不存在,则初始化
if(xxxDao.count()==0){
List<xxx> xxxs = new ArrayList();
//2.初始化xxxs
initXxxs();
//3.保存xxxs
xxxDao.saveAll(xxxs);
}
//4.查询数据
return xxxDao.query();
}
2.数据库设置唯一索引
通过字段唯一索引进行约束,这种方式的好处是无需调整代码,通过数据库的fast-fail机制,后进入方法的线程会提示唯一约束错误
注意事项
本篇幅所演示示例,在各个方法上未添加@Transaction注解保证开启事务,如果你的查询并初始化方法上开启了事务,那么synchronized锁可能会导致事务失效。
解决方案一:将synchronized加到调用queryAndInit方法的controller
@PostMapping("/queryAndInit")
public List<xxx> queryAndInit() throws Exception {
try {
synchronized (lock) {
return shangyuMajorService.queryAndInit();
}
} catch (Exception e) {
logger.error("queryShangyuMajor error", e);
throw e;
}
}
解决方案二:
public synchronized List<xxx> queryAndInit(){
return ((demo方法所在的类名) AopContext.currentProxy()).queryAndInit();
}
public List<xxx> queryAndInit(){
//1.查询param参数对应的数据是否存在,不存在,则初始化
if(xxxDao.count()==0){
List<xxx> xxxs = new ArrayList();
//2.初始化xxxs
initXxxs();
//3.保存xxxs
xxxDao.saveAll(xxxs);
}
//4.查询数据
return xxxDao.query();
}
各位读者还有没有更好的方式?