一天,开发忽然找过来讲KLock分布式锁失效了,高并发状况下没有锁住请求,致使数据库抛乐观锁的异常。一开始我是不信的,KLock是通过线上大量验证的,怎么会出现这么低级的问题呢?而后,协助开发一块儿排查了一下午,最后通过不懈努力和一探到底的摸索精神最终查明不是KLock锁的问题,问题出在Spring Data Jpa的Open-EntityManager-in-view这个配置上,这里先建议各位看官关闭Open-EntityManager-in-view,具体原因下面慢慢道来java
假设咱们有一张帐户表account,业务逻辑是先用id查询出来,校验下,而后用于其余的逻辑操做,最后在用id查询出来更新这个account,业务流程以下:git
首先,请求一和请求二是模拟的并发请求,而后问题出在,当请求一事务正常提交结束后,请求二最后一次查询的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简述下就是在视图层打开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会给出一个警告提示: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)博主。