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

Spring+Mybatis环境下DAO层多次异步调用后返回空数据集求助

问题描述

我遇到一个严重问题:DAO层在被调用几次后就不再返回记录。我使用的是Spring Framework 5.3.10,涉及的核心组件如下:

  • Spring MVC 基于 HikariCP 5.0.0 的连接池
  • JDBC 驱动 Jaybird 4.0.3(对应 Firebird 3.0.7 数据库服务端)
  • ThreadPoolExecutor(使用默认配置)
  • Spring 事务管理
  • Mybatis

我有一个Spring控制器(A),每隔2-3秒就异步调用Spring服务(B)的方法,每次调用传入不同参数,该方法被@Async注解标记。DAO层(C)被定义为Spring服务。Spring服务(B)的工作方法每次运行初期会调用DAO方法,根据传入的参数从数据库表中获取对应数据集;在工作方法执行结束时,会更新入参对应的行(不会修改入参对应的字段)。Spring服务(B)的方法处理数据耗时较长,约10-15秒。

在控制器(A)发起第3到4次调用后,DAO方法就会返回空结果集。如果放慢服务(B)的调用速度,等待上一次调用完成后再发起下一次,所有功能都运行正常。

调整事务隔离级别完全没有效果。

我已经尝试解决这个问题好几天了,但毫无进展。如果有人能指明解决方向我会非常感激。使用互斥锁或信号量这类方案只是规避问题,没有从根源解决。

架构示意图

Controller A <---------
    |                  |
    |                  | 每2-3秒重复调用
Service B              |
worker method          |
执行耗时15 - 20秒 --------

调用DAO方法 getData(token)
    |
执行业务逻辑
    |
调用DAO方法 updateData(token)

控制器(A)代码

@Controller
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestService testService;
    ...
    ...

    @GetMapping(value="/RunWorker")
    public String runWorker(ModelMap map, HttpServletRequest hsr)  {
        ...
        testService.workerMethod(token);
        ...
    }
}

服务(B)代码

public interface TestService {
    public void workerMethod(long token);
}

@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private TestDAO testDao;

    @Override
    public void workerMethod(long token) {
        List<MyData> myDataSet = testDao.getData(token);
        ...
        // 长耗时业务逻辑
        ...
        testDao.updateData(token);
    }
}

DAO(C)代码

public interface TestDAO {
    public List<MyData> getData(long token);
    public void updateData(long token);
}

@Service
public class TestDAOImpl implements TestDAO {

    @Autowired
    private TestMapper testMapper;  // 使用Mybatis mapper

    public List<MyData> getData(long token) {
        return testMapper.getData(token);
    }

    public void updateData(long token) {
        testMapper.updateData(token);
    }
}

Mapper类(D)代码

public interface TestMapper {
    @Select("SELECT * FROM TESTTABLE WHERE TOKEN=#{token}")
    public List<MyData> getData(@Param("token") long token);

    @Update("UPDATE TESTTABLE SET STATUS=9 WHERE TOKEN=#{token}")
    public void updateData(@Param("token") long token);
}

注:原代码中Update注解的SQL字符串缺少闭合引号,属于贴代码时的笔误

我已经试过@M. Deinum提出的使用@Repository的建议,但该方案并没有解决问题。

我将Spring服务(B)改为prototype作用域的Bean,通过@Lookup注入,运行表现依然没有变化,第二次调用后DAO的getData方法就返回空结果集,这个问题非常令人费解和受挫。


排查与解决方案

1. 长事务占用连接池是最高概率根因

你当前的workerMethod执行耗时10-15秒,如果该方法或者全局事务配置会让整个方法被事务包裹,会导致数据库连接被事务持有整整15秒不释放。HikariCP默认最大连接数为10,每2-3秒发起一次请求的情况下,4次调用就会占满4个连接,15秒内累计触发5-7次调用很快就会打满连接池。部分驱动和框架在连接等待超时后会静默返回空集合而不是抛出异常,完全符合你遇到的现象。

修复步骤:

给DAO的读写方法单独加@Transactional注解,不要给整个workerMethod加事务,确保数据库连接在查询/更新执行完成后立即释放,不要被长耗时的业务逻辑占用:

@Repository
public class TestDAOImpl implements TestDAO {
    @Autowired
    private TestMapper testMapper;

    @Transactional(readOnly = true)
    public List<MyData> getData(long token) {
        return testMapper.getData(token);
    }

    @Transactional
    public void updateData(long token) {
        testMapper.updateData(token);
    }
}

2. 替换默认@Async线程池

Spring默认的@Async线程池实现是SimpleAsyncTaskExecutor,不会复用线程,每次调用都会新建线程,高频率调用下会导致线程数爆炸,进一步加剧连接占用问题。

修复步骤:

自定义线程池注入Spring上下文,指定合理的线程参数:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("AsyncWorker-");
        executor.initialize();
        return executor;
    }
}

3. 适配Firebird数据库特性

Jaybird驱动和Firebird 3.x的MVCC机制存在特殊行为:如果长事务没有提交,后续的查询可能会拿到过期的快照,或者因为事务版本冲突导致查询不到数据。同时Mybatis默认的SqlSession级缓存也可能导致跨请求拿到旧结果。

修复步骤:

  1. 在JDBC连接串中添加自动提交参数,确保非事务查询执行后自动提交:
spring.datasource.url=jdbc:firebirdsql://localhost:3050/你的数据库路径?autoCommit=true
  1. 关闭Mybatis一级缓存,避免跨请求复用SqlSession时拿到旧缓存:
mybatis.configuration.local-cache-scope=STATEMENT

4. 兜底定位方案

如果上述方案都没有解决,开启以下日志打印,确认查询参数、执行SQL、返回结果是否符合预期,直接定位问题出在数据库层还是框架层:

logging.level.org.mybatis=DEBUG
logging.level.java.sql=DEBUG

内容的提问来源于stack exchange,提问作者Deadpete

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 15:09:04