Mybatis缓存解析

Mybatis缓存解析

1. 概述

在系统代码的运行中,我们可能会在一个数据库会话中,执行多次查询条件完全相同的Sql,鉴于日常应用的大部分场景都是读多写少,这重复的查询会带来一定的网络开销,同时select查询的量比较大的话,对数据库的性能是有比较大的影响的。
如果是Mysql数据库的话,在服务端和Jdbc端都开启预编译支持的话,可以在本地JVM端缓存Statement,可以在Mysql服务端直接执行Sql,省去编译Sql的步骤,但也无法避免和数据库之间的重复交互。
在默认情况下,mybatis 的一级缓存是默认开启的。类似于hibernate, 所谓一级缓存,也就是基于同一个sqlsession 的查询语句,即 session 级别的缓存,非全局缓存,或者非二级缓存.
如果要实现 mybatis 的二级缓存,一般来说有如下两种方式:
(1)采用 mybatis 内置的 cache 机制。
(2)采用三方 cache 框架, 比如ehcache, oscache 等等.

2. 一级缓存

Mybatis提供了一级缓存的方案来优化在数据库会话间重复查询的问题。实现的方式是每一个SqlSession中都持有了自己的缓存,一种是SESSION级别,即在一个Mybatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个statement有效。如果用一张图来代表一级查询的查询过程的话,可以用下图表示。
这里写图片描述
每一个SqlSession中持有了自己的Executor,每一个Executor中有一个Local Cache。当用户发起查询时,Mybatis会根据当前执行的MappedStatement生成一个key,去Local Cache中查询,如果缓存命中的话,返回。如果缓存没有命中的话,则写入Local Cache,最后返回结果给用户。

2.1 一级缓存配置

上文介绍了一级缓存的实现方式,解决了什么问题。在这个章节,我们学习如何使用Mybatis的一级缓存。只需要在Mybatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别。

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

接下来我们就用简单的示例来测试下一级缓存。
示例1,

    public static void main(String[] args) {
        SqlSession session = MybatisUtils.getSqlSession();

        RoleMapper mapper = session.getMapper(RoleMapper.class);

        System.out.println(mapper.findRoleById(1L).getRoleName());
        System.out.println(mapper.findRoleById(1L).getRoleName());
        System.out.println(mapper.findRoleById(1L).getRoleName());
    }

我们看下执行的日志,如下图:
这里写图片描述
我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。
示例2,
在这次的试验中,我们增加了对数据库的修改操作,验证在一次数据库会话中,对数据库发生了修改操作,一级缓存是否会失效。如下代码:

    public static void main(String[] args) {
        SqlSession session = MybatisUtils.getSqlSession();

        RoleMapper mapper = session.getMapper(RoleMapper.class);

        Role role = new Role();
        role.setRoleName("cache1");
        role.setNote("cache1");
        System.out.println(mapper.findRoleById(1L).getRoleName());
        mapper.insert(role);
        System.out.println(mapper.findRoleById(1L).getRoleName());
    }

再看下执行日志:
这里写图片描述
我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。
示例3,开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。

    public static void main(String[] args) {
        SqlSession session1 = MybatisUtils.getSqlSession();
        SqlSession session2 = MybatisUtils.getSqlSession();
        RoleMapper mapper1 = session1.getMapper(RoleMapper.class);
        RoleMapper mapper2 = session2.getMapper(RoleMapper.class);
        Role role = new Role();
        role.setRoleName("update");
        role.setNote("update");
        role.setId(1L);
        System.out.println(mapper1.findRoleById(1L).getRoleName());
        System.out.println(mapper1.findRoleById(1L).getRoleName());
        mapper2.update(role);
        System.out.println(mapper1.findRoleById(1L).getRoleName());
        System.out.println(mapper2.findRoleById(1L).getRoleName());
    }

继续看下执行日志:
这里写图片描述
我们可以看到,sqlSession2插入了一个新的role,但session1之后的查询中,id为1的role的名字test1,出现了脏数据,也证明了我们之前就得到的结论,一级缓存只存在于只在数据库会话内部共享。

2.2 一级缓存工作流程&源码分析

根据一级缓存的工作流程,我们绘制出一级缓存执行的时序图,如下图所示。
这里写图片描述
主要步骤如下:
(1)对于某个Select Statement,根据该Statement生成key。
(2)判断在Local Cache中,该key是否用对应的数据存在。
(3)如果命中,则跳过查询数据库,继续往下走。
(4)如果没命中:
去数据库中查询数据,得到查询结果;
将key和查询到的结果作为key和value,放入Local Cache中。
将查询结果返回;
(5)判断缓存级别是否为STATEMENT级别,如果是的话,清空本地缓存。
了解具体的工作流程后,我们队Mybatis查询相关的核心类和一级缓存的源码进行走读。这对于之后学习二级缓存时也有帮助。
SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。它的一个默认实现类是DefaultSqlSession。
如下是接口SqlSession的内部方法:
这里写图片描述
Executor: SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。如下是Executor的结构:
这里写图片描述
如下图所示,Executor有若干个实现类,为Executor赋予了不同的能力,大家可以根据类名,自行私下学习每个类的基本作用。
这里写图片描述
这里写图片描述
在一级缓存章节,我们主要学习BaseExecutor。
BaseExecutor: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。如下图所示:
这里写图片描述
在一级缓存的介绍中,我们提到对Local Cache的查询和写入是在Executor内部完成的。在阅读BaseExecutor的代码后,我们也发现Local Cache就是它内部的一个成员变量,如下代码所示。
这里写图片描述
从上图可以看到这个LocalCache的成员变量是PerpetualCache的一个实例,而PerpetualCache是实现了Cache接口。提供了和缓存相关的最基本的操作,有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力。我们看下Cache的内部结构,如下图:
这里写图片描述
下图就是mybatis中cache的一些实现:
这里写图片描述
BaseExecutor成员变量之一的PerpetualCache,就是对Cache接口最基本的实现,其实现非常的简内部持有了hashmap,对一级缓存的操作其实就是对这个hashmap的操作。如下代码所示。
这里写图片描述
在阅读相关核心类代码后,从源代码层面对一级缓存工作中涉及到的相关代码,出于篇幅的考虑,对源码做适当删减,读者朋友可以结合本文,后续进行更详细的学习。
为了执行和数据库的交互,首先会通过DefaultSqlSessionFactory开启一个SqlSession,在创建SqlSession的过程中,会通过Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数,如下图所示。
这里写图片描述
如果用户不进行制定的话,Configuration在创建Executor时,默认创建的类型就是SimpleExecutor,它是一个简单的执行类,只是单纯执行Sql。以下是具体用来创建的代码。如下代码:

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

在SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的selectList,代码如下所示。

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在上文的代码中,SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。代码如下所示。

  @Override
  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,进入该方法查看CacheKey是如何生成的,代码如下所示。

  @Override
  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;
  }

在上述的代码中,我们可以看到它将MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的参数传入了CacheKey这个类,最终生成了CacheKey。我们看一下这个类的结构。如下代码:

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;
  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  private List<Object> updateList;
  public CacheKey() {
      this.hashcode = DEFAULT_HASHCODE;
      this.multiplier = DEFAULT_MULTIPLYER;
      this.count = 0;
      this.updateList = new ArrayList<Object>();
  }

首先是它的成员变量和构造函数,有一个初始的hachcode和乘数,同时维护了一个内部的updatelist。在CacheKey的update方法中,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中。如下代码所示。

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

我们是如何判断CacheKey相等的呢,在CacheKey的equals方法中给了我们答案,代码如下所示。

  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

除去hashcode,checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条Sql的下列五个值相同,即可以认为是相同的Sql。

Statement Id + Offset + Limmit + Sql + Params

回到BaseExecutor的query方法继续往下走,代码如下所示。

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

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。
在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。
在源码分析的最后,我们确认一下,如果是insert/delete/update方法,缓存就会刷新的原因。
SqlSession的insert方法和delete方法,都会统一走update的流程,update方法也是委托给了Executor执行。BaseExecutor的执行方法如下所示。。

  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前都会清空localCache。至此,一级缓存的工作流程讲解以及源码分析完毕。
总结
(1)Mybatis一级缓存的生命周期和SqlSession一致。
(2)Mybatis的缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念,同时只是使用了默认的hashmap,也没有做容量上的限定。
(3)Mybatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,有操作数据库写的话,会引起脏数据,建议是把一级(4)缓存的默认级别设定为Statement,即不使用一级缓存。

3. 二级缓存

3.1 二级缓存介绍

在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,那么如何让多个SqlSession之间也可以共享缓存呢,答案是二级缓存。
当开启二级缓存后,会使用CachingExecutor装饰Executor,在进入后续执行前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
这里写图片描述
在二级缓存的使用中,一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存是被多个SqlSession共享着的,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

3.2 二级缓存的配置

要正确的使用二级缓存,需完成如下配置的。
(1) 在Mybatis的配置文件中开启二级缓存。

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

(2) 在Mybatis的映射XML中配置cache或者 cache-ref 。

<cache/>   

cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。
(1)type: cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
(2)eviction: 定义回收的策略,常见的有FIFO,LRU。
(3)flushInterval: 配置一定时间自动刷新缓存,单位是毫秒
(4)size: 最多缓存对象的个数
(5)readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
(6)blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

<cache-ref namespace="mapper.RoleMapper"/>

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。

3.3 简单示例

示例1,测试二级缓存效果,不提交事务,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。

        SqlSession session1 = MybatisUtils.getSqlSession();
        SqlSession session2 = MybatisUtils.getSqlSession();
        RoleMapper mapper1 = session1.getMapper(RoleMapper.class);
        RoleMapper mapper2 = session2.getMapper(RoleMapper.class);
        System.out.println("mapper1读取数据:"+mapper1.findRoleById(1L).getRoleName());
        System.out.println("mapper2读取数据:"+mapper2.findRoleById(1L).getRoleName());

我们看下执行日志,如下图所示:
这里写图片描述
我们看到当sqlsession没有调用commit()方法时,二级缓存并没有起到作用。
示例2,测试二级缓存效果,当提交事务时,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。

        SqlSession session1 = MybatisUtils.getSqlSession();
        SqlSession session2 = MybatisUtils.getSqlSession();
        RoleMapper mapper1 = session1.getMapper(RoleMapper.class);
        RoleMapper mapper2 = session2.getMapper(RoleMapper.class);
        System.out.println("mapper1读取数据:"+mapper1.findRoleById(1L).getRoleName());
        session1.commit();
        System.out.println("mapper2读取数据:"+mapper2.findRoleById(1L).getRoleName());

3.4 二级缓存源码分析

Mybatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor装饰了BaseExecutor的子类,实现了缓存的查询和写入功能,所以二级缓存直接从源码开始分析。
源码分析从CachingExecutor的query方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细讲解,可以在文后留言,我会在交流环节更详细的表示出来。
CachingExecutor的query方法,首先会从MappedStatement中获得在配置初始化时赋予的cache。

  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.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

本质上是装饰器模式的使用,具体的执行链是
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。
(1)SynchronizedCache: 同步Cache,实现比较简单,直接使用synchronized修饰方法。
(2)LoggingCache: 日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
(3)SerializedCache: 序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
(4)LruCache: 采用了Lru算法的Cache实现,移除最近最少使用的key/value。
(5)PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
然后是判断是否需要刷新缓存,在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示。

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
  }

Mybatis的CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm。TransactionalCacheManager中持有了一个Map,代码如下所示。
这里写图片描述
这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示。

  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

继续回到CachingExecutor中的query方法继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。之后会尝试从tcm中获取缓存的列表。在getObject方法中,会把获取值的职责一路向后传,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中,以下是此方法的代码:

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

从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下所示。

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

会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache。

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

看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示。

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

在flushPendingEntries中,就把待提交的Map循环后,委托给包装的Cache类,进行putObject的操作。后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutor的update方法,其中调用了这个函数,代码如下所示,因此不再赘述。

private void flushCacheIfRequired(MappedStatement ms)

3.5 总结

Mybatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到Mapper级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
Mybatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用的条件比较苛刻。
在分布式环境下,由于默认的Mybatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将Mybatis的Cache接口实现,有一定的开发成本,不如直接用Redis,Memcache实现业务上的缓存就好了。

4. 实现自定义缓存

除了系统定义的缓存外,你也可以通过实现你自己的缓存或为其他第三方缓存方案 创建适配器来完全覆盖缓存行为,比如比较流行的Redis、Memcache等。要实现自定义缓存我们需要实现mybatis提供的Cache接口,如下图所示是Cache的结构
这里写图片描述
在我们实现了接口后可以用以下方式使用我们自定义的缓存:

<cache type="com.domain.something.MyCustomCache"/>

要配置你的缓存, 简单和公有的 JavaBeans 属性来配置你的缓存实现, 而且是通过 cache 元素来传递属性, 比如, 下面代码会在你的缓存实现中调用一个称为 “setCacheFile(String file)” 的方法:

<cache type="com.yhl.mybatis.cache.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

猜你喜欢

转载自blog.csdn.net/uftjtt/article/details/80265214