由ReentrantLock和JPA(spring.jpa.open-in-view)致使的死锁问题缘由分析。java
问题
在压测过程当中,发现服务通过一段时间压测以后出现无响应,且没法自动恢复。spring
分析
从上述问题表象中,猜想服务出现死锁,致使全部线程都在等待获取锁,从而没法响应后续全部请求。数据库
接下来经过jstack输出线程堆栈信息查看,发现大量容器线程在等待数据库链接jsp
"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源码,能够看出当前已经没有可用的数据库链接,因此线程等待。async
DruidConnectionHolder takeLast() 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已经被其余线程持有。ide
分析多是由于某种状况这8个线程没有释放数据库链接,致使其余线程没法获取数据库链接(为何是8个呢,由于数据库链接池采用默认配置,默认最大链接数为8)。性能
接下来继续查看ReentrantLock为何没有正常的释放,查看当前持有该锁的线程信息,发现该线程持有了ReentrantLock锁,可是又再等待数据库链接。因为异常致使上一次获取到锁以后没有释放(没有在finally代码块中释放锁),若是数此线程能够获取到数据库链接,下次可能就会释放锁,应该不会致使死锁,因此问题的根本缘由不是ReentrantLock没有释放锁。ui
经过上面的分析得知,有一个线程持有了ReentrantLock锁,可是在等待数据库链接,而另外8个线程持有了数据库链接,却在等待ReentrantLock锁,产生死锁。this
可是正常状况下,当数据库操做执行完成以后,线程应该会释放数据库链接,这里显然没有释放。因为咱们这边使用的JPA,因此猜想多是JPA的问题。spa
联想到在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
的时候,在启动的过程当中打印出一条警告日志,通知用户关注此项配置,而后做出选择。
转载:诺顿教育