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

Spring Boot @Transactional(readOnly=true)在AWS RDS+HikariCP下未释放连接

问题描述

在AWS RDS环境下,使用Spring Boot 3.4.3、MySQL 8.0、Hibernate 6.6.8.Final,通过JPA+QueryDSL实现CRUD操作。所有功能正常,但给只读查询方法添加@Transactional(readOnly = true)注解后,数据库连接未被归还,持续处于sleep状态,且不断创建新连接,最终可能导致连接池耗尽。

服务层、Repository层、Controller层代码如下:

服务层代码

@Transactional(readOnly = true)
public Page<BarCode> getAllActiveBarcodes(Pageable pageable, Boolean isAction, String keyword, String searchType, String type) throws IllegalArgumentException {
    return barCodeRepository.searchBarcodes(pageable, isAction, keyword, searchType, type);
}

@Transactional(readOnly = true)
public List<BarCode> barCodeSearch(String keyword) throws IllegalArgumentException {
    return barCodeRepository.searchKeyword(keyword);
}

@Transactional(readOnly = true)
public List<BarCode> barCodeList(){
    return barCodeRepository.findAll();
}

Repository层实现代码

@Override
public List<BarCode> searchKeyword(String keyword) {
    BooleanBuilder builder = new BooleanBuilder();
    builder.and(barCode.isActive.isTrue());
    builder.and(barCode.deleteDate.isNull());

    if (keyword != null && !keyword.trim().isEmpty()) {
        BooleanBuilder keywordBuilder = new BooleanBuilder();
        keywordBuilder.or(barCode.barcode.containsIgnoreCase(keyword));
        keywordBuilder.or(barCode.type.containsIgnoreCase(keyword));
        keywordBuilder.or(barCode.message.containsIgnoreCase(keyword));
        keywordBuilder.or(barCode.descText.containsIgnoreCase(keyword));
        keywordBuilder.or(barCode.createUser.containsIgnoreCase(keyword));
        builder.and(keywordBuilder);
    }

    return queryFactory
            .selectFrom(barCode)
            .where(builder)
            .orderBy(barCode.createDate.desc())
            .fetch();
} 
@Override
public Page<BarCode> searchBarcodes(Pageable pageable, Boolean isAction, String keyword, String searchType, String type) {
    BooleanBuilder builder = new BooleanBuilder();

    if (isAction != null) {
        builder.and(barCode.isActive.eq(isAction));
    }
    if(type != null && !type.trim().isEmpty()) {
        builder.and(barCode.type.eq(type));
    }

    if (keyword != null && !keyword.trim().isEmpty()) {
        switch (searchType.toLowerCase()) {
            case "barcode" -> builder.and(barCode.barcode.containsIgnoreCase(keyword));
            case "message" -> builder.and(barCode.message.containsIgnoreCase(keyword));
            case "desc_text" -> builder.and(barCode.descText.containsIgnoreCase(keyword));
            case "create_user" -> builder.and(barCode.createUser.containsIgnoreCase(keyword));
            default -> throw new IllegalArgumentException("잘못된 검색 유형입니다.");
        }
    }

    long total = Optional.ofNullable(
            queryFactory
                    .select(barCode.count())
                    .from(barCode)
                    .where(builder)
                    .fetchOne()).orElse(0L);


    if (total == 0) {
        return Page.empty(pageable);
    }

    List<BarCode> result = queryFactory
            .selectFrom(barCode)
            .where(builder)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(barCode.createDate.desc())
            .fetch();

    return new PageImpl<>(result, pageable, total);
}

Controller层代码

@GetMapping("/barcodes")
public ResponseEntity<Page<BarCode>> getAllBarcodes(@PageableDefault(page = 0, size = 32) Pageable pageable,
                                                    @RequestParam(value = "isAction", required = false) Boolean isAction,
                                                    @RequestParam("type") String type,
                                                    @RequestParam("searchType") String searchType,
                                                    @RequestParam("keyword") String keyword)  {

    try {
        Page<BarCode> barcodeList = barcodeService.getAllActiveBarcodes(pageable, isAction, keyword, searchType, type);

        if(barcodeList.isEmpty()){
            return ResponseEntity.noContent().build();
        }

        return ResponseEntity.ok(barcodeList);
    } catch (IllegalStateException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

@GetMapping("/barcodes/all")
public ResponseEntity<List<BarCode>> getBarcode() throws IllegalArgumentException {
    try {
        List<BarCode> barCodeList = barcodeService.barCodeList();
        return ResponseEntity.ok(barCodeList);
    }catch (IllegalStateException e){
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }catch (Exception e){
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}


@GetMapping("/barcodes/active")
public ResponseEntity<List<BarCode>> getActiveBarcode(@RequestParam(required = false) String keyword) throws IllegalArgumentException {

    try{
        List<BarCode> search = barcodeService.barCodeSearch(keyword);
        return ResponseEntity.ok(search);
    }catch (IllegalStateException e){
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }catch (Exception e){
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

原因分析与解决方案

1. QueryFactory未绑定到事务内的EntityManager

QueryDSL的JPAQueryFactory如果是通过非Spring代理的EntityManager创建(比如直接从EntityManagerFactory手动创建),会导致查询使用独立的连接,不受Spring事务管理器管控,事务结束后连接无法自动归还。

解决方法:

确保Repository中注入Spring管理的EntityManager,并用它初始化JPAQueryFactory:

@Repository
public class BarCodeRepositoryImpl implements BarCodeRepositoryCustom {

    private final EntityManager entityManager;
    private JPAQueryFactory queryFactory;

    // 注入Spring代理的EntityManager
    @Autowired
    public BarCodeRepositoryImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @PostConstruct
    public void init() {
        // 使用当前事务的EntityManager创建QueryFactory
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    // 后续查询逻辑不变
}

2. 连接池参数配置不合理

默认的连接池参数(比如HikariCP)可能允许空闲连接长时间存活,加上AWS RDS的连接超时机制,会导致sleep连接堆积,无法被及时回收。

解决方法:

在application.yml中调整HikariCP参数,适配AWS RDS环境:

spring:
  datasource:
    hikari:
      max-lifetime: 1800000          # 连接最大存活时间(30分钟,小于AWS RDS默认超时)
      idle-timeout: 600000           # 空闲连接回收时间(10分钟)
      maximum-pool-size: 20          # 连接池最大容量(根据业务并发调整)
      minimum-idle: 5                # 最小空闲连接数
      connection-timeout: 30000      # 获取连接超时时间(30秒)

3. Hibernate连接释放模式未配置

如果Hibernate没有在事务结束后及时释放连接,会导致连接被占用。

解决方法:

添加Hibernate连接释放配置:

spring:
  jpa:
    properties:
      hibernate:
        connection:
          release_mode: after_transaction  # 事务结束后立即释放连接

4. 事务边界验证

确认事务是否正确开启与关闭,可通过日志排查:

解决方法:

开启Spring事务与JPA日志,查看事务生命周期:

logging:
  level:
    org.springframework.transaction: DEBUG
    org.springframework.orm.jpa: DEBUG

查看日志中是否有Committing transaction或Rolling back transaction的记录,确认事务正常结束。


内容的提问来源于stack exchange,提问作者박아</think_never_used_51bce0c785ca2f68081bfa7d91973934>

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 09:23:10