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

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

本文目录结构如下:

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

1. Mybatis缓存机制整体设计

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

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

 

 

 2.  二级缓存的工作模式

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

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

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

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

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

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

 

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

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

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

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

    虽然在Mapper中配置了<cache>,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在<select> 节点中配置useCache="true",Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询,不会经过Cache缓存。如下所示,Select语句配置了useCache="true",则表明这条Select语句的查询会使用二级缓存。

<select id="selectByPrimaryKey" resultMap="BaseResultMap" useCache="true">

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;
 
/**
 * <p>
 *    使用Redis实现Mybatis二级缓存
 * </p>
 * @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<String, Object> 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<String> 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<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.dbSize();
            }
        });
        return size.intValue();
    }
 
    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="com.sunnada.hurd.dictionary.dao.CertificationTypeMapper">
    <!--<resultMap id="DemoResultMap" type="com.ssm.demo.pojo.Demo">
        <id column="id" jdbcType="INT" property="id" />
        <result column="name" jdbcType="VARCHAR" property="name" />
    </resultMap>-->
 
    <cache type="com.ssm.demo.MybatisRedisCache">
        <property name="eviction" value="LRU" />
        <property name="flushInterval" value="6000000" />
        <property name="size" value="1024" />
        <property name="readOnly" value="false" />
    </cache>
 
    <select id="get" parameterType="_int" resultType="CertificationType">
        select * from DIS_CERTIFICATION_TYPE where ID= #{id} and STATUS = 1
    </select>
 
</mapper>

 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值