易被忽视的出现并发问题的代码

82 阅读3分钟

请看下列代码,你能发现有什么问题吗?

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由于查询不到该年份数据,同样也会再次对同样年份配置数据进行一次初始化保存,这与代码要求实现的业务逻辑相悖。

总结一下,如果你在项目里写过类似的代码,模板如下,那么这些代码将会存在着并发问题!

    1. 先查询初始化数据是否存在
    1. 不存在则初始化并保存
    1. 查询数据
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锁可能会导致事务失效。

2536235-0008cc9304c0d133.webp

解决方案一:将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();
}

各位读者还有没有更好的方式?