由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的時候,在啟動的過程中打印出一條警告日志,通知用戶關注此項配置,然后作出選擇。
https://www.cnblogs.com/thisismartin/p/13594399.html
使用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 warning
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,差不多都是因為懶加載異常,
報錯截圖:
大概意思就是初始化的時候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>
