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

上一篇帖子在这--># 每天一个BUG--记一个事务注解和异步线程导致数据异常的故事-上

虽然上个帖子也没啥人互动,但总归是有些人看到了,其实本来是写一篇的,昨天写到中间被拉去解决问题,解决完就快下班了,就偷个懒直接先发问题

再放一次昨天最后一段的代码

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

    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(), "导入项目及其报名人员时修改");
                });
            }
        });


    }
}

回想下昨天最后的关键信息,报名用户的自定义分数是在laborHoursService.changeStudentHours()方法里被操作的,那有问题的时候肯定就是这个方法没有被执行了(其实我还在这个方法里边加过日志打印,发现失败的时候确实是没执行,都不是执行报错的事了,就压根没执行)。

所以往上找找,唯一的判断条件就是signList这个集合对象了,所以肯定是这个对象是空的,有时空,有时不空,我确信在signService.userImport()里执行了对Sign对象的保存了,那应该都有值,中间也别的逻辑会删除这个对象,然后我想是不是保存的时候失败了,好像也不对,要是失败了,那应该就走不到后边了,那到底为啥有时候是空的,有时候不是

不啰嗦了,挺简单个问题,我叨叨了半天(虽然我当时看了挺久才发现的问题=-=)

问题关键就是标题里写的,事务注解和异步线程的问题,laborHoursService.changeStudentHours()方法和signList对象的查询都是在异步线程池里去执行的,虽然sign对象确实在之前就save了,但那是在signService.userImport()执行的,看看那个方法上边有个啥

有个@Transactional的注解哇,最开始其实没咋关注它=-=

加了事务之后可以保障在方法中的数据库操作会被包装在一个事务中。如果在方法执行期间发生了异常,整个事务将被回滚,以确保数据的一致性。@Transactional 注解的默认传播行为(Propagation)是 REQUIRED(如果当前存在事务,则加入该事务;否则,创建一个新的事务)。默认隔离级别(Isolation)是数据库的默认隔离级别,通常是 DEFAULT。所以在事务之外的方法是无法读取到还没提交事务中的数据的(事务相关传播行为和隔离级别就不展开说了)。让我们来看一个示例

kotlin 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyTransactionalService {

    @Autowired
    private MyRepository myRepository;

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void performTransactionalOperation() {
        // 事务开始

        // 从数据库中读取数据
        Data data = myRepository.findById(1L).orElse(null);
        System.out.println("Initial Data Value: " + data.getValue());

        // 在数据库中修改数据
        data.setValue("New Value");
        myRepository.save(data);

        // 模拟另一个事务修改同一行数据
        anotherTransaction();

        // 再次读取数据
        Data updatedData = myRepository.findById(1L).orElse(null);
        System.out.println("Updated Data Value: " + updatedData.getValue());

        // 事务结束
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void anotherTransaction() {
        // 在另一个事务中修改相同的数据
        Data data = myRepository.findById(1L).orElse(null);
        data.setValue("Another Transaction");
        myRepository.save(data);
    }
}

在这个代码中anotherTransaction() 方法的事务传播行为为 Propagation.REQUIRES_NEW,这样会让它在一个新的事务中执行,它和performTransactionalOperation()属于两个事务,在performTransactionalOperation()中修改的数据,在anotherTransaction()中是读不到的。

回到我们这个问题,异步线程中的处理情况和案例类似,其实也是读取不到事务方法里还没提交的数据,那为啥是有时候能读取到,有时候不能,我们看下图

在开启异步线程处理后,主方法已经处理完成,进行了return,提交了事务,当事务的提交 发生在异步线程中查询之前,那异步线程就能读到想要的数据,反之,就读不到了

这就是为啥重启后每个节点第一次请求都正常了,事务提的快啊,嘎嘎快,异步里的还没执行明白呢,外边就完事了。找到问题之后就开始改呗!

bash 复制代码
Thread.sleep(1500);

改完了,收工

(重构的话,介于当时的情况不太支持,要的比较急,容我后续在慢慢看)

其实之前也没写过这种类似分享的帖子,头一次,有些地方说的比较啰嗦,后续努力学习,争取改正,各位客官点个赞支持下呀

相关推荐
丁卯40418 分钟前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo19 分钟前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
李长渊哦21 分钟前
常用的 JVM 参数:配置与优化指南
java·jvm
计算机小白一个22 分钟前
蓝桥杯 Java B 组之设计 LRU 缓存
java·算法·蓝桥杯
南宫生3 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
计算机毕设定制辅导-无忧学长4 小时前
Maven 基础环境搭建与配置(一)
java·maven
bing_1584 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
天上掉下来个程小白4 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
风与沙的较量丶5 小时前
Java中的局部变量和成员变量在内存中的位置
java·开发语言
m0_748251725 小时前
SpringBoot3 升级介绍
java