<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
一天,開發突然找過來說KLock分散式鎖失效了,高並行情況下沒有鎖住請求,導致資料庫拋樂觀鎖的異常。一開始我是不信的,KLock是經過線上大量驗證的,怎麼會出現這麼低階的問題呢?然後,協助開發一起排查了一下午,最後經過不懈努力和一探到底的摸索精神最終查明不是KLock鎖的問題,問題出在Spring Data Jpa的Open-EntityManager-in-view這個設定上,這裡先建議各位看官關閉Open-EntityManager-in-view,具體緣由下面慢慢道來
假設我們有一張賬戶表account,業務邏輯是先用id查詢出來,校驗下,然後用於其他的邏輯操作,最後在用id查詢出來更新這個account,業務流程如下:
首先,請求一和請求二是模擬的並行請求,然後問題出在,當請求一事務正常提交結束後,請求二最後一次查詢的JpaVersion還是沒有變化,導致了當前版本和資料庫中的版本不一致二拋樂觀鎖異常,而KLock鎖是加在第二次查詢更新的方法上面的,可以肯定KLock鎖沒有問題,鎖住了請求,直到請求一結束後,請求二才進方法。
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簡述下就是在檢視層開啟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博主給出了幾種方案解決問題,如下:
/** * @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); } } }
在Spring boot2.x中,如果沒有顯示設定spring.jpa.open-in-view,預設開啟的這個特性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");
用來告訴你,我開啟這個特性了,你可以顯示設定來關閉這個提示。博主猜測就是告知使用者,你可能用不著吧。確實,現在微服務中的應用在使用Spring Data JPA時,已經很少使用懶載入的特性了。而且如果你的程式碼規範點,也用不著直接在Controller層寫Dao層的程式碼。總結下就是根本就不需要Open-EntityManager-in-view的特性,然後它還有副作用,開啟Open-EntityManager-in-view,會使資料庫租用連線時長變長,長時間佔用連線直接影響整體事務吞吐量。然後一不小心就會陷進Session快取的坑裡。所以,新專案就直接去掉吧,老專案去掉後迴歸驗證下
因為對業務不熟悉,不知道業務邏輯中查詢了兩次相同的實體,導致整個排錯過程比較曲折。先是開發懷疑鎖的問題,驗證鎖沒問題後,又陷進了IDEA斷點的問題,因為模擬的並行請求,斷點釋放一次會通過多個請求,看上去就像很多請求沒進來一樣。然後又懷疑了事務和加鎖前後的邏輯問題,如果釋放鎖在釋放事務前就會有問題,將斷點打在了JDBC的Commit方法裡,確認了這個也是正常的。最後才聯想到Spring boot中預設開啟了spring.jpa.open-in-view,會不會有關係,也不確定,懷著死馬當活馬醫的心態試了下,果然是這個導致的,這個時候只知道是這個導致的,還沒發現是這個導致的Session問題,以為是進KLock前就開啟了事務鎖定了資料庫版本記錄,所以查詢的時候返回的老的記錄,最後把事務序列化後還不行,才發現的業務查詢了兩次進而發現了Session快取的問題。至此,水落石出,所有問題迎刃而解。
以上就是SpringBoot專案中建議關閉Open-EntityManager-in-view原因的詳細內容,更多關於Spring Boot關閉Open-EntityManager-in-view的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45