SpringBoot項目中建議關(guān)閉Open-EntityManager-in-view原因
前言
一天,開發(fā)突然找過來說KLock分布式鎖失效了,高并發(fā)情況下沒有鎖住請求,導(dǎo)致數(shù)據(jù)庫拋樂觀鎖的異常。一開始我是不信的,KLock是經(jīng)過線上大量驗證的,怎么會出現(xiàn)這么低級的問題呢?然后,協(xié)助開發(fā)一起排查了一下午,最后經(jīng)過不懈努力和一探到底的摸索精神最終查明不是KLock鎖的問題,問題出在Spring Data Jpa的Open-EntityManager-in-view這個配置上,這里先建議各位看官關(guān)閉Open-EntityManager-in-view,具體緣由下面慢慢道來
問題背景
假設(shè)我們有一張賬戶表account,業(yè)務(wù)邏輯是先用id查詢出來,校驗下,然后用于其他的邏輯操作,最后在用id查詢出來更新這個account,業(yè)務(wù)流程如下:
- 請求一:
查詢id =6的記錄,此時JpaVersion =6,業(yè)務(wù)處理,再次查詢id =6的記錄,JpaVersion =6,然后更新數(shù)據(jù)提交 - 請求二:
查詢id =6的記錄,此時JpaVersion =6, 業(yè)務(wù)處理,此時請求一結(jié)束了,再次查詢id=6的記錄,JpaVersion =6,更新數(shù)據(jù)提交失敗
首先,請求一和請求二是模擬的并發(fā)請求,然后問題出在,當(dāng)請求一事務(wù)正常提交結(jié)束后,請求二最后一次查詢的JpaVersion還是沒有變化,導(dǎo)致了當(dāng)前版本和數(shù)據(jù)庫中的版本不一致二拋樂觀鎖異常,而KLock鎖是加在第二次查詢更新的方法上面的,可以肯定KLock鎖沒有問題,鎖住了請求,直到請求一結(jié)束后,請求二才進方法。
2019-11-20 18:32:00.573 [/] pay-settlement-app [http-nio-8086-exec-4] ERROR c.k.p.p.s.a.e.ControllerExceptionHandler - Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1 org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:320) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:488) at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59) at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:147) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
OPEN-ENTITYMANAGER-IN-VIEW的前世今生
Open-EntityManager-in-view簡述下就是在視圖層打開EntityManager,spring boot2.x中默認(rèn)是開啟這個配置的,作用是綁定EntityManager到當(dāng)前線程中,然后在試圖層就開啟Hibernate Session。用于在Controller層直接操作游離態(tài)的對象,以及懶加載查詢。在應(yīng)用配置中可以使用spring.jpa.open-in-view=true/false來開啟和關(guān)閉它,最終控制的其實是OpenEntityManagerInViewInterceptor攔截器,如果開啟就添加此攔截器,如果關(guān)閉則不添加。然后在這個攔截器中會開啟連接,打開Session,業(yè)務(wù)Controller執(zhí)行完畢后關(guān)閉資源。打開關(guān)閉代碼如下:
public void preHandle(WebRequest request) throws DataAccessException { String key = getParticipateAttributeName(); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); if (asyncManager.hasConcurrentResult() && applyEntityManagerBindingInterceptor(asyncManager, key)) { return; } EntityManagerFactory emf = obtainEntityManagerFactory(); if (TransactionSynchronizationManager.hasResource(emf)) { // Do not modify the EntityManager: just mark the request accordingly. Integer count = (Integer) request.getAttribute(key, WebRequest.SCOPE_REQUEST); int newCount = (count != null ? count + 1 : 1); request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST); } else { logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor"); try { 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); } } } public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException { if (!decrementParticipateCount(request)) { EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory()); logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor"); EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } }
在Spring MVC時代,懶加載的問題也比較常見,那個時候是通過定義一個OpenEntityManagerInViewFilter的過濾器解決問題的,效果和攔截器是一樣的,算是同門師兄弟的關(guān)系。如果沒有配置,在懶加載的場景下就會拋出LazyInitializationException的異常。
問題的真實原因
了解了Open-EntityManager-in-view后,我們來分析下具體的原因。由于在view層就開啟Session了,導(dǎo)致了同一個請求第二次查詢時根本就沒走數(shù)據(jù)庫,直接獲取的Hibernate Session緩存中的數(shù)據(jù),此時無論怎么加鎖,都讀不到數(shù)據(jù)庫中的數(shù)據(jù),所以只要有并發(fā)就會拋樂觀鎖異常。這讓我聯(lián)想到了老早前一個同事和我說的他們遇到的一個并發(fā)問題,即使給@Transactional事務(wù)的隔離級別設(shè)置為串行化執(zhí)行,還是會報樂觀鎖的異常。有可能就是這個問題導(dǎo)致的,在這個案例中,加鎖不好使,即使使用數(shù)據(jù)庫的串行化隔離級別也不好使。因為第二次查詢根本就不走數(shù)據(jù)庫了。
解決方案
真實原因已經(jīng)定位到了,KL博主給出了幾種方案解決問題,如下:
- 方案一、將KLock前置,把加分布式鎖的邏輯移到第一次使用id查詢之前,即讓查詢發(fā)生在別的請求事務(wù)結(jié)束之前,這樣無論第一次查詢還是第二次查詢獲取到的都是別的事務(wù)已提交的內(nèi)容
- 方案二、使用spring.jpa.open-in-view=false關(guān)閉,這個方案比較簡單粗暴,但是影響會比較大,其他的代碼很可能已經(jīng)依賴了懶加載的功能特性,貿(mào)然去掉會帶來大量的回歸測試工作,所以雖然博主建議關(guān)閉這個特性,但是在已經(jīng)使用了的系統(tǒng)中不推薦
- 方案三、局部控制Open-EntityManager-in-view行為,就是人為編碼控制EntityManager的綁定,在有影響的地方先取消綁定,然后執(zhí)行完后在添加回來,不添加回來會導(dǎo)致Jpa自己的解綁邏輯報錯。代碼如下:
/** * @author: kl @kailing.pub * @date: 2019/11/20 */ @Component public class OpenEntityManagerInViewManager extends EntityManagerFactoryAccessor { public void cancel() { EntityManagerFactory emf = obtainEntityManagerFactory(); EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.unbindResourceIfPossible(emf); EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } public void add() { EntityManagerFactory emf = obtainEntityManagerFactory(); if (!TransactionSynchronizationManager.hasResource(emf)) { EntityManager em = createEntityManager(); EntityManagerHolder emHolder = new EntityManagerHolder(em); TransactionSynchronizationManager.bindResource(emf,emHolder); } } }
- 方案四:方案三為了達到效果有點費勁哈,其實還有一種方案,在第二次查詢前使用EntityManager的clear清除Session緩存即可,
建議關(guān)閉OPEN-ENTITYMANAGER-IN-VIEW
在Spring boot2.x中,如果沒有顯示配置spring.jpa.open-in-view,默認(rèn)開啟的這個特性Spring會給出一個警告提示:
logger.warn("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");
用來告訴你,我開啟這個特性了,你可以顯示配置來關(guān)閉這個提示。博主猜測就是告知用戶,你可能用不著吧。確實,現(xiàn)在微服務(wù)中的應(yīng)用在使用Spring Data JPA時,已經(jīng)很少使用懶加載的特性了。而且如果你的代碼規(guī)范點,也用不著直接在Controller層寫Dao層的代碼??偨Y(jié)下就是根本就不需要Open-EntityManager-in-view的特性,然后它還有副作用,開啟Open-EntityManager-in-view,會使數(shù)據(jù)庫租用連接時長變長,長時間占用連接直接影響整體事務(wù)吞吐量。然后一不小心就會陷進Session緩存的坑里。所以,新項目就直接去掉吧,老項目去掉后回歸驗證下
結(jié)語
因為對業(yè)務(wù)不熟悉,不知道業(yè)務(wù)邏輯中查詢了兩次相同的實體,導(dǎo)致整個排錯過程比較曲折。先是開發(fā)懷疑鎖的問題,驗證鎖沒問題后,又陷進了IDEA斷點的問題,因為模擬的并發(fā)請求,斷點釋放一次會通過多個請求,看上去就像很多請求沒進來一樣。然后又懷疑了事務(wù)和加鎖前后的邏輯問題,如果釋放鎖在釋放事務(wù)前就會有問題,將斷點打在了JDBC的Commit方法里,確認(rèn)了這個也是正常的。最后才聯(lián)想到Spring boot中默認(rèn)開啟了spring.jpa.open-in-view,會不會有關(guān)系,也不確定,懷著死馬當(dāng)活馬醫(yī)的心態(tài)試了下,果然是這個導(dǎo)致的,這個時候只知道是這個導(dǎo)致的,還沒發(fā)現(xiàn)是這個導(dǎo)致的Session問題,以為是進KLock前就開啟了事務(wù)鎖定了數(shù)據(jù)庫版本記錄,所以查詢的時候返回的老的記錄,最后把事務(wù)串行化后還不行,才發(fā)現(xiàn)的業(yè)務(wù)查詢了兩次進而發(fā)現(xiàn)了Session緩存的問題。至此,水落石出,所有問題迎刃而解。
以上就是SpringBoot項目中建議關(guān)閉Open-EntityManager-in-view原因的詳細(xì)內(nèi)容,更多關(guān)于Spring Boot關(guān)閉Open-EntityManager-in-view的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于Nacos和Eureka的區(qū)別及說明
這篇文章主要介紹了關(guān)于Nacos和Eureka的區(qū)別及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06Spring?Boot?教程之創(chuàng)建項目的三種方式
這篇文章主要分享了Spring?Boot?教程之創(chuàng)建項目的三種方式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-05-05使用String轉(zhuǎn)換到Map結(jié)構(gòu)
這篇文章主要介紹了使用String轉(zhuǎn)換到Map結(jié)構(gòu),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11