压测场景下Hibernate出现ConcurrentModificationException的问题排查与解决方案咨询
我这边有个基于以下技术栈搭建的应用:
- HK2(依赖注入框架)
- Hibernate(ORM框架)
- GrizzlyHttpServer(Web服务器)
平时运行一切正常,但一进行压测就抛出了ConcurrentModificationException异常,麻烦帮忙分析下问题根源,还有怎么修复,比如配置连接池这类的方案?
应用核心代码展示
Hibernate配置类
import org.hibernate.*; import org.hibernate.boot.*; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.jvnet.hk2.annotations.Service; @Service public class HibernateOracle { private Session session; private SessionFactory factory; public HibernateOracle() { final StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure() // 从hibernate.cfg.xml加载配置 .build(); factory = new MetadataSources(registry).addPackage("com.art.entity").buildMetadata().buildSessionFactory(); session = factory.openSession(); } public Session getSession() { if (session == null || !session.isOpen()) session = factory.openSession(); return this.session; } }
Repository中的查询方法
@jakarta.inject.Inject private HibernateOracle hibernateOracle; @Override public User get(String username) throws UserNotFoundException { Session session = hibernateOracle.getSession(); try { return (User) session.createNamedQuery("User.findOneByUserName") .setParameter("username", username).getSingleResult(); } catch (Exception e) { e.printStackTrace(); throw new UserNotFoundException(); } finally { // 此处未做Session关闭操作 } }
压测时抛出的异常栈
java.util.ConcurrentModificationException at
java.base/java.util.HashMap.forEach(HashMap.java:1424) at
org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl.releaseResources(ResourceRegistryStandardImpl.java:328)
at
org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.afterTransaction(AbstractLogicalConnectionImplementor.java:60)
at
org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.afterTransaction(LogicalConnectionManagedImpl.java:167)
at
org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.afterTransaction(JdbcCoordinatorImpl.java:276)
at
org.hibernate.internal.SessionImpl.afterOperation(SessionImpl.java:550)
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1464) at
org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1649)
at
org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1617)
at
org.hibernate.query.internal.AbstractProducedQuery.getSingleResult(AbstractProducedQuery.java:1665)
at
com.art.vesal.backend.core.dao.impl.UserDaoImpl.get(UserDaoImpl.java:238)
at
com.art.vesal.backend.core.controller.customer_management.UserControllerImpl.get(UserControllerImpl.java:159)
at
com.art.vesal.backend.core.controller.customer_management.CustomerControllerImpl.checkCustomer(CustomerControllerImpl.java:598)
at
com.art.vesal.backend.core.controller.messaging.MTSMSControllerImpl.sendMessageManyToMany(MTSMSControllerImpl.java:282)
at
com.art.vesal.backend.core.controller.messaging.ArtMTSMSImpl.sendMessageManyToMany(ArtMTSMSImpl.java:140)
at
com.art.vesal.backend.core.controller.rest.MessageRelayRest.sendMessageManyToMany(MessageRelayRest.java:26)
at jdk.internal.reflect.GeneratedMethodAccessor35.invoke(Unknown
Source) at
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568) at
org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)
at
org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:124)
at
org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:167)
at
org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219)
at
org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)
at
org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)
at
org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)
at
org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
at
org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248) at
org.glassfish.jersey.internal.Errors$1.call(Errors.java:244) at
org.glassfish.jersey.internal.Errors.process(Errors.java:292) at
org.glassfish.jersey.internal.Errors.process(Errors.java:274) at
org.glassfish.jersey.internal.Errors.process(Errors.java:244) at
org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
at
org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)
at
org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:680)
at
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(GrizzlyHttpContainer.java:356)
at
org.glassfish.grizzly.http.server.HttpHandler$1.run(HttpHandler.java:190)
at
org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:535)
at
org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:515)
at java.base/java.lang.Thread.run(Thread.java:833)
## 问题根源分析 核心问题出在`HibernateOracle`类的设计上: - HK2的`@Service`默认是**单例模式**,整个应用只会创建一个`HibernateOracle`实例 - 这个单例里维护了一个全局共享的`Session`对象,压测时多个请求线程会同时操作这个Session - Hibernate的`Session`本身是**线程不安全**的,多线程并发访问会导致其内部状态(比如资源注册表的HashMap)被并发修改,从而触发`ConcurrentModificationException` 另外还有两个辅助问题: 1. Repository方法的finally块没有关闭Session,会导致数据库连接泄漏,压测时容易出现连接池耗尽的情况 2. 未配置Hibernate连接池,默认的连接管理方式无法应对高并发场景 ## 修复方案 ### 方案一:每次获取新的Session并手动关闭 修改`HibernateOracle`,去掉全局Session,每次调用`getSession()`都返回新的Session: ```java @Service public class HibernateOracle { private SessionFactory factory; public HibernateOracle() { final StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure() .build(); factory = new MetadataSources(registry).addPackage("com.art.entity").buildMetadata().buildSessionFactory(); } // 每次返回新的Session,由调用方负责关闭 public Session getSession() { return factory.openSession(); } }
然后修改Repository方法,在finally块中关闭Session:
@Override public User get(String username) throws UserNotFoundException { Session session = hibernateOracle.getSession(); try { return (User) session.createNamedQuery("User.findOneByUserName") .setParameter("username", username).getSingleResult(); } catch (Exception e) { e.printStackTrace(); throw new UserNotFoundException(); } finally { if (session != null && session.isOpen()) { session.close(); // 用完关闭Session,释放数据库连接 } } }
方案二:配置HikariCP连接池
在hibernate.cfg.xml中配置HikariCP(Hibernate 5.4+默认支持),让Hibernate自动管理连接池:
<!-- 启用HikariCP连接池 --> <property name="hibernate.connection.provider_class">org.hibernate.hikaricp.internal.HikariCPConnectionProvider</property> <!-- 连接池大小配置,根据压测需求调整 --> <property name="hibernate.hikari.maximumPoolSize">20</property> <property name="hibernate.hikari.minimumIdle">5</property> <!-- 连接超时设置 --> <property name="hibernate.hikari.idleTimeout">30000</property> <property name="hibernate.hikari.connectionTimeout">20000</property>
配置后Hibernate会自动复用连接,避免频繁创建销毁连接的开销,同时也能更好地应对高并发场景。
方案三:使用请求级Scope绑定Session
将HibernateOracle的Scope改为请求级,让每个请求拥有独立的Session,避免多线程冲突:
import org.glassfish.hk2.api.RequestScope; import org.jvnet.hk2.annotations.Service; import org.jvnet.hk2.annotations.Scope; import javax.annotation.PreDestroy; @Service @Scope(RequestScope.class) // 每个请求创建一个实例 public class HibernateOracle { private Session session; private SessionFactory factory; public HibernateOracle() { final StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure() .build(); factory = new MetadataSources(registry).addPackage("com.art.entity").buildMetadata().buildSessionFactory(); session = factory.openSession(); } public Session getSession() { return this.session; } // 请求结束时自动关闭Session @PreDestroy public void closeSession() { if (session != null && session.isOpen()) { session.close(); } } }
这样每个请求都会有专属的Session,既避免了多线程问题,又不用手动管理Session的关闭。
备注:内容来源于stack exchange,提问作者reza ramezani matin

