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
相关产品推荐
相关产品推荐

