每天一个BUG--记一个事务注解和异步线程导致数据异常的故事

❓背景

前阵子从同事手里接过一个项目。随后项目经理反馈一个BUG,说是一个导入功能有问题。

我们假设这个功能需要导入一个excel文件,里边保存了业务主体。包含的内容大概就是保存一个任务信息报名了这个任务的人员信息。通过导入保存这个任务和报名任务的人员,任务本身有一个分数,完成任务就可以获得这个分数,但是导入的时候是可以手动调整某一个报名人员的分数的。

BUG的表现就是导入完应该展示导入人员的自定义分数,偏偏导入完了所有报名人员的分数都是任务本身的分数。好比任务名叫"坚持七点起床打卡一周"完成这个任务获得2分,张三导入时老师觉得他表现好,给他填了个9分,但是导入之后张三和其他人一样都是2分,张三郁闷不已=-=。

其实如果这个问题是单纯的没有把报名人员的分数正确赋值也简单了,去看看字段调用啥的应该就可以了。嘿嘿,等我去测试环境实测以后发现,当项目重启后,第一次导入时,一切正常,张三获得了他的定制分数9分,再导入一次,嘿,成2分了,随后进行了几次测试情况也都差不多,基本上都是第一次成功,之后就不行了(其实有些时候是前两次成功,有些时候是1、3次成功,厉害吧、enmmm后来问了下,测试环境后端是两个节点,也就是差不多每个节点的第一次请求都是正常的)。

这段背景好像写的有点长了,反正大概是这么个情况,由于我也头一次看这个项目的代码,业务也不熟,就只能从接口一点一点捋了

less 复制代码
@Slf4j
@Service
public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> implements ProjectService {


    @Override
    @Transactional
    public Map<Integer,String> projectImport(String filePath) {
        //接口从这里开始!!!
        //首先任务信息和报名的人员信息是存在了两个sheet中,所以他先对任务信息进行了处理

        ImportParams params = new ImportParams();
        params.setTitleRows(0);
        params.setHeadRows(1);
        params.setSheetNum(1);
        List<ProjectImportParam> list = null;
        try {
            list = ExcelImportUtil.importExcel(new File(filePath), ProjectImportParam.class, params);
        } catch (Exception e) {
            log.error(">>> 导入数据异常:{}", e.getMessage());
        }

        //1.校验任务信息的相关字段
        ......
        //2.处理一些关联表数据,进行任务主体的信息保存--把任务数据存库里了
        this.save(project);

        //3.然后准备去处理报名的人员数据
        signService.projectSignImport(projectNoMap,filePath);
        for (Project project : finishProjectList) {
        //4.这里是去把报名人员获得的分数由任务自带的分数更新为自定义的--问题触发点在3和4
            this.awardToStudent(project.getProjectNo(),null);
        }
        return projectNoMap;
        //到这里导入就完事了

    }
}

上边这一段是直接从项目里摘出来的,省略了些其他的,下边让我们来看下signService.projectSignImport(projectNoMap,filePath);

typescript 复制代码
@Slf4j
@Service
public class SignServiceImpl extends ServiceImpl<SignMapper, Sign> implements SignService {

    
    @Override
    public void projectSignImport(Map<Integer, String> projectNoMap, String filePath) {
        //这个方法都是去检验报名人员的信息还有检验下这些人是不是存在任务冲突啥的
        //相关的是下边这个方法
        this.userImport(projectNo2StudentParamList.get(projectNo), project, phone, isNotComplete,null);

    }
   
    /*
     * 导入报名记录
     * */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void userImport(List<StudentParam> studentList, Project project, String importUserPhone, boolean needSendMessage,Integer overProject) {
        //在这个方法里才是去组装报名人员的对象信息,为了显得字数多,粘贴点
        // 1.添加子项目报名记录,后续子活动自动新增
        List<SignDetail> list = new ArrayList<>(projectDetailIds.size());
        for (Long projectDetailId : projectDetailIds) {
            SignDetail signDetail = new SignDetail();
            ......
            list.add(signDetail);
        }
        try {
            signDetailService.saveBatch(list);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new GpowerException("批量导入数据异常,请检查文件导入信息!");
        }
        ......
        
        // 2.同步项目报名
        Sign sign = new Sign();
        ......
        //关键点是这个实体对象Sign
        this.save(sign);
        ......
    }
}

所以最后是this.userImport()这个方法中对报名信息Sign类进行了保存,嗯Sign类是重点,然后让我们回到上边第四点要执行的方法

this.awardToStudent(project.getProjectNo(),null);

在这个方法中也是去计算报名人员的一些数据,他是分了两次给人员分数赋值,先把任务本身的值给更新了上去,然后再去更新报名人员自定义的分数,因为分数的更新需要有些联动操作(其实也是没啥脑子去捋这业务为什么要分好几次去处理分数=-=),让我们接着看

ini 复制代码
private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 100, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20));

@Transactional(rollbackFor = Exception.class)
@Override
public void awardToStudent(String projectNo, Long awardStudentId) {
    //进行了一通操作,各种map,list的最后只把任务自带的分数更新给了报名人员=-=
    //啰嗦了半天重点来了,你看下边写了这个方法执行时间长了,为了页面快速响应所以用了一个异步的方式去修改,而且还是用的线程池
    
    // 导入项目及其报名人的时候有一个时长修改,这个知道时间可能比较长,所以开一个异步吧--这个是前人加的
    threadPool.execute(() -> {
        if (projectDetailId1 == null) {
            log.error("导入项目及其报名人的项目无子项目id:" + projectDetailId1);
            return;
        }
        LambdaQueryWrapper<Sign> signLambdaQueryWrapper = new LambdaQueryWrapper<>();
        signLambdaQueryWrapper.eq(Sign::getProjectNo, projectNo);
        signLambdaQueryWrapper.isNotNull(Sign::getDuration);
        signLambdaQueryWrapper.ne(Sign::getSignType, SignTypeEnum.USER_DELETE.getCode());
        if (ObjectUtil.isNotNull(awardStudentId)) {
            signLambdaQueryWrapper.eq(Sign::getStudentId, awardStudentId);
        }
        List<Sign> signList = signService.list(signLambdaQueryWrapper);
        if (CollUtil.isNotEmpty(signList)) {
            signList.forEach(sign -> {
            //1.这里这里这里,下边这个方法是去更新正确的分数,就是把自定义分数更新给报名人,张三的9分就在这里被更新(当然肯定也包含其他操作)具体的咱不看了
                laborHoursService.changeStudentHours(projectDetailId1.toString(), sign.getStudentId().toString(), sign.getDuration(), "导入项目及其报名人员时修改");
            });
        }
    });

    
}

经过上边的代码我们知道Sign类是一个关键点,在awardToStudent()方法异步处理的时候,他先去查了任务的报名人员信息集合signList,当signList不等于空的时候进行分数的更新。

最开始吧我肯定没一行一行读代码,我就大概看了看字段赋值了,Sign对象也都新增了,咋就一会好使一会不好使了=-=。我基本没怀疑过signList是空的,我觉得它不可能是空,因为上边的代码有写保存,都执行到这了,指定是都保存了,到底是为啥导致下边laborHoursService.changeStudentHours()方法没正确执行,或者说没执行呢,啊是有时执行有时不执行,各位看到这的好心人请踊跃发言吧

+1+1+1

相关推荐
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天6 小时前
java的threadlocal为何内存泄漏
java
caridle6 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^7 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋37 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx