由ReentrantLock和JPA(spring.jpa.open-in-view)导致的死锁问题原因分析。

问题
在压测过程中,发现服务经过一段时间压测之后出现无响应,且无法自动恢复。

分析
从上述问题表象中,猜测服务出现死锁,导致所有线程都在等待获取锁,从而无法响应后续所有请求。

接下来通过jstack输出线程堆栈信息查看,发现大量容器线程在等待数据库连接

 

"XNIO-1 task-251" #375 prio=5 os_prio=0 tid=0x00007fec640cf800 nid=0x53ea waiting on condition [0x00007febf64c5000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x0000000081565b80> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:1899)
at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1460)
at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1255)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1235)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1225)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:90)
at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122)
at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:35)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:106)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:136)
at org.hibernate.internal.SessionImpl.connection(SessionImpl.java:542)

查看DruidDataSource源码,可以看出当前已经没有可用的数据库连接,所以线程等待。

throws InterruptedException, SQLException {
try {
while (poolingCount == 0) {
emptySignal(); // send signal to CreateThread create connection

if (failFast && failContinuous.get()) {
throw new DataSourceNotAvailableException(createError);
}

notEmptyWaitThreadCount++;
if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
}
try {
// 当有新的连接被创建或者其他线程释放连接,就会被唤醒
notEmpty.await(); // signal by recycle or creator
} finally {
notEmptyWaitThreadCount--;
}
notEmptyWaitCount++;

if (!enable) {
connectErrorCountUpdater.incrementAndGet(this);
throw new DataSourceDisableException();
}
}
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to non-interrupted thread
notEmptySignalCount++;
throw ie;
}

decrementPoolingCount();
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;

return last;
}

再查看其他容器线程状态,发现有8个线程在等待 0x000000008437e2c8 锁,此锁是ReentrantLock,说明ReentrantLock已经被其他线程持有。

分析可能是因为某种情况这8个线程没有释放数据库连接,导致其他线程无法获取数据库连接(为什么是8个呢,因为数据库连接池采用默认配置,默认最大连接数为8)。

接下来继续查看ReentrantLock为什么没有正常的释放,查看当前持有该锁的线程信息,发现该线程持有了ReentrantLock锁,但是又再等待数据库连接。由于异常导致上一次获取到锁之后没有释放(没有在finally代码块中释放锁),如果数此线程可以获取到数据库连接,下次可能就会释放锁,应该不会导致死锁,所以问题的根本原因不是ReentrantLock没有释放锁。

通过上面的分析得知,有一个线程持有了ReentrantLock锁,但是在等待数据库连接,而另外8个线程持有了数据库连接,却在等待ReentrantLock锁,产生死锁。

但是正常情况下,当数据库操作执行完成之后,线程应该会释放数据库连接,这里显然没有释放。由于我们这边使用的JPA,所以猜测可能是JPA的问题。

联想到在SpringBoot启动日志中发现JPA的警告日志,具体如下

spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning

猜想可能是由于这个配置问题,于是开始找Spring Data JPA相关文档。发现这个配置会导致MVC的Controller执行完数据库操作后,仍然持有数据库连接。因为对于JPA(默认是Hibernate实现)来说,ToMany关系默认是懒加载,ToOne关系默认是立即加载。当我们通过JPA查询到一个对象之后,可能会去调用ToMany关系对应实体的get方法,获取对应实体集合,如果此时没有Hibernate Session会报LazyInitializationException异常,所以默认情况下MVC的Controller方法执行完成之后才会释放数据库连接。

查看​​spring.jpa.open-in-view​​对应的拦截器源码

public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {

@Override
public void preHandle(WebRequest request) throws DataAccessException {

EntityManagerFactory emf = obtainEntityManagerFactory();
if (TransactionSynchronizationManager.hasResource(emf)) {
// ...
}
else {
logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
try {
// 创建EntityManager并绑定到当前线程
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(emf, emHolder);

AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
asyncManager.registerCallableInterceptor(key, interceptor);
asyncManager.registerDeferredResultInterceptor(key, interceptor);
}
catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
}
}
}

@Override
public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
// 关闭EntityManager
if (!decrementParticipateCount(request)) {
EntityManagerHolder emHolder = (EntityManagerHolder)
TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
}
}

@Override
public void afterConcurrentHandlingStarted(WebRequest request) {
// 解除绑定
if (!decrementParticipateCount(request)) {
TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
}
}

}

结论

由于没有配置​​spring.jpa.open-in-view​​​(默认为true),JPA方法执行完成之后,并没有释放数据库连接(需要等到Controller方法执行完成才会释放),而恰好由于异常导致ReentrantLock锁没有正确释放,进而导致其他已经获取到数据库连接的线程无法获取ReentrantLock锁,其他线程也无法获取到数据库连接(其中就包含持有ReentrantLock锁的线程),最终导致死锁。修复的方法非常简单,finally代码块中释放锁,并且关闭​​spring.jpa.open-in-view​​配置(可选)。

对于​​spring.jpa.open-in-view​​这个配置大致存在两种观点,一种认为需要这个配置,它有利于提升开发效率,另一个部分人认为这个配置会影响到性能(Controller方法执行完成之后才释放连接),造成资源的浪费。但是如果执行完数据库操作就释放连接的话,就无法通过get方法获取ToMany关系对应的实体集合(或者获取手动获取,但显然不合适)。

其实这两种观点没有对错,只不过需要根据业务实际情况作出选择。我猜想可能出于这种考虑,官方才在用户没有主动配置​​spring.jpa.open-in-view​​​的时候,在启动的过程中打印出一条警告日志,通知用户关注此项配置,然后作出选择。​

 

使用springboot jpa,项目启动的时候有个warn的log:

WARN 26351 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this

Spring Boot中的spring.jpa.open-in-view = true属性是什么?

​spring.jpa.open-in-view=true​​在Spring Boot文档中看到了有关JPA配置的属性。

  • ​true​​如果根本没有提供此属性的默认值?
  • 这到底是做什么的?我没有找到任何很好的解释。
  • 它使你​​SessionFactory​​​代替使用​​EntityManagerFactory​​​吗?如果是,我如何告诉它允许我使用它​​EntityManagerFactory​​​?
    谢谢!

The OSIV Anti-Pattern

OSIV(视图中的打开会话)没有让业务层决定如何最好地获取视图层所需的所有关联,而是强制持久性上下文保持打开状态,以便视图层可以触发代理初始化,如图所示通过下图。

  • 该​​OpenSessionInViewFilter​​​调用​​openSession​​​底层的方法​​SessionFactory​​​,并获得新的​​Session​​。
  • 将​​Session​​​被绑定到​​TransactionSynchronizationManager​​。
  • 该​​OpenSessionInViewFilter​​​调用​​doFilter​​​的的​​javax.servlet.FilterChain​​对象引用和所述请求被进一步处理
  • 会​​DispatcherServlet​​​被调用,并将HTTP请求路由到基础​​PostController​​。
  • 该​​PostController​​​呼叫​​PostService​​拿到名单Post的实体。
  • 将​​PostService​​​打开一个新的事务,而​​HibernateTransactionManager​​​重用相同Session,是由打开的​​OpenSessionInViewFilter​​。
  • 在不初始化任何惰性关联的情况下PostDAO获取Post实体列表。
  • ​PostService​​​提交将提交基础事务,但未​​Session​​关闭,因为它是在外部打开的。
  • 在​​DispatcherServlet​​开始渲染的UI,这反过来,导航懒惰协会,并触发其初始化。
  • 在​​OpenSessionInViewFilter​​​可以关闭​​Session​​,和底层数据库连接被释放为好。

乍看起来,这似乎并不可怕,但是,从数据库的角度来看,一系列缺陷变得更加明显。

服务层打开和关闭数据库事务,但是此后,没有任何显式事务在进行。因此,在自动提交模式下执行从UI渲染阶段发出的所有其他语句。自动提交给数据库服务器带来了压力,因为每个语句都必须将事务日志刷新到磁盘,因此在数据库侧会导致大量I / O通信。一种优化是将标记Connection为只读,这将允许数据库服务器避免写入事务日志。

由于服务层和UI呈现过程都生成了语句,因此不再存在关注点分离。编写断言要生成的语句数量的集成测试需要在将应用程序部署在Web容器上的同时遍历所有层(Web,服务,DAO)。即使在使用内存数据库(例如HSQLDB)和轻量级Web服务器(例如Jetty)时,这些集成测试的执行速度也要比分离层和后端集成测试使用数据库的速度慢。前端集成测试完全模拟了服务层。

UI层仅限于导航关联,这又会触发N + 1查询问题。尽管Hibernate提供@BatchSize了批量获取关联的功能,并且FetchMode.SUBSELECT为了应对这种情况,但注释会影响默认的获取计划,因此它们会应用于每个业务用例。因此,数据访问层查询非常适合,因为它可以针对当前用例数据获取要求进行定制。

最后但并非最不重要的一点是,数据库连接在整个UI呈现阶段均保持不变,这会增加连接租用时间并由于数据库连接池上的拥塞而限制总体事务吞吐量。保持的连接越多,等待从池中获取连接的其他并发请求越多。

Spring Boot and OSIV

不幸的是,在Spring Boot中默认启用了OSIV(视图中的Open Session),从性能和可伸缩性的角度来看,OSIV确实不是一个好主意。

因此,请确保在​​application.properties​​配置文件中具有以下条目:

spring.jpa.open-in-view=false

这将禁用OSIV这样就可以处理​​LazyInitializationException​​的正确方法。

从2.0版开始,默认情况下启用OSIV时,Spring Boot会发出警告,因此你可以在此问题影响生产系统很长时间之前就发现它。

 

报错org.hibernate.LazyInitializationException: could not initialize proxy - no Session,差不多都是因为懒加载异常,

报错截图:

Explicitly configure spring.jpa.open-in-view to disable this warning_JPA

 

大概意思就是初始化的时候session关闭了,这个主要是因为hibernate默认的懒加载策略:默认lazy为true 引起的异常。

解决方案:
以下方案选其一即可。

1、如果你有xml配置文件,可以在class标签里,加一个lazy="true",将lazy设成false

2、如果你使用的是springboot的jpa,可以在配置文件里加一个配置:(推荐使用)

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

3、使用OpenSessionInViewFilter解决解决懒加载问题,在web.xml中配置,放到struts过滤器之前

<!-- 配置Spring的OpenSessionInViewFilter,以解决懒加载问题  -->
<filter>
<filter-name>OpenSessionInViewFilter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OpenSessionInViewFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>