You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确编写带事务支持的Quartz调度器集成测试

问题根因分析
  • 事务隔离级别导致的可见性问题:你配置的REPEATABLE_READ隔离级别下,Quartz任务线程的事务如果在测试代码保存Event的事务提交前就启动,会基于事务启动时的快照读数据,看不到后续提交的Event记录,因此只会生成1条甚至0条Task。
  • 固定等待逻辑不可靠:Thread.sleep只能固定等待时长,无法保证调度任务刚好在等待周期内完成全量执行,也无法保证Event的事务提交对任务线程可见。
  • enable_lazy_load_no_trans属于高危临时配置:该配置会为每个懒加载操作创建临时会话,本身就极易引发数据不一致、会话泄漏等隐性问题,不适合在测试或生产环境使用。
修复方案

1. 替换不可靠的固定等待逻辑

使用条件等待工具Awaitility替代Thread.sleep,只有当符合预期的前置条件满足后才执行断言,避免提前读取未生成完成的Task数据:

<!-- 引入Awaitility依赖,版本可自行适配SpringBoot版本 -->
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <scope>test</scope>
</dependency>

修改测试代码中的等待逻辑:

import static org.awaitility.Awaitility.await;
import java.time.Duration;

@SneakyThrows
@Test
public void taskGenerationBeanTest() {
    RequestData request = requestBuilder(str(), str(), str()).build();
    String orderId = String.valueOf(UUID.randomUUID());
    Event event1 = getEvent("type1", request, orderId);
    Event event2 = getEvent("type2", request, orderId);

    eventRepository.save(event1);
    eventRepository.save(event2);

    // 等待最多30s,每500ms轮询一次,直到Task数量符合预期
    await().atMost(Duration.ofSeconds(30))
            .pollInterval(Duration.ofMillis(500))
            .until(() -> taskRepository.count() >= 2);
    List<Task> actual = taskRepository.findAll();

    assertAll(
            () -> assertEquals(2, actual.size()),
            () -> assertTrue(actual.stream().anyMatch(t ->
                    "type1".equals(t.getEvent().getEventType()))),
            () -> assertEquals(event1.getOrderId(), actual.get(0).getEvent().getOrderId())
    );
}

2. 解决事务可见性问题

如果业务没有强制要求REPEATABLE_READ隔离级别,可将任务方法的事务隔离级别调整为READ_COMMITTED,保证每次都能读取到已提交的最新数据:

@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public int createAndSaveTasks(EventType eventType) {
    // 原有逻辑保持不变
    List<Task> tasks = eventService.getEvent(eventType)
            .stream()
            .map(Task::new)
            .collect(Collectors.toList());
    taskRepository.saveAll(tasks);
    return tasks.size();
}

如果必须保留REPEATABLE_READ隔离级别,可在读取Event前手动刷新、清空持久化上下文,强制读取最新数据:

@PersistenceContext
private EntityManager entityManager;

@Override
@Transactional(isolation = Isolation.REPEATABLE_READ)
public int createAndSaveTasks(EventType eventType) {
    entityManager.flush();
    entityManager.clear();
    List<Task> tasks = eventService.getEvent(eventType)
            .stream()
            .map(Task::new)
            .collect(Collectors.toList());
    taskRepository.saveAll(tasks);
    return tasks.size();
}

3. 正确解决懒加载异常

移除spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true配置,通过关联查询一次性加载关联属性,避免无会话场景下的懒加载问题:

// 在TaskRepository中重写findAll方法,添加EntityGraph注解关联加载Event
@EntityGraph(attributePaths = "event")
List<Task> findAll();

4. 可选优化:提高测试确定性

如果不需要验证Quartz cron触发器本身的调度逻辑,可直接在测试中手动调用createAndSaveTasks方法两次(对应type1、type2两种事件类型),完全避免调度延迟、事务提交时序带来的不稳定问题。

内容的提问来源于stack exchange,提问作者petrov.aleksandr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 06:24:05