zoukankan      html  css  js  c++  java
  • spring.jpa.open-view问题

    由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的时候,在启动的过程中打印出一条警告日志,通知用户关注此项配置,然后作出选择。

  • 相关阅读:
    java的map
    linux的date的几个例子
    URI记录
    eclipse进行关联代码
    hive的top n
    python类定义
    sql一些常用的经典语句,最后是select as的用法
    linux,shell脚本set -x的意思
    python循环for,range,xrange;while
    python安装json的方法;以及三种json库的区别
  • 原文地址:https://www.cnblogs.com/thisismarc/p/13594399.html
Copyright © 2011-2022 走看看