问题情形
springboot + jpa + querydsl + mysql + druid 的web应用,服务启动后,几次请求之后便无响应,甚至请求都没有进到controller断点,无任何报错。
排查过程
jps
查看应用进程号 (可选步骤:top -Hp 进程号
显示该进程下的线程)jstack -l 进程号
查看该进程的堆栈- 搜索应用包名关键字,定位到具体某个线程,查看该线程的状态,发现都是
waiting
状态 - 查看堆栈轨迹,发现都卡在了 druid 获取数据库连接上,连接池中没有可用连接,新进来的请求全部卡在 一个锁上,等待有连接释放。且由于没有配置
maxWait
,不会超时,会无限期永远等待下去,造成了应用卡死的表象
原因
本地测试环境,并没有什么并发,数据库连接也足够,所以怀疑是哪里没有释放连接,即连接泄露造成的。
查看druid的监控页面,发现activeCount(活跃连接数)一直在增加,poolingCount(池中连接数)一直在减少,直到降为了0,然后卡主。怀疑得到了佐证。
但是又发现有些接口是正常的,activeCount 和 poolingCount 都是正常的,只是个别接口会有上述情况。观察问题接口,里面有2个查询方法,怀疑其中有查询没有释放连接,最后定位到是 querydsl 的 transform 方法在没有加事务的情况下不会释放连接,这是querydsl的bug,github上也有相关issue,使用版本是5.0 。
解决办法
加上spring事务注解 Transactional(readonly=true)
解决
原理
EntityManager的创建
SharedEntityManagerCreator
的 createSharedEntityManager
方法,返回的是一个 jdk 动态代理,Invocation Handler 是 SharedEntityManagerInvocationHandler
.
凡是与JPA集成的所有框架的增删改查操作本质上都是操作 EntityManager,querydsl 也不例外.
都是一样的套路:
1
2
Query query=entityManager.createQuery(...);
query.getResultList() //或其他方法
下面从 没有加事务 和 加了事务 两种情况来讨论
未加事务时
从 entityManager.createQuery
入手,因为 entityManager 是一个动态代理,会进到 SharedEntityManagerInvocationHandler
中的 invoke
方法,首先会获取到当前真正要使用的 entityManager:
1
2
3
// Determine current EntityManager: either the transactional one
// managed by the factory or a temporary one for the given invocation.
EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(this.targetFactory, this.properties, this.synchronizedWithTransaction);
因为没有加事务,此处获取到的是 null.
接着往下走,会创建一个新的 entityManager:
1
2
3
4
5
6
7
if (target == null) {
logger.debug("Creating new EntityManager for shared EntityManager invocation");
target = (!CollectionUtils.isEmpty(this.properties) ?
this.targetFactory.createEntityManager(this.properties) :
this.targetFactory.createEntityManager());
isNewEm = true;
}
然后就是正常的反射调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object result = method.invoke(target, args);
if (result instanceof Query) {
Query query = (Query) result;
if (isNewEm) {
Class<?>[] ifcs = cachedQueryInterfaces.computeIfAbsent(query.getClass(), key ->
ClassUtils.getAllInterfacesForClass(key, this.proxyClassLoader));
result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs,
new DeferredQueryInvocationHandler(query, target));
isNewEm = false;
}
else {
EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory);
}
}
return result;
重点来了,当返回结果类型是 Query
时,会对结果再进行一次动态代理,Invocation Handler 是 DeferredQueryInvocationHandler
。而上面提到的的 createQuery
方法便正是要创建 Query,所以,后续该query上的所有增删改查操作都会进入到 DeferredQueryInvocationHandler
中的 invoke 方法.
在 DeferredQueryInvocationHandler
中的 invoke 方法中最后有一个finally块,其中便包含了 entityManager 的关闭操作,最终会转到 datasource 上,也就是在此时druid连接池进行连接的回收(DruidDataSource 的 recycle 方法):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
finally {
if (queryTerminatingMethods.contains(method.getName())) {
// Actual execution of the query: close the EntityManager right
// afterwards, since that was the only reason we kept it open.
if (this.outputParameters != null && this.target instanceof StoredProcedureQuery) {
StoredProcedureQuery storedProc = (StoredProcedureQuery) this.target;
for (Map.Entry<Object, Object> entry : this.outputParameters.entrySet()) {
try {
Object key = entry.getKey();
if (key instanceof Integer) {
entry.setValue(storedProc.getOutputParameterValue((Integer) key));
}
else {
entry.setValue(storedProc.getOutputParameterValue(key.toString()));
}
}
catch (IllegalArgumentException ex) {
entry.setValue(ex);
}
}
}
EntityManagerFactoryUtils.closeEntityManager(this.entityManager);
this.entityManager = null;
}
}
EntityManagerFactoryUtils.closeEntityManager 其实就是调用了 entityManager(此处的 entityManager 是 Hibernate的实现 SessionImpl) 的 close 方法:
1
2
3
4
5
6
7
8
9
10
11
12
public static void closeEntityManager(@Nullable EntityManager em) {
if (em != null) {
try {
if (em.isOpen()) {
em.close();
}
}
catch (Throwable ex) {
logger.error("Failed to release JPA EntityManager", ex);
}
}
}
所以,是Spring 为我们自动加上了创建连接和关闭连接的代码,从而我们无需在应用代码中手动调用。
那为什么 querydsl 的transform方法没有释放连接呢?
回看上面的 DeferredQueryInvocationHandler
的 finally 块,它有一个 if条件
:
1
queryTerminatingMethods.contains(method.getName())
而
1
2
3
4
5
6
7
8
9
10
static {
...
queryTerminatingMethods.add("execute"); // JPA 2.1 StoredProcedureQuery
queryTerminatingMethods.add("executeUpdate");
queryTerminatingMethods.add("getSingleResult");
queryTerminatingMethods.add("getResultStream");
queryTerminatingMethods.add("getResultList");
queryTerminatingMethods.add("list"); // Hibernate Query.list() method
}
所以就很明显,只有这6个方法,Spring会帮我们自动关闭entityManager,从而释放连接。transform
并没有在其中,需要我们手动释放。自然,querydsl的 getResultList
方法是没问题的。
这也是一个 querydsl 的bug,github issue 上有提到。
加了Spring的事务
那为什么加上事务便OK了呢?
首先还是从 entityManager.createQuery 开始,与上面不同的是,由于加了事务,EntityManagerFactoryUtils.doGetTransactionalEntityManager
能得到值,便不会进入 target==null 的 if块
,因此 isNewEm 为 false
,得到 query 后会进入
1
2
3
else {
EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory);
}
即此时的query不会是动态代理对象,而是原生的 hibernate 的 QueryImpl。因此此时query上的增删改查便不会自动释放连接,那这个工作由谁来完成?自然是Spring.
加了事务注解后,Spring 会为目标对象生成cglib代理(具体体现在 spring-aop 的 TransactionInterceptor),在方法开始前会开启事务,获取连接,方法结束后,会释放连接,并且保证同一个事务内使用的是同一个连接。保证了连接的释放。
总结
Spring大量使用了代理,包括基于jdk和基于字节码的代理。
未加事务时,生成的query是个代理对象,在目标方法调用结束后会自动释放连接(entityManager.close)
加了事务,生成的query是hibernate的原生QueryImpl对象,但是service类或方法是Spring生成的cglib代理对象,在方法开始前会开启事务,获取连接,方法结束后,会释放连接,并且保证同一个事务内使用的是同一个连接。保证了连接的释放。