由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源码,可以看出当前已经没有可用的数据库连接,所以线程等待。

    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已经被其他线程持有。

分析可能是因为某种情况这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的时候,在启动的过程中打印出一条警告日志,通知用户关注此项配置,然后作出选择。

spring.jpa.open-view问题的更多相关文章

  1. 使用Spring JPA中Page、Pageable接口和Sort类完成分页排序

    显示时,有三个参数,前两个必填,第几页,一页多少个size,第三个参数默认可以不填. 但是发现这个方法已经过时了,通过查看它的源码发现,新方法为静态方法PageRequest of(page,size ...

  2. 去掉WARN spring.jpa.open-in-view is enabled by default

    使用springboot jpa,在运行项目时发现一个WARN WARN 11472 --- [ main] aWebConfiguration$JpaWebMvcConfiguration : sp ...

  3. spring jpa 实体互相引用返回restful数据循环引用报错的问题

    spring jpa 实体互相引用返回restful数据循环引用报错的问题 Java实体里两个对象有关联关系,互相引用,比如,在一对多的关联关系里 Problem对象,引用了标签列表ProblemLa ...

  4. spring jpa 自定义查询数据库的某个字段

    spring jpa 提供的查询很强大, 就看你会不会用了. 先上代码, 后面在解释吧 1. 想查单个表的某个字段 在repository中 @Query(value = "select i ...

  5. Hibernate | Spring JPA | MySQL 使用过程遇到的一些问题

    1. 使用过程 2. 背景 3. 遇到问题 3.1 不指定Hibernate数据库方言,默认SQL生成方式 3.2 抛出异常Hibernate加入了@Transactional事务不会回滚 3.3 H ...

  6. Spring JPA 使用@CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy 自动生成时间和修改者

    JPA Audit 在spring jpa中,支持在字段或者方法上进行注解@CreatedDate.@CreatedBy.@LastModifiedDate.@LastModifiedBy,从字面意思 ...

  7. Spring JPA学习笔记

    目录 什么是JPA? 引入配置 新建一个Entity Bean类 JPA的增删改查 新建操作接口 新建测试类 总结 什么是JPA? 什么是JDBC知道吧?数据库有Mysql,SQL Server,Or ...

  8. Spring JPA实现逻辑源码分析总结

    1.SharedEntityManagerCreator: entitymanager的创建入口 该类被EntityManagerBeanDefinitionRegistrarPostProcesso ...

  9. spring jpa和mybatis整合

    spring jpa和mybatis整合 前一阵子接手了一个使用SpringBoot 和spring-data-jpa开发的项目 后期新加入一个小伙伴,表示jpa相比mybatis太难用,多表联合的查 ...

随机推荐

  1. H5播放需要解密的m3u8音频文件

    <audio ref="audio"></audio> import CryptoJS from "crypto-js"; import ...

  2. 通过浏览器运行cmd命令、启动steam

    我们先来看看实现起来的效果,我们在浏览器中输入ping so.com 试试打开计算器.启动steam 要实现这个效果其实用到了浏览器自定义协议,我们可以通过自定义协议打开wechat.扣扣.emali ...

  3. hdu1233 最小生成树Prim算法和Kruskal算法

    Prim算法 时间复杂度:O(\(N^2\),N为结点数) 说明:先任意找一个点标记,然后每次找一条最短的两端分别为标记和未标记的边加进来,再把未标记的点标记上.即每次加入一条合法的最短的边,每次扩展 ...

  4. 12、Linux磁盘设备基础知识(1)

    GB TB PB EP ZB YB BB:

  5. Python装饰器-给你的咖啡加点料

    今天你的咖啡加糖了吗? 让我们通过一个简单的例子来引出装饰器的概念及用法.在引出装饰器之前,我们先来了解一下函数的概念. 一.函数回顾 1.在python中函数是一等公民,函数也是对象.我们可以把函数 ...

  6. 2012年第三届蓝桥杯C/C++程序设计本科B组省赛 密码发生器

    密码发生器 题目描述: ```bash 在对银行账户等重要权限设置密码的时候,我们常常遇到这样的烦恼:如果为了好记用生日吧,容易被破解,不安全:如果设置不好记的密码,又担心自己也会忘记:如果写在纸上, ...

  7. 第十一章:random库概述

    random库是使用随机数的python标准库 伪随机数:采用梅森旋转算法生成的伪随机序列中的元素 random库主要用于产生随机数 使用random库:import random random库的概 ...

  8. 「CF547D」 Mike and Fish

    「CF547D」 Mike and Fish 传送门 介绍三种做法. \(\texttt{Solution 1}\) 上下界网络流 我们将每一行.每一列看成一个点. 两种颜色的数量最多相差 \(1\) ...

  9. 【多线程】C++ 互斥锁(mutex)的简单原理分析

    多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序.一般情况下,分为两种类型的多任务处理:基于进程和基于线程. 1)基于进程的多任务处理是程序的并发执行. 2)基于线程 ...

  10. L inux系统安全及应用---暴力破解密码

    系统安全及应用一.开关机安全控制① 调整BIOS引导设置② GRUB限制二.终端登录安全控制① 限制root只在安全终端登录② 禁止普通用户登录举例三.系统弱口令检测① Joth the Ripper ...