美文网首页
数据库:带你彻底了解乐观锁的实现

数据库:带你彻底了解乐观锁的实现

作者: 进击的三文鱼 | 来源:发表于2021-04-29 08:46 被阅读0次

    一 乐观锁概念

    乐观锁是什么?他是干什么用的,首先乐观锁是用来,控制数据准确性,比如A连接和B连接同时修改一条数据,A连接将数据修改了,B连接也将数据修改了,这时B先提交了事务,这时如果A连接也提交了事务,那么A就会覆盖B连接的修改,因为A查询出的对象不包含B的修改,所以这时是有问题的。而乐观锁就是通过version进行判断,谁先提交谁后提交,如果后提交的和数据库中的version不一致,就抛出异常,从而保证数据的准确定

    二 乐观锁实现

    乐观锁是数据库实现的还是java代码中实现的呢?他是怎么实现的呢,我想这个问题会困扰很多人,网上有很多篇文章只是说了一个大概,比如乐观锁和悲观锁的区别,只是说了乐观锁需要我们手动实现,而并没有提怎么实现,现在就来看看他到底是怎么实现的

    1 基于SpringDataJpa的乐观锁实现

    首先我们看一段代码

    SpecialPrintingRule rule =new SpecialPrintingRule();
    rule.audit(identify);
    specialPrintingRuleRepository.saveAndFlush(rule);
    

    这段代码就是SpringDataJpa的保存实体的代码,我们就通过他来看看乐观锁是怎么一回事

    我们来看看乐观锁报错的代码

    Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.sunisco.eir.domain.EdiBooking#8a8ac82978d388880178f1fdcd5e247f]
            at org.hibernate.event.def.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:485)
            at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:255)
            at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:84)
            at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:867)
            at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:851)
            at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:855)
            at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:853)
            at sun.reflect.GeneratedMethodAccessor816.invoke(Unknown Source)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:606)
            at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298)
            at com.sun.proxy.$Proxy62.merge(Unknown Source)
            at com.inspireso.framework.jpa.repository.support.InternalRepository.saveInternal(InternalRepository.java:436)
            at com.inspireso.framework.jpa.repository.support.InternalRepository.save(InternalRepository.java:352)
            at com.inspireso.framework.jpa.repository.support.GenericRepositorySupport.save(GenericRepositorySupport.java:344)
            at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:413)
            at sun.reflect.GeneratedMethodAccessor815.invoke(Unknown Source)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:606)
            at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:425)
            at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:410)
            at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:364)
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
            at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
            at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
            at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
            at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
            ... 147 common frames omitted
    

    如果经历过乐观锁报错的同学对这段报错信息一定不陌生,没经历过的同学也没关系,因为只是通过这段报错信息来找到乐观锁的实现,我们看到报错信息中有一行代码DefaultMergeEventListener.onMerge(),我们去查看下他做了什么

    //杨乐乐源码分析  https://juejin.cn/user/4486440898279832
    public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
    ......
     //1 获取PersistenceContext对象,再从PersistenceContext中查找是否有和我们插入实体相同的实体
     EntityEntry entry = source.getPersistenceContext().getEntry(entity);
                if (entry == null) {
                    EntityPersister persister = source.getEntityPersister(event.getEntityName(), entity);
                    Serializable id = persister.getIdentifier(entity, source);
                    if (id != null) {
                        EntityKey key = new EntityKey(id, persister, source.getEntityMode());
                        Object managedEntity = source.getPersistenceContext().getEntity(key);
                        entry = source.getPersistenceContext().getEntry(managedEntity);
                        if (entry != null) {
                            entityState = 2;
                        }
                    }
                }
    
                if (entityState == -1) {
                 //2 获取entityState状态为后续判断做准备
                    entityState = this.getEntityState(entity, event.getEntityName(), entry, source);
                }
                //3 通过上面返回的entityState来进行判断走不同方法
                switch(entityState) {
                case 0:
                    this.entityIsPersistent(event, copyCache);
                    break;
                case 1:
                    this.entityIsTransient(event, copyCache);
                    break;
                case 2:
                    //4 触发了乐观锁会进入此方法
                    this.entityIsDetached(event, copyCache);
                    break;
       ......
    

    我们跟着调试最终看到,程序执行了这段代码,我们看到注释的第一步,这段代码

    EntityEntry entry = source.getPersistenceContext().getEntry(entity);
    他会先获取PersistenceContext对象,翻译一下就是持久化的上下文对象,听起来就很厉害,这个对象里面有什么呢?一起看一看

    public interface PersistenceContext {
    

    原来他是一个接口,那我们就去看他的实现

    //杨乐乐源码分析  https://juejin.cn/user/4486440898279832
    public class StatefulPersistenceContext implements PersistenceContext {
        public static final Object NO_ROW = new MarkerObject("NO_ROW");
        private static final Logger log = LoggerFactory.getLogger(StatefulPersistenceContext.class);
        private static final Logger PROXY_WARN_LOG = LoggerFactory.getLogger(StatefulPersistenceContext.class.getName() + ".ProxyWarnLog");
        private static final int INIT_COLL_SIZE = 8;
        private SessionImplementor session;
        private Map entitiesByKey;
        private Map entitiesByUniqueKey;
        //保存持久化实体的Map映射
        private Map entityEntries;
    

    他的实现是StatefulPersistenceContext类,我们看到entityEntries,他就是保存持久化实体的Map对象。

    这里再说一下实体的持久化状态,再我们学习Hibernate的时候一定学习过实体有几种状态,Jpa也一样,他们就是Jpa对象的生命周期

    image.png

    New:瞬时对象,尚未有id,还未和Persistence Context建立关联的对象。
    Managed:持久化受管对象,有id值,已经和Persistence Context建立了关联的对象。
    Datached:游离态离线对象,有id值,但没有和Persistence Context建立关联的对象。
    Removed:删除的对象,有id值,尚且和Persistence Context有关联,但是已经准备好从数据库中删除
    当从数据库获取的数据后,因为有事务管理,所以数据已与session关联,并且数据库有数据,已经持久化了,并且在数据库的缓存当中了,当我们对查询出来的数据进行修改时,缓存缓存Session中的数据发生改变,那么接着数据库也会跟着进行相应的改变。所以就自动执行了update的更新操作。

    如上所述,我们的持久化状态就是Managed状态,也就是在一个事务中查询出来的实体对象,他有一个不一样的地方就是,就算你不进行save方法操作,在提交事务的时候,他也会去更新数据库,因为他与PersistenceContext对象进行了关联,这个关联就是上面的源码中所写的

    我们接着看源码,最终的乐观锁报错会走到这个方法

    //杨乐乐源码分析  https://juejin.cn/user/4486440898279832
    protected void entityIsDetached(MergeEvent event, Map copyCache) {
        log.trace("merging detached instance");
       ....
           if (this.isVersionChanged(entity, source, persister, target)) {
                if (source.getFactory().getStatistics().isStatisticsEnabled()) {
                    source.getFactory().getStatisticsImplementor().optimisticFailure(entityName);
                }
                //这里就是前面报错信息中的  StaleObjectStateException异常
                throw new StaleObjectStateException(entityName, id);
            }
    

    然后我们看看他的判断条件this.isVersionChanged(entity, source, persister, target)

    //杨乐乐源码分析  https://juejin.cn/user/4486440898279832
    private boolean isVersionChanged(Object entity, EventSource source, EntityPersister persister, Object target) {
            //1 判断实体中有没有version字段,如果没有不回触发乐观锁
            if (!persister.isVersioned()) {
                return false;
            } else {
                //2 判断事务中要保存的实体和数据库中的实体的version是否一致,不一致就会触发乐观锁
                boolean changed = !persister.getVersionType().isSame(persister.getVersion(target, source.getEntityMode()), persister.getVersion(entity, source.getEntityMode()), source.getEntityMode());
                return changed && this.existsInDatabase(target, source, persister);
            }
        }
    

    这段代码就是乐观锁实现的核心判断,这里面就解答了乐观锁的实现是代码实现还是数据库中实现,也和乐观锁的version属性控制的概念向吻合。

    但是这里还有一点要注意,就是如果保存的实体是持久化的状态是不回触发乐观锁的,为什么呢,我们看到这段代码的比较

    //杨乐乐源码分析  https://juejin.cn/user/4486440898279832
    boolean changed = !persister.getVersionType().isSame(persister.getVersion(target, source.getEntityMode()), persister.getVersion(entity, source.getEntityMode()), source.getEntityMode());
    

    实际上就是target实体和entity实体中verson的比较,我们理解entity实体是我们要保存到数据库中的对象,target就是数据库中的的实体数据。其实这样理解是错误的。

    通过对target对象的获取,我们可以分析到,实际上他的赋值逻辑是,如果在PersistenceContext对象,能找到对应的实体,那么他就不回去查数据库,那么这时他的version就是不准确,也就是会和entity对象中verison一样,是不回触发乐观锁的。

    三 结语

    技术的成长离不开总结与复盘,也离不开抽丝剥茧的分析,希望这篇文章在你的技术成长上能带来一些帮助,我是散播欢笑散播爱的杨乐乐,在技术的道路上期待和大家一起成长。

    相关文章

      网友评论

          本文标题:数据库:带你彻底了解乐观锁的实现

          本文链接:https://www.haomeiwen.com/subject/aquirltx.html