上一篇帖子在这--># 每天一个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);
改完了,收工
(重构的话,介于当时的情况不太支持,要的比较急,容我后续在慢慢看)
其实之前也没写过这种类似分享的帖子,头一次,有些地方说的比较啰嗦,后续努力学习,争取改正,各位客官点个赞支持下呀