MyBatis一级缓存和二级缓存

一级缓存

一级缓存存在于SqlSession的生命周期中。在同一个SqlSession中查询时,执行的方法、参数、结果会封装成一个CacheKey然后放到一个Map中。如果再次执行相同的方法、参数,判断是否同一个CacheKey,如果是,直接返回Map缓存对象中的结果,无需入库。

默认启用。如果要关闭需要配置flushCache。

<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="true">
    select login_id, customer_id, login_time, login_ip, login_type
    from customer_login_log
    where login_id = #{loginId,jdbcType=INTEGER}
</select>

CacheKey的真容:

public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new CacheKey(){
    @Override
    public void update(Object object) {
      throw new CacheException("Not allowed to update a null cache key instance.");
    }
    @Override
    public void updateAll(Object[] objects) {
      throw new CacheException("Not allowed to update a null cache key instance.");
    }
  };

  private static final int DEFAULT_MULTIPLIER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  // 8/21/2017 - Sonarlint flags this as needing to be marked transient.  While true if content is not serializable, this is not always true and thus should not be marked transient.
  private List<Object> updateList;

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

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

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

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

  @Override
  public int hashCode() {
    return hashcode;
  }

  @Override
  public String toString() {
    StringJoiner returnValue = new StringJoiner(":");
    returnValue.add(String.valueOf(hashcode));
    returnValue.add(String.valueOf(checksum));
    updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
    return returnValue.toString();
  }

  @Override
  public CacheKey clone() throws CloneNotSupportedException {
    CacheKey clonedCacheKey = (CacheKey) super.clone();
    clonedCacheKey.updateList = new ArrayList<>(updateList);
    return clonedCacheKey;
  }

}

一级缓存的测试

严格来说,MyBatis根据以下几个因素判断放入一级缓存的CacheKey是否同一个:

  1. MappedStatement的ID
  2. 查询sql的offset
  3. 查询sql的limit
  4. sql语句
  5. 入参(如果查询sql有入参的话)
  6. <environment id="development">
    只要有一个变了,就会生成不同的CacheKey对象。
    对上面这句话的测试:
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select login_id, customer_id, login_time, login_ip, login_type
    from customer_login_log
    where login_id = #{loginId,jdbcType=INTEGER}
</select>

<select id="selectByPrimaryKey2" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select login_id, customer_id, login_time, login_ip, login_type
    from customer_login_log
    where login_id = #{loginId,jdbcType=INTEGER}
</select>
public interface CustomerLoginLogMapper {

    CustomerLoginLog selectByPrimaryKey(Integer loginId);

    CustomerLoginLog selectByPrimaryKey2(Integer loginId);
}
@Test
public void test1(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    System.out.println(sqlSession.getClass());
    try {
        CustomerLoginLogMapper customerLoginLogMapper = sqlSession.getMapper(CustomerLoginLogMapper.class);
        CustomerLoginLog result = customerLoginLogMapper.selectByPrimaryKey(1);
        log.info("{}",result);
        CustomerLoginLog result2 = customerLoginLogMapper.selectByPrimaryKey2(1);
        log.info("{}",result2);  
        sqlSession.commit();
    } finally {
        sqlSession.close();
    }
}

第一次查询和第二次查询调用的Statement不是同一个,其他的例如sql、入参、结果都相同。还是生成了2个CacheKey缓存对象。
在这里插入图片描述

任何的Insert、Update、Delete都会清空一级缓存的证据

首先MyBatis真正执行sql都是通过Executor执行的,结构如图:
在这里插入图片描述

Executor接口里定义了一些公用方法,
BaseExecutor抽象类使用模板方法模式,定义了通用的方法实现。供ReuseExecutor、SimpleExecutor、BatchExecutor调用。
SimpleExecutor是最简单的Executor接口实现,ReuseExecutor和SimpleExecutor的区别是:ReuseExecutor能重用Statement。而BatchExecutor能够批量执行sql。
CachingExecutor使用了装饰模式,给Executor接口实现类封装了二级缓存的功能,稍后专写一篇关于它的介绍。
BaseExecutor的真容如下:

public abstract class BaseExecutor implements Executor {

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

  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }
    
  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
  
  查询方法略
}

可以看到commit、rollback、update方法都会出发clearLocalCache()方法,而insert、delete、update的sql都会走BaseExecutor的update方法。

二级缓存

二级缓存存在于SqlSessionFactory生命周期中,跨SqlSession共享。
二级缓存存在于 SqlSessionFactory 的生命周期中,可以理解为跨sqlSession;缓存是以namespace为单位的,不同namespace下的操作互不影响。
setting参数 cacheEnabled,这个参数是二级缓存的全局开关,默认值是 true,如果把这个参数设置为false, 即使有后面的二级缓存配置,也不会生效;

要开启二级缓存,你需要在你的 SQL 映射文件中添加配置:<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
字面上看就是这样。这个简单语句的效果如下:
1、映射语句文件中的所有 select 语句将会被缓存。
2、映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
3、缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
4、根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
5、缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
6、缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
Tips: 使用二级缓存容易出现脏读, 建议避免使用二级缓存,在业务层使用可控制的缓存代替更好

如何开启二级缓存?

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

mapper文件中:
<cache eviction="FIFO" flushInterval="100000" size="100" 
readOnly="true"></cache>

开启了的mapper文件中的:
1、所有insert、update、delete都会刷新缓存。
2、所有select都会被缓存。

缓存回收默认使用LRU算法。

使用二级缓存容易出现脏读,不建议使用,建议使用其他的缓存组件代替。

二级缓存测试

@Test
public void test1(){
    try {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        System.out.println(sqlSession.getClass());
        CustomerLoginLogMapper customerLoginLogMapper = sqlSession.getMapper(CustomerLoginLogMapper.class);
        CustomerLoginLog result = customerLoginLogMapper.selectByPrimaryKey(1);
        log.info("{}",result);
        CustomerLoginLog result2 = customerLoginLogMapper.selectByPrimaryKey(1);
        log.info("{}",result2);
        sqlSession.commit();
        sqlSession.close();

        sqlSession = sqlSessionFactory.openSession();
        System.out.println(sqlSession.getClass());
        customerLoginLogMapper = sqlSession.getMapper(CustomerLoginLogMapper.class);
        result = customerLoginLogMapper.selectByPrimaryKey(1);
        log.info("{}",result);
        result2 = customerLoginLogMapper.selectByPrimaryKey(1);
        log.info("{}",result2);
        sqlSession.commit();
        sqlSession.close();
    } finally {
    }
}

在这里插入图片描述
开启二级缓存后,查询的时候,先去二级缓存查。
如果二级缓存没查到,再去一级缓存查,一级缓存未查到再查数据库。
如果二级缓存查到了,返回。
第1次查询,命中率0
第2次查询,命中率0/2= 0
第3次查询,命令率1/3 = 0.33
第4次查询,命中率2/4= 0.5

缓存使用示意图

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值