《深入理解Mybatis原理》 05-Mybatis二级缓存详解

      MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。本文将全面分析MyBatis的二级缓存的设计原理。

本文目录结构如下:

  • Mybatis缓存机制整体设计
  • 二级缓存的工作模式
  • 使用二级缓存必须要具备的条件
  • Mybatis二级缓存设计分析

1. Mybatis缓存机制整体设计

《深入理解Mybatis原理》 05-Mybatis二级缓存详解_第1张图片

      如上图所示,当开启一个会话时,SqlSession对象首先会到CachingExecutor中进行判断二级缓存中是否可以命中数据(当Mybatis没有使用二级缓存时,也会首先到CachingExecutor中,只不过if 判断不通过,不会执行二级缓存流程),若二级缓存命中数据,则直接返回结果。否则继续到BaseExecutor中执行流程,在BaseExecutor中首先会判断 一级缓存LocalCache是否可以命中数据,若命中则直接返回结果,否则到数据库中获取数据,然后将数据库中获取的数据缓存到一级缓存LocalCache中。若开启了二级缓存,还会将数据库中回去的数据缓存到二级缓存 TransactionalCacheManager中。

   CachingExecutorExecutor的装饰者,使用到设计模式中的装饰者模式。用于增强Executor,使用其具备缓存功能。

 

《深入理解Mybatis原理》 05-Mybatis二级缓存详解_第2张图片

 

 2.  二级缓存的工作模式

《深入理解Mybatis原理》 05-Mybatis二级缓存详解_第3张图片

如上图所示:MyBatis中的Cache以SPI实现,给需要集成其它Cache或者自定义Cache提供了接口。 

Mybatis二级缓存也不例外,但是二级缓存的嵌套(装饰)比较深。

  // CachingExecutor query(二级缓存入口)
  @Override
  public  List 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")
        //从二级缓存  TransactionalCacheManeger中查找缓存
        List list = (List) 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);
  }

    如上图 Mybatis二级缓存入口代码为例:

     缓存的获取通过Mybatis的一个工具TransactionalCacheManager来取出。实际的缓存K/V存放数据结构非常复杂。Key(参见CacheKey)与一级缓存一致, V层次较深, 也大量使用到了包装器模式, 包装层次为:

《深入理解Mybatis原理》 05-Mybatis二级缓存详解_第4张图片

     如上如所示:二级缓存走向较为复杂,但是上图中所有的类(除HashMap外)都实现了Cache接口,每个XXXCache都有各自的职责,通过装饰走向的方式增加二级缓存Cache.

 

3. 使用二级缓存必须要具备的条件

     Mybatis默认开启会话级别一级缓存,但是二级缓存是关闭的。如果想要使用Mybatis的二级缓存,需要经过以下几个配置:

1. XML配置文件中开启二级缓存 全局配置变量参数  cacheEnabled=true
2. 在Mapper XML 中添加 标签开启Mapper Cache
3. 在select 语句上添加 useCache = true

     MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。

    虽然在Mapper中配置了,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在

4. Mybatis二级缓存设计分析

     Mybatis将二级缓存的粒度控制的非常小,使得缓存的使用更加通用,足以满足局部查询数据缓存。当我们尝试在项目做数据缓存时,没有必要一上来直接上Redis (即使Redis性能很好(相对情况下),但是仅仅为了局部数据缓存就在项目中引入Redis也是不太合理的。),可以首先考虑使用Mybatis的二级缓存。

    使用Mybatis二级缓存注意事项:

        1. 只能在【只有单表操作】的表上使用缓存 (多表可能会出现脏数据)

            不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。

        2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存

       

    当然,使用Mybatis二级缓存也可以考虑使用第三方提供的缓存组件,比如: ehcache等.

    也可以自己设计一个Cache,比如借用项目中已有的Redis进行缓存数据,自定义Cache 只需要实现Cache接口即可,然后配置自定义Cache即可使用,下面是笔者自己使用Redis实现的Cache,仅供参考:

   

package com.ssm.demo;
 
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
 
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * 

* 使用Redis实现Mybatis二级缓存 *

* @author: chengxiaonan **/ public class MybatisRedisCache implements Cache { //private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class); // 读写锁 private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); private RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate"); private String id; public MybatisRedisCache(final String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } //logger.info("Redis Cache id " + id); this.id = id; } @Override public String getId() { return this.id; } @Override public void putObject(Object key, Object value) { if (value != null) { // 向Redis中添加数据,有效时间是2天 redisTemplate.opsForValue().set(key.toString(), value, 2, TimeUnit.DAYS); } } @Override public Object getObject(Object key) { try { if (key != null) { Object obj = redisTemplate.opsForValue().get(key.toString()); return obj; } } catch (Exception e) { //logger.error("redis "); } return null; } @Override public Object removeObject(Object key) { try { if (key != null) { redisTemplate.delete(key.toString()); } } catch (Exception e) { } return null; } @Override public void clear() { //logger.debug("清空缓存"); try { Set keys = redisTemplate.keys("*:" + this.id + "*"); if (!CollectionUtils.isEmpty(keys)) { redisTemplate.delete(keys); } } catch (Exception e) { } } @Override public int getSize() { Long size = (Long) redisTemplate.execute(new RedisCallback() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.dbSize(); } }); return size.intValue(); } @Override public ReadWriteLock getReadWriteLock() { return this.readWriteLock; } }


 

    
 
    
        
        
        
        
    
 
    
 

 

你可能感兴趣的:(深入理解Mybatis原理)