13.mybatis 一级、二级缓存源码解读

课程标题《mybatis框架源码解读一级、二级缓存源码解读》
课程内容:
1.什么是一级、二级缓存
2.一级缓存特点?源码解读、优缺点
3.如何禁止使用一级缓存
4.spring中底层如何使用一级缓存
5.什么是二级缓存?如何开启二级缓存
6.二级缓存的优缺点有哪些

mybatis一级、二级缓存概述

缓存基本越小 查询速度越快、缓存内容越少
缓存基本越大 查询速度越慢 缓存非常多内容

多级缓存概念

之前学习到多级缓存查询方式

先查询一级、一级缓存如果没有
在查询二级 二级缓存没有在查询数据库

在mybatis中反过来

先查询二级、二级如果没有在查询一级、一级如果没有在查询

数据库。

BaseExecutor 属于一级缓存执行器

CachingExecutor 属于二级缓存执行器

缓存 缓存key 、缓存value

1.Mybatis 中有一级缓存和二级缓存,采用装饰设计模式;

2.默认情况下一级缓存是开启的,而且是不能关闭的 ,一级缓存是指 SqlSession 级别的缓存,当在同一个 SqlSession 中进行相同的 SQL 语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存 1024 条 SQL。

3.二级缓存是指可以跨 SqlSession 的缓存。 是 mapper 级别的缓存,对于 mapper 级别的缓存不同的sqlsession 是可以共享的,需要额外整合到第三方缓存 例如Redis、MongoDB、oscache、ehcache等。

一级、二级缓存 采用装饰模式设计封装。

mybatis一级缓存源码解读

一级缓存特点

一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存作用范围只能在同一个SqlSession中,多个不同的SqlSession是无效的。

MyBatis中一级缓存是默认开启的,不需要任何额外配置

一级缓存效果演示

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 1.定义mybatis-config.xml
        String resource = "mybatis-config.xml";
        // 2.读取mybatis-config.xml
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //3. 创建SqlSessionFactoryBuilder.build(inputStream) 建造者  键盘快捷键ctrl+点击方法
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //4.获取到session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        System.out.println("第一次查询:");
        List<UserEntity> userList1 = sqlSession.selectList("com.mayikt.mapper.UserMapper.getByUsers2",
                UserEntity.class);
        System.out.println("userList1:" + userList1);
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        System.out.println("第二次查询:");
        List<UserEntity> userList2 = sqlSession2.selectList("com.mayikt.mapper.UserMapper.getByUsers2",
                UserEntity.class);
        System.out.println("userList2:" + userList2);

如果是在同一个sqlSession 第二次在查询 则会走一级缓存 不会查询db。

通过日志可以得出第二次没有查询db 走的是 一级缓存返回数据:

img

一级缓存源码解读

第一次发出一个查询 sql, sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个Map集合。

key: MapperID+offset+limit+Sql+所有的入参

value:缓存的数据

同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询, 从数据库查询到再写入缓存。

1.默认是开启了二级缓存,则应该将二级缓存关闭

在mayikt-config.xml 设置 关闭二级缓存

    <settings>
        <setting name="cacheEnabled" value="false"/>
        <!-- 打印sql日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

2.执行到org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)

3.执行到org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 拼接缓存key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

CacheKey主要是由以下6部分组成

1、将Statement中的id添加到CacheKey对象中的updateList属性

2、将offset(分页偏移量)添加到CacheKey对象中的updateList属性(如果没有分页则默认0)

3、将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(如果没有分页则默认Integer.MAX_VALUE)

4、将sql语句(包括占位符?)添加到CacheKey对象中的updateList属性

5、循环用户传入的参数,并将每个参数添加到CacheKey对象中的updateList属性

6、如果有配置Environment,则将Environment中的id添加到CacheKey对象中的updateList属性

4.一级缓存核心代码

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是为null 则调用 localCache.getObject(key) 是为一级缓存如果
      // 如果一级缓存不为null 则直接返回一级缓存数据
      // 否则查询db
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      //查询db 
        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;
  }

PerpetualCache 为一级缓存 底层采用Map集合实现

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

一级缓存没有数据 则查询db,如果db中有数据 则将db中数据返回给一级缓存。

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

当我们一级缓存有数据之后 在同一个sqlsession 则会使用一级缓存中数据,不会查询db。

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

img

第二次查询时,判断如果一级缓存有数据 则 list返回数据 就是 从一级缓存中查询的数据

一级缓存优缺点

\1. Mybatis的一级缓存存放在SqlSession的生命周期,在同一个SqlSession中查询时,Mybatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。

如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在改键值时,则会返回缓存中的对象。(一个SqlSession连续两次查询 得到的是同一个java对象)

任何的insert update delete操作都会清空一级缓存(增删改任何记录都会清空当前SqlSession的缓存)。

2.如果服务器集群的时候,每个sqlSession有自己独立的缓存相互之间不存在共享,所以在服务器集群的时候容易产生查询数据冲突问题。

一级缓存依赖jvm–而与jvm没有解耦

如果缓存数据过多的情况下 容易造成内存溢出的问题

如何禁止使用mybatis一级缓存

方案1 在sql语句上 随机生成 不同的参数 存在缺点:map集合可能爆 内存溢出的问题

方案2 开启二级缓存(共享 依赖Redis实现)

方案3 使用sqlSession强制清除缓存

方案4 创建新的sqlSession连接。

2022年7月16日22:04开始

一级缓存在什么时候清除?

1.提交事务的时候

sqlSession.commit();//提交事务 把一级缓存中数据给清除掉的

@Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();// 一级缓存对应的 map集合中数据 清空
      localOutputParameterCache.clear();
    }

2.执行 insert、delete、update语句的时候

Spring整合Mybatis的时候一级缓存的问题

1.在未开启事务的情况之下,每次查询spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有启作用的

2.在开启事务的情况之下,spring使用threadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的

mybatis二级缓存源码解读

mybatis二级缓存特点

1.二级缓存的范围是 mapper 级别( mapper 同一个命名空间), mapper 以命名空间为单位创建缓存数据结构,结构是 map, mybatis 的二级缓存是通过 CacheExecutor 实现的。 CacheExecutor其实是 Executor 的代理对象,所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库,该过程采用装饰模式设计

key: MapperID+offset+limit+Sql+所有的入参

具体使用需要配置:

Mybatis 全局配置中启用二级缓存配置

在对应的 Mapper.xml 中配置 cache 节点

在对应的 select 查询节点中添加 useCache=true

开启mybatis二级缓存

1.mybatis 配置文件 默认开启了二级缓存配置

    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <setting name="cacheEnabled" value="true"/>
    </settings>

2.MybatisRedisCache(先启动Redis)

package com.mayikt.cache;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.mayikt.utils.SerializeUtil;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class MybatisRedisCache implements Cache {
    private static Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);

    private Jedis redisClient = createReids();

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private String id;

    public MybatisRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id);
        this.id = id;
    }


    public String getId() {
        return this.id;
    }


    public int getSize() {

        return Integer.valueOf(redisClient.dbSize().toString());
    }


    public void putObject(Object key, Object value) {
        logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value);
        redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value));
    }

    public Object getObject(Object key) {
        Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
        logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value);
        return value;
    }


    public Object removeObject(Object key) {
        return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
    }


    public void clear() {
        redisClient.flushDB();
    }


    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    protected static Jedis createReids() {
        JedisPool pool = new JedisPool("127.0.0.1", 6379);
        return pool.getResource();
    }
}

3.UserMapper 中配置:

    <cache eviction="LRU" type="com.mayikt.cache.MybatisRedisCache"/>

mybatis源码解读

1、创建一级缓存的CacheKey

2、获取二级缓存

3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。

4、查询到结果之后将结果进行缓存。

1.先执行org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

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

执行到二级缓存org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)

 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 如果开启了二级缓存 则cache不为null
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, 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);
          // 在将一级缓存数据存入到map集合中  
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

二级缓存底层也是采用 HashMap集合实现

/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache;

import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.cache.decorators.TransactionalCache;

/**
 * @author Clinton Begin
 */
public class TransactionalCacheManager {

  private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}
       List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            // 回调到我们自定义的  将数据存入到redis中
            tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;

笔记

@SuppressWarnings("unchecked")
  @Override
  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 {
         // 一级返回如果没有数据 则开始查询db
        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;
  }
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
     // 在查询db之前 该缓存的内容 占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 开始查询db
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 删除占位符
      localCache.removeObject(key);
    }
      // 在将我们db中数据返回到一级缓存中存放
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

PerpetualCache 一级缓存 底层就算一个map集合

key

value

一级缓存默认就是开启

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //判断是否配置了我们二级缓存 如果配置了cache != null
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
          // 先查询二级缓存---(redis)
        List<E> list = (List<E>) tcm.getObject(cache, key);
          // 如果二级缓存中没有该数据 则查询 简单执行器
        if (list == null) {
            // 简单执行器---base执行器  调用一级缓存查询-----装饰模式
          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);
  }

源码解读:

二级缓存在什么时候存放数据到redis中呢?

1.先将一级缓存中数据 先放入到 二级缓存临时 map集合中

   tcm.putObject(cache, key, list);
   entriesToAddOnCommit.put(key, object);

2.当我们调用commit 将二级缓存临时 map集合 中数据 遍历 写入到redis中

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

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)) {
         // 将数据写入到redis中缓存中
        delegate.putObject(entry, null);
      }
    }
  }

相关代码

📎一级、二级缓存源码解读.rar

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值