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

Spring Boot读写实体时H2数据库连接耗尽致程序挂起问题

我在Java Spring/Hibernate使用方面经验相对不足,该问题解决难度不高,但我已花费数小时检索相关错误方案仍未定位根因。当前程序整体配置如下:采用本地存储的H2数据库,引入spring-boot-starter、spring-boot-security依赖。

问题描述

我正在开发在线Wiki系统,前期开发过程未遇到阻碍,已实现WikiPage实体的保存、更新、删除功能。但测试时发现,对WikiPage执行约5次读/保存/更新/删除操作后,应用无法与本地存储的H2服务端通信,程序挂起,所有网页均无法访问。我此前开发的项目使用了结构高度相似的DAO层,从未出现过同类问题。
我怀疑DAO层与数据库的交互逻辑存在缺陷,以下是相关配置和代码,已开启控制台DEBUG级日志输出。

Application Properties 配置
spring.datasource.url = jdbc:h2:tcp://localhost/~/Downloads/Data/Database
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=none
spring.h2.console.enabled=true

server.error.include-message=always

server.port=80

logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate=ERROR
WikiPage 实体类代码
package com.costi.csw9.Model;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;

@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@Entity
public class WikiPage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;
    @Column(nullable = false, unique = true)
    private String title;
    @Column(nullable = false)
    private LocalDateTime lastEdited;
    @Column(nullable = false)
    private String subtitle;

    private boolean enabled = false;
    private String category;
    @Column(columnDefinition="text")
    private String body;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User author;

    public WikiPage(String title, String subtitle, User author, String category, String body) {
        this.title = title;
        this.subtitle = subtitle;
        this.author = author;
        this.category = category;
        this.body = body;
    }

    public WikiPage(User author){
        this.author = author;
    }

}
WikiDaoImpl 实现类代码
package com.costi.csw9.Repository;

import com.costi.csw9.Model.User;
import com.costi.csw9.Model.UserRole;
import com.costi.csw9.Model.WikiCategory;
import com.costi.csw9.Model.WikiPage;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.hibernate.query.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.time.LocalDateTime;
import java.util.List;

@Repository
public class WikiDaoImpl implements WikiRepository{
    @Autowired
    private SessionFactory sessionFactory;

    @Override
    public WikiPage findById(Long id) {
        Session session = sessionFactory.openSession();
        WikiPage wikiPage = session.get(WikiPage.class, id);
        session.close();
        return wikiPage;
    }

    @Override
    public List<WikiPage> findByCategory(WikiCategory category) {
        Session session = sessionFactory.openSession();

        CriteriaBuilder cb = session.getCriteriaBuilder();
        CriteriaQuery<WikiPage> cr = cb.createQuery(WikiPage.class);
        Root<WikiPage> root = cr.from(WikiPage.class);
        cr.select(root);

        cr.select(root).where(cb.like(root.get("category"), category.name()));

        Query<WikiPage> query = session.createQuery(cr);
        List<WikiPage> results = query.getResultList();
        
        return results;
    }

    @Override
    public List<WikiPage> getByApproval(boolean enabled) {
        Session session = sessionFactory.openSession();

        CriteriaBuilder cb = session.getCriteriaBuilder();
        CriteriaQuery<WikiPage> cr = cb.createQuery(WikiPage.class);
        Root<WikiPage> root = cr.from(WikiPage.class);
        cr.select(root);

        if(enabled){
            cr.select(root).where(cb.isTrue(root.get("enabled")));
        }else{
            cr.select(root).where(cb.isFalse(root.get("enabled")));
        }

        Query<WikiPage> query = session.createQuery(cr);
        List<WikiPage> results = query.getResultList();

        return results;
    }

    @Override
    public List<WikiPage> findAll() {
        Session session = sessionFactory.openSession();
        List<WikiPage> all = session.createCriteria(WikiPage.class).list();
        session.close();
        return all;
    }

    @Override
    public void save(WikiPage wikiPage) {
        wikiPage.setLastEdited(LocalDateTime.now());
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        session.saveOrUpdate(wikiPage);
        session.getTransaction().commit();
        session.close();
    }

    @Override
    public void delete(WikiPage wikiPage) {
        Session session = sessionFactory.openSession();
        WikiPage page = session.get(WikiPage.class, wikiPage.getId());
        session.beginTransaction();
        session.delete(page);
        session.getTransaction().commit();
        session.close();
    }
}
错误日志
2022-06-28 20:35:22.933 DEBUG 10820 --- [p-nio-80-exec-4] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2022-06-28 20:35:22.934 DEBUG 10820 --- [p-nio-80-exec-4] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
2022-06-28 20:35:22.935 DEBUG 10820 --- [p-nio-80-exec-4] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, text/html;q=0.8]
2022-06-28 20:35:22.935 DEBUG 10820 --- [p-nio-80-exec-4] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500
根因定位

故障由数据库连接泄漏直接导致:

  • 检查DAO层代码可以发现,findByCategory、getByApproval两个方法每次调用都会通过sessionFactory.openSession()打开新的Hibernate Session(占用一个数据库连接),但拿到查询结果后直接返回,没有调用session.close()释放连接资源
  • H2默认连接池的最大连接数通常为5-10,连续调用几次这两个方法后,连接池的所有连接都会被未关闭的Session占满,后续所有数据库操作都无法获取可用连接,请求直接阻塞挂死,最终导致整个应用无法响应
  • 其余findById、findAll、save、delete方法都手动写了session.close(),唯独这两个查询方法遗漏关闭逻辑,和“执行约5次操作后挂起”的现象完全吻合。
  • 现有代码里即使是写了关闭逻辑的方法,也存在风险:如果事务提交、查询过程抛出异常,程序会直接跳出,session.close()不会执行,同样会造成连接泄漏。
修复方案

快速修复(基于现有代码改造)

给所有手动打开Session的方法加上try-finally块,保证无论方法执行是否抛异常,Session都能被正常关闭。以findByCategory为例:

@Override
public List<WikiPage> findByCategory(WikiCategory category) {
    Session session = sessionFactory.openSession();
    try {
        CriteriaBuilder cb = session.getCriteriaBuilder();
        CriteriaQuery<WikiPage> cr = cb.createQuery(WikiPage.class);
        Root<WikiPage> root = cr.from(WikiPage.class);
        // 原代码重复调用了cr.select(root),属于冗余逻辑可删除
        cr.select(root).where(cb.like(root.get("category"), category.name()));
        Query<WikiPage> query = session.createQuery(cr);
        return query.getResultList();
    } finally {
        session.close();
    }
}

getByApproval以及其余四个方法都按相同逻辑改造,把业务逻辑放在try块中,finally块中统一执行session.close()。
另外注意findByCategory中使用cb.like但没有传入通配符%,实际执行效果是精确匹配,如果需要模糊匹配要手动拼接通配符,如果是精确匹配直接替换为cb.equal即可。

长期优化方案(符合Spring Boot最佳实践)

手动管理Session和事务的写法极易出现连接泄漏,不推荐在Spring Boot项目中使用:

  • 直接让Repository接口继承JpaRepository<WikiPage, Long>,内置的CRUD方法、派生查询方法都由Spring自动管理连接和事务,不会出现资源泄漏问题
  • 移除手动注入的SessionFactory,自定义查询优先使用JPQL、Criteria API或Spring Data JPA的Specification实现,事务操作统一使用@Transactional注解声明,由Spring托管事务生命周期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 16:12:17