美文网首页
MyBatis缓存

MyBatis缓存

作者: 扎瓦叔叔 | 来源:发表于2019-03-14 12:26 被阅读0次

      myBatis是一个常用的java数据库访问的持久层框架,它支持定制化 SQL、存储过程以及高级映射。

      最近在项目中遇到了mybatis缓存的一些问题,所以在这里整理了下,与大家分享,也欢迎大家一起多交流

    一级缓存

    结构

      缓存这东西,大家都清楚,就是在client执行多次相同的sql的时候,防止穿透,增加响应速度,减少DB压力
      通常我们一次write或者read请求都会new一个SqlSession,在mybatis里,sqlSession是一个接口类,它的实现类DefaultSqlSession,每一个sqlSession持有一个executor,BaseExecutor里持有一份localCache,localCache底层就是使用一个Map<Object,Object>来存储的,那基本流程相信大家也基本猜到了,MyBatis来查询的时候,根据语句生成MappedStatement,然后在localCahe里查询,如果命中直接返回,如果没有命中,则查询数据库,再将结果写入localCache

    Mybatis一级缓存.png

      一级缓存属于SqlSession级别的,一个SqlSession可以理解为一次数据库会话,如果新建一个SqlSession,则之前的缓存不会生效(即一级缓存生效范围在同一个SqlSession内),如果SqlSession执行任何一次update操作,则之前的localCache也会失效

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        clearLocalCache();
        return doUpdate(ms, parameter);
    }
    

    这里update之后,直接clearLocalCache(),即Map.clear()

    如何保证缓存的命中

      既然底层是Map<k,v>来存储的,那么缓存的命中就取决于key值的生成了。

    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
      if (closed) throw new ExecutorException("Executor was closed.");
      CacheKey cacheKey = new CacheKey();
      cacheKey.update(ms.getId());
      cacheKey.update(rowBounds.getOffset());
      cacheKey.update(rowBounds.getLimit());
      cacheKey.update(boundSql.getSql());
      List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
      if (parameterMappings.size() > 0 && parameterObject != null) {
          TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
          if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            cacheKey.update(parameterObject);
          } else {
              MetaObject metaObject = configuration.newMetaObject(parameterObject);
              for (ParameterMapping parameterMapping : parameterMappings) {
              String propertyName = parameterMapping.getProperty();
              if (metaObject.hasGetter(propertyName)) {
                cacheKey.update(metaObject.getValue(propertyName));
              } else if (boundSql.hasAdditionalParameter(propertyName)) {
                cacheKey.update(boundSql.getAdditionalParameter(propertyName));
              }
           }
         }
      }
      return cacheKey;
    }
    

    CacheKey的决定因素
    1、statementId
    2、rowBounds.offset
    3、rowBound.limit
    4、sql语句
    5、sql参数
    这里的update()函数,实际是计算一个参数的hashcode,做一个校验和

    public void update(Object object) {
      int baseHashCode = object == null ? 1 : object.hashCode();
    
      count++;
      checksum += baseHashCode;
      baseHashCode *= count;
    
      hashcode = multiplier * hashcode + baseHashCode;
    
      updateList.add(object);
    }
    

    意义

      一级缓存底层就是使用一个HashMap来实现的缓存,前提是预设该缓存存在的时间很短。其中的问题就是如果长时间使用一个SqlSession,那么这个hashMap的缓存会越来越大,同时缓存的时效性也会越来越弱。虽然Cache提供了缓存失效的接口,但是并不是强制的。任何update操作时也会清空缓存,但也有可能出现DB数据修改了上层没有感知的情况。所以要严格控制SqlSession的生命周期

    流程

    首先通过DefaultSqlSessionFactory.openSession

    private SqlSession openSessionFromDataSource(ExecutorType execType,       TransactionIsolationLevel level, boolean autoCommit) {
          ······
          final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
          return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    这里会new一个Executor,根据executorType来生成不同的Executor的实现类,默认的defaultExecutorType是SIMPLE类型

      public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor, autoCommit);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    

    创建完executor,这里有个cacheEnabled参数,默认为true,会为executor套上一层装饰类,这个与二级缓存有关,之后再讲。

    SqlSession会把具体的职责委托给Executor,只开启一级缓存的时候,委托会给BaseExecutor,看下BaseExecutor的query方法

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    

    生成CacheKey,然后去localCache获取,如之前所说,如果没有命中则去DB里查询

    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
         clearLocalCache(); // issue #482
    }
    

    最后有一条判断,如果localCache是statement级别的,那么直接清空localCache,也就是statement级别的一级缓存是无法共享localCache的

    <setting name="localCacheScope" value="STATEMENT"/>
    

    所以在分布式或者多SqlSession上建议使用statement缓存级别,避免在数据库写入时导致其他session的脏读

    二级缓存

      之前提到一级缓存是SqlSession级别的,而二级缓存的作用域是全局的。一级缓存是默认打开的,而二级缓存需要通过一定的配置(配置这里不具体讲了,大家可以自己看使用文档)

    结构与流程

      上文提到的BaseExecutor的装饰类CacheExecutor,当有请求来的时候,CacheExecutor会先判断二级缓存是否命中,之前看到有一副不错的流程图,这里拿来分享下:


    MyBatis总体流程.png

      在CacheExecutor每次query时,先会去获取Cache,每个CacheExecutor持有一个TransactionalCacheManager对象,而TransactionalCacheManager持有一个Map对象,其中存储了Cache和TransactionalCache的映射关系。

      TransactionalCache是一个实现了Cache接口的包装类,只有在事务提交后缓存才会生效

    查询与写入

    再回来看CacheExecutor的query方法

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
          flushCacheIfRequired(ms);
          if (ms.isUseCache() && resultHandler == null) { 
            ensureNoOutParams(ms, key, parameterObject, boundSql);
            if (!dirty) {
              cache.getReadWriteLock().readLock().lock();
              try {
                @SuppressWarnings("unchecked")
                List<E> cachedList = (List<E>) cache.getObject(key);
                if (cachedList != null) return cachedList;
              } finally {
                cache.getReadWriteLock().readLock().unlock();
              }
            }
            List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
            return list;
          }
        }
        return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    

    如果MappedStatement中对应的Cache存在,并且对于的查询开启了二级缓存(useCache="true"),那么在CachingExecutor中会先从缓存中根据CacheKey获取数据,如果缓存中不存在则从数据库获取。获取后tcm.putObject放入entriesToAddOnCommit(只有当commit的时候才会再次读取生效)

    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
          delegate.putObject(entry.getKey(), entry.getValue());
        }
    }
    

    总结:

      二级缓存很容易出现脏数据,虽然在update时候会通过dirty字段强制更新,但在多表查询中,还是极可能出现脏数据,所以在生产环境还是建议关闭。分布式环境下还是建议自己实现redis等缓存来实现

    相关文章

      网友评论

          本文标题:MyBatis缓存

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