Spring Boot中建議關閉Open-EntityManager-in-view

前言

一天,開發忽然找過來講KLock分佈式鎖失效了,高併發狀況下沒有鎖住請求,致使數據庫拋樂觀鎖的異常。一開始我是不信的,KLock是通過線上大量驗證的,怎麼會出現這麼低級的問題呢?而後,協助開發一塊兒排查了一下午,最後通過不懈努力和一探到底的摸索精神最終查明不是KLock鎖的問題,問題出在Spring Data Jpa的Open-EntityManager-in-view這個配置上,這裏先建議各位看官關閉Open-EntityManager-in-view,具體原因下面慢慢道來java

問題背景

假設咱們有一張帳戶表account,業務邏輯是先用id查詢出來,校驗下,而後用於其餘的邏輯操做,最後在用id查詢出來更新這個account,業務流程以下:git

  • 請求一:查詢id =6的記錄,此時JpaVersion =6,業務處理,再次查詢id =6的記錄,JpaVersion =6,而後更新數據提交
  • 請求二:查詢id =6的記錄,此時JpaVersion =6, 業務處理,此時請求一結束了,再次查詢id=6的記錄,JpaVersion =6,更新數據提交失敗

首先,請求一和請求二是模擬的併發請求,而後問題出在,當請求一事務正常提交結束後,請求二最後一次查詢的JpaVersion仍是沒有變化,致使了當前版本和數據庫中的版本不一致二拋樂觀鎖異常,而KLock鎖是加在第二次查詢更新的方法上面的,能夠確定KLock鎖沒有問題,鎖住了請求,直到請求一結束後,請求二才進方法。spring

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中默認是開啓這個配置的,做用是綁定EntityManager到當前線程中,而後在試圖層就開啓Hibernate Session。用於在Controller層直接操做遊離態的對象,以及懶加載查詢。在應用配置中可使用spring.jpa.open-in-view=true/false來開啓和關閉它,最終控制的實際上是OpenEntityManagerInViewInterceptor攔截器,若是開啓就添加此攔截器,若是關閉則不添加。而後在這個攔截器中會開啓鏈接,打開Session,業務Controller執行完畢後關閉資源。打開關閉代碼以下:數據庫

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的過濾器解決問題的,效果和攔截器是同樣的,算是同門師兄弟的關係。若是沒有配置,在懶加載的場景下就會拋出LazyInitializationException的異常。緩存

問題的真實緣由

瞭解了Open-EntityManager-in-view後,咱們來分析下具體的緣由。因爲在view層就開啓Session了,致使了同一個請求第二次查詢時根本就沒走數據庫,直接獲取的Hibernate Session緩存中的數據,此時不管怎麼加鎖,都讀不到數據庫中的數據,因此只要有併發就會拋樂觀鎖異常。這讓我聯想到了老早前一個同事和我說的他們遇到的一個併發問題,即便給@Transactional事務的隔離級別設置爲串行化執行,仍是會報樂觀鎖的異常。有可能就是這個問題致使的,在這個案例中,加鎖很差使,即便使用數據庫的串行化隔離級別也很差使。由於第二次查詢根本就不走數據庫了。架構

解決方案

真實緣由已經定位到了,KL博主給出了幾種方案解決問題,以下:併發

  • 方案1、將KLock前置,把加分佈式鎖的邏輯移到第一次使用id查詢以前,即讓查詢發生在別的請求事務結束以前,這樣不管第一次查詢仍是第二次查詢獲取到的都是別的事務已提交的內容
  • 方案2、使用spring.jpa.open-in-view=false關閉,這個方案比較簡單粗暴,可是影響會比較大,其餘的代碼極可能已經依賴了懶加載的功能特性,貿然去掉會帶來大量的迴歸測試工做,因此雖然博主建議關閉這個特性,可是在已經使用了的系統中不推薦
  • 方案3、局部控制Open-EntityManager-in-view行爲,就是人爲編碼控制EntityManager的綁定,在有影響的地方先取消綁定,而後執行完後在添加回來,不添加回來會致使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緩存便可,

建議關閉Open-EntityManager-in-view

在Spring boot2.x中,若是沒有顯示配置spring.jpa.open-in-view,默認開啓的這個特性Spring會給出一個警告提示:app

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");

用來告訴你,我開啓這個特性了,你能夠顯示配置來關閉這個提示。博主猜想就是告知用戶,你可能用不着吧。確實,如今微服務中的應用在使用Spring Data JPA時,已經不多使用懶加載的特性了。並且若是你的代碼規範點,也用不着直接在Controller層寫Dao層的代碼。總結下就是根本就不須要Open-EntityManager-in-view的特性,而後它還有反作用,開啓Open-EntityManager-in-view,會使數據庫租用鏈接時長變長,長時間佔用鏈接直接影響總體事務吞吐量。而後一不當心就會陷進Session緩存的坑裏。因此,新項目就直接去掉吧,老項目去掉後迴歸驗證下async

結語

由於對業務不熟悉,不知道業務邏輯中查詢了兩次相同的實體,致使整個排錯過程比較曲折。先是開發懷疑鎖的問題,驗證鎖沒問題後,又陷進了IDEA斷點的問題,由於模擬的併發請求,斷點釋放一次會經過多個請求,看上去就像不少請求沒進來同樣。而後又懷疑了事務和加鎖先後的邏輯問題,若是釋放鎖在釋放事務前就會有問題,將斷點打在了JDBC的Commit方法裏,確認了這個也是正常的。最後才聯想到Spring boot中默認開啓了spring.jpa.open-in-view,會不會有關係,也不肯定,懷着死馬當活馬醫的心態試了下,果真是這個致使的,這個時候只知道是這個致使的,還沒發現是這個致使的Session問題,覺得是進KLock前就開啓了事務鎖定了數據庫版本記錄,因此查詢的時候返回的老的記錄,最後把事務串行化後還不行,才發現的業務查詢了兩次進而發現了Session緩存的問題。至此,水落石出,全部問題迎刃而解。分佈式

做者簡介:

陳凱玲,2016年5月加入凱京科技。現任凱京科技研發中心架構組經理,救火隊隊長。獨立博客KL博客(http://www.kailing.pub)博主。

相關文章
相關標籤/搜索