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的時候,在啟動的過程中打印出一條警告日志,通知用戶關注此項配置,然后作出選擇。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM