mybatis之一级、二级缓存

缓存体系结构

        缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

        MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。

但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)。

所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

 

缓存实现类描述作用装饰条件
基本缓存缓存基本实现类默认是 PerpetualCache,也可以自定义比如
RedisCache、EhCache 等,具备基本功能的缓存类
LruCacheLRU策略的缓存当缓存达到上限时,删除最近最少使用的缓存eviction="LRU"
FifoCacheFIFO策略的缓存当缓存达到上限时,删除最先保存的缓存eviction="FIFO"
SoftCache
WeakCache
带清理策略的缓存通过 JVM 的软引用和弱引用来实现缓存,当 JVM内存不足时,会自动清理掉这些缓存,基于SoftReference 和WeakReferenceeviction="SOFT"
eviction="WEAK"
LoggingCache带日志功能的缓存比如:输出缓存命中率 基本
SynchronizedCache同步缓存基于 synchronized 关键字实现,解决并发问题基本
BlockingCache阻塞缓存通过在 get/put 方式中加锁,保证只有一个线程操作缓存,基于 Java 重入锁实现blocking=true
SerializedCache支持序列化的缓存将对象序列化以后存到缓存中,取出时反序列化readOnly=false(默
认)
ScheduledCache定时调度的缓存在进行 get/put/remove/getSize 等操作前,判断
缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每隔一段时间清空一次缓存
flushInterval 不为空
TransactionalCache事务缓存 在二级缓存中使用,可一次存入多个缓存,移除多个缓存在TransactionalCach
eManager 中用 Map
维护对应关系

 

一级缓存

        一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

        DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在 Executor 里面维护——SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中持有了 PerpetualCache。

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

我们接下来看下源码中是如何存入和取出的:

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);
  }

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();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }


public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }


private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

在查询之前,先创建CacheKey,然后通过CacheKey从localCache中找,如果没找到则从数据库中查询,然后再存入localCache中。

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);
  }

更新时会清空缓存。。。

因为一级缓存是不可以跨会话共享的,所以如果在会话1缓存了,会话2更新的话,会导致会话1查询到脏数据

二级缓存

        二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。

       既然是跨会话共享,那么肯定是在SqlSession的外层,通过源码我们可以看出,是通过包装类CachingExecutor包装了创建的Executor来实现的(cacheEnabled默认为true):

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    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);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

开启二级缓存的方法:

1.在 mybatis-config.xml 中配置了(可以不配置,默认是 true)

<setting name="cacheEnabled" value="true"/>

2.在 Mapper.xml 中配置<cache/>标签:

<!-- 声明这个 namespace 使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" <!—最多缓存对象个数,默认 1024--> eviction="LRU" <!—回收策略--> flushInterval="120000" <!—自动刷新时间 ms,未配置时只有调用时刷新--> readOnly="false"/> <!— 默认是  false(安全),改为 true 可读写时,对象必须支持序列化 -->

cache 属性详解:

属性含义取值
type缓存实现类需要实现 Cache 接口,默认是PerpetualCache
size最多缓存对象个数默认 1024
eviction回收策略(缓存淘汰算法)

LRU – 最近最少使用的:移除最长时间不被使用的对象(默认)。

FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

flushInterval定时自动清除缓存间隔自动刷新时间,单位 ms,未配置时只有调用时刷新
readOnly是否只读true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是 false。
改为 false 可读写时,对象必须支持序列化。
blocking是否使用可重入锁实现
缓存的并发控制
true,会使用 BlockingCache 对 Cache 进行装饰
默认 false

Mapper.xml 配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。

只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置<cache>,决定了在启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor  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, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?
我们可以在单个 Statement ID 上显式关闭二级缓存(默认是 true):

<select id="selectBlog"  resultMap="BaseResultMap"  useCache="false"/>

这里需要注意一下:如果不提交事务,二级缓存是会失效的,原因:CachingExecutor只有一个TransactionCacheManager实例tcm,在tcm.putObject方法时,只是将数据放入tcm下的transactionalCaches属性中:

public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

而transactionalCaches的putObject的方法将数据放入entriesToAddOnCommit属性中:

public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

而只有调用CachingExecutor的commit方法才放入到Cache中:

public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

tcm的commit:

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

TransactionalCache的commit方法,将entriesToAddOnCommit数据提交:

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

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

最终调用了PurpetualCache的putObject方法存入Cache中:

public Object getObject(Object key) {
    return cache.get(key);
  }

 

CachingExcutor获取二级缓存:

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, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

会先从tcm中是否能获取到缓存,如果没有则从数据库中获取。。。

 

第三方缓存做二级缓存:

<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>

Mapper.xml 配置,type 使用 RedisCache:

<cache  type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO"  flushInterval="60000"  size="512"  readOnly ="true"/>

redis.properties 配置:

host= localhost
port= 6379
connectionTimeout= 5000
soTimeout= 5000
database=0 0

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值