Mybatis二级缓存原理

gugumomo65 4年前
   <p>记录是一种精神,是加深理解最好的方式之一。</p>    <p>最近看了下Mybatis的源码,分析了二级缓存的实现方式,在这里把他记下来。虽然这不复杂,对这方面的博客也有很多,写的也很好。但我坚信看懂了是其一,能够教别人或者描述清楚记下来才能真正的掌握。</p>    <p>这篇文章能够帮你</p>    <ul>     <li>学会对Mybatis配置二级缓存</li>     <li>学会Mybatis二级缓存的实现方式</li>     <li>学会整合外部缓存框架(如: <a href="/misc/goto?guid=4958983962471963195" rel="nofollow,noindex"> <em>Ehcache</em> </a> )</li>     <li>学会自定义二级缓存</li>    </ul>    <h2>1. Mybatis内部二级缓存的配置</h2>    <p>要使用Mybatis的二级缓存,需要对Mybatis进行配置,配置分三步</p>    <ul>     <li>Mybatis全局配置中启用二级缓存配置 <pre>  <code class="language-java"><setting name="cacheEnabled" value="true"/></code></pre> </li>     <li>在对应的Mapper.xml中配置cache节点 <pre>  <code class="language-java"><mapper namespace="userMapper">    <cache />    <result ... />    <select ... />  </mapper></code></pre> </li>     <li> <p>在对应的select查询节点中添加useCache=true</p> <pre>  <code class="language-java"><select id="findUserById" parameterType="int" resultMap="user" useCache="true">    select * from users where id=#{id};  </select></code></pre> </li>     <li> <p>高级配置</p> <p>a. 为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置)</p> <p>b. 多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置)</p> </li>    </ul>    <p>只要简单的三步配置即可开启Mybatis的二级缓存了。在使用mybatis查询时候("userMapper.findUserById"),不同会话(Sqlsession)在查询时候,只会查询数据库一次,第二次会从二级缓存中读取。</p>    <pre>  <code class="language-java">@Before  public void before() {      String mybatisConfigFile = "MybatisConfig/Mybatis-conf.xml";      InputStream stream = TestMybatis.class.getClassLoader().getResourceAsStream(mybatisConfigFile);      sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream); //构建sqlSession的工厂  }  @Test  public void test() {      SqlSession sqlSession = sqlSessionFactory.openSession();      User i = sqlSession.selectOne("userMapper.findUserById", 1);      System.out.println(i);      sqlSession.close();      sqlSession = sqlSessionFactory.openSession();      User x = sqlSession.selectOne("userMapper.findUserById", 1); // 读取二级缓存数据      System.out.println(x);      sqlSession.close();  }</code></pre>    <h2>2. Mybatis内部二级缓存的设计及工作模式</h2>    <p><img src="https://simg.open-open.com/show/904359263e5cc37883010ca1fe2cbd57.jpg"></p>    <p>首先我们要知道,mybatis的二级缓存是通过CacheExecutor实现的。CacheExecutor其实是Executor的代理对象。所有的查询操作,在CacheExecutor中都会先匹配缓存中是否存在,不存在则查询数据库。</p>    <h2>3. 内部二级缓存的实现详解</h2>    <p>竟然知道Mybatis二级缓存是通过CacheExecotur实现的,那看下Mybatis中创建Executor的过程</p>    <pre>  <code class="language-java">// 创建执行器(Configuration.newExecutor)  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {     //确保ExecutorType不为空(defaultExecutorType有可能为空)     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的Cache包装类对象        executor = new CachingExecutor(executor);     }     executor = (Executor) interceptorChain.pluginAll(executor);     return executor;  }</code></pre>    <p>重点在cacheEnabled这个参数。如果你看了我的文章[ <a href="/misc/goto?guid=4959722775693302097" rel="nofollow,noindex">Mybatis配置文件解析过程详解</a> ],就应该知道了怎么设置cacheEnabled。对,就是此文章第一点说的开启Mybatis的全局配置项。我们继续看下CachingExecutor具体怎么实现的。</p>    <pre>  <code class="language-java">public class CachingExecutor implements Executor {      private Executor delegate;      public CachingExecutor(Executor delegate) {          this.delegate = delegate;          delegate.setExecutorWrapper(this);      }      public int update(MappedStatement ms, Object parameterObject) throws SQLException {          flushCacheIfRequired(ms); //是否需要更缓存          return delegate.update(ms, parameterObject);  //更新数据      }      ......  }</code></pre>    <p>很清晰,静态代理模式。在CachingExecutor的所有操作都是通过调用内部的delegate对象执行的。缓存只应用于查询,我们看下CachingExecutor的query方法。</p>    <pre>  <code class="language-java">public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {      BoundSql boundSql = ms.getBoundSql(parameterObject);      //创建缓存值      CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);      //获取记录      return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  }    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              if (ms.getStatementType() == StatementType.CALLABLE) {                  for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {                      if (parameterMapping.getMode() != ParameterMode.IN) {                          throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");                      }                  }              }              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); // 结果保存到缓存中              }              return list;          }      }      return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  }</code></pre>    <p>如果MappedStatement中对应的Cache存在,并且对于的查询开启了二级缓存(useCache="true"),那么在CachingExecutor中会先从缓存中根据CacheKey获取数据,如果缓存中不存在则从数据库获取。这里的代码很简单,很容易理解。</p>    <p>说到缓存,有效期和缓存策略不得不提。在Mybatis中二级缓存也实现了有效期的控制和缓存策略。Mybatis中是使用责任链模式实现的,具体可以看下mybatis的cache包</p>    <p><img src="https://simg.open-open.com/show/d5dcf1cc6e9c24f5fe46c9dc48d4b5a8.png"></p>    <p>具体于配置如下:</p>    <pre>  <code class="language-java"><cache eviction="FIFO|LRU|SOFT|WEAK" flushInterval="300" size="100" /></code></pre>    <p>对应具体实现源码可以参考CacheBuilder类的源码。</p>    <pre>  <code class="language-java">public Cache build() {      if (implementation == null) { //缓存实现类          implementation = PerpetualCache.class;          if (decorators.size() == 0) {              decorators.add(LruCache.class);          }      }      Cache cache = newBaseCacheInstance(implementation, id);      setCacheProperties(cache);      if (PerpetualCache.class.equals(cache.getClass())) {          for (Class<? extends Cache> decorator : decorators) {              cache = newCacheDecoratorInstance(decorator, cache);              setCacheProperties(cache);          }          // 采用默认缓存包装类          cache = setStandardDecorators(cache);      } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {          cache = new LoggingCache(cache);      }      return cache;  }</code></pre>    <h2>4. 一级缓存和二级缓存的使用顺序</h2>    <p>如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:</p>    <p>二级缓存 ———> 一级缓存——> 数据库</p>    <h2>5. mybatis二级缓存和分页插件同时使用产生的问题</h2>    <p>问题:分页插件开启二级缓存后,分页查询时无论查询哪一页都返回第一页的数据</p>    <p>在之前讲解Mybatis的执行流程的时候提到,在开启cache的前提下,Mybatis的executor会先从缓存里读取数据,读取不到才去数据库查询。问题就出在这里,sql自动生成插件和分页插件执行的时机是在statementhandler里,而statementhandler是在executor之后执行的,无论sql自动生成插件和分页插件都是通过改写sql来实现的,executor在生成读取cache的key(key由sql以及对应的参数值构成)时使用都是原始的sql,这样当然就出问题了。</p>    <p>找到问题的原因后,解决起来就方便了。只要通过拦截器改写executor里生成key的方法,在生成可以时使用自动生成的sql(对应sql自动生成插件)或加入分页信息(对应分页插件)就可以了。</p>    <p>参考: <a href="/misc/goto?guid=4959722775787130162" rel="nofollow,noindex">http://blog.csdn.net/hupanfeng/article/details/16950161</a></p>    <h2>6. mybatis整合第三方缓存框架</h2>    <p><img src="https://simg.open-open.com/show/f081501932a000ad0d1adf7665ca250a.jpg"></p>    <p>我们以ehcache为例。对于ehcache我只会简单的使用。这里我只是介绍Mybatis怎么使用ehcache,不对ehcache配置作说明。我们知道,在配置二级缓存时候,我们可以指定对应的实现类。这里需要mybatis-ehcache-1.0.3.jar这个jar包。在Mapper中我们只要配置如下即可。</p>    <pre>  <code class="language-java"><cache type="org.mybatis.caches.ehcache.EhcacheCache"/></code></pre>    <p>当然,项目中ehcache的配置还是需要的。</p>    <h2>小结</h2>    <p>对于Mybatis整合第三方的缓存,实现骑士很简单,只要在配置的地方制定实现类即可。</p>    <p>Mybatis默认二级缓存的实现在集群或者分布式部署下是有问题的,Mybatis默认缓存只在当节点内有效,并且对缓存的失效操作无法同步的其他节点。需要整合第三方分布式缓存实现,如ehcache或者自定义实现。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/5ff874fa696f</p>    <p> </p>