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

960 阅读6分钟

一 乐观锁概念

乐观锁是什么?他是干什么用的,首先乐观锁是用来,控制数据准确性,比如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一样,是不回触发乐观锁的。

三 结语

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