0
点赞
收藏
分享

微信扫一扫

Mybatis缓存实现原理

妖妖妈 2022-03-30 阅读 65

Mybatis缓存

mybatis提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-67m7jXMz-1648396495645)(image/image-20220327100245409.png)]

①、一级缓存是SqlSession级别的缓存,默认开启。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

②、二级缓存是mapper级别的缓存,基于mapper文件的namespace,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

一级缓存

⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KcLdkDaL-1648396495646)(image/image-20220327102727111.png)]

在执行openSession的时候,会创建Executor对象,根据执行器的类型ExecutorType选择的是SIMPLE类型,

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    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) {
        // 创建缓存执行器
        // 处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis会先去二级缓存中去查找
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

然后创建的SimpleExecutor对象,在这个对象创建的过程中,是调用的父类BaseExecutor对象,在父类实例化的过程中,创建PerpetualCache缓存对象。一级缓存其实就是本地存放的⼀个map对象。

 protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
     // 延迟加载
    this.deferredLoads = new ConcurrentLinkedQueue<>();
     // 创建本地缓存,即一级缓存
    this.localCache = new PerpetualCache("LocalCache");
     // 处理存储过程缓存,这里不讨论存储过程
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

SqlSession内部调用BaseExecutor查询,核心代码如下。

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  	// 去掉了不必要、延时加载等代码。
    List<E> list;
      // 查询缓存
      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);
      }
    return list;
  }

执行更新操作时,先清空缓存,再执行更新操作。

public int update(MappedStatement ms, Object parameter) throws SQLException {
  // ...
  clearLocalCache();
  return doUpdate(ms, parameter);
}

二级缓存

需要在配置文件中显式配置启动二级缓存。

配置

核心配置文件:mybatis-config.xml

 <settings>
<!--全局缓存开关,默认开启-->
<setting name="cacheEnabled " value="true"/>
</settings>

mapper.xml

<!--开启⼆级缓存,mapper.xml中所有标签默认才使用二级缓存-->
<cache></cache>

开启了⼆级缓存后,还需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。

flushCache和useCache属性

在statement中设置useCache=false可以禁⽤当前select语句的⼆级缓存。

flushCache在查询时默认false,在更新操作时默认true,清空二级缓存。

<select id="selectUserByUserId" flushCache="true" useCache="false" resultType="User" parameterType="int">
select * from user where id=#{id}
</select>

⼀般下执⾏完commit操作都需要刷新缓存,默认即可。

原理

原理与一级缓存基本相同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O3eD82Bc-1648396495646)(image/image-20220327124655536.png)]

多个sqlsession对同一个Mapper进行查询操作时是可以共享这个namespace中的缓存数据,但是当我们去当前这个mapper中的进行增、删、改或者commit的时候,同样是会清空缓存。

原理

如上。CachingExecutor对象是处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis会先去二级缓存中去查找

二级缓存Cache是从MappedStatement中获取到的,而MappedStatement又和每一个<insert>、<delete>、<update>、<select>绑定并在MyBatis启动的时候存入Configuration中:

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, 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);
        // 同时放到tcm事务缓存对象中
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 再查询一级缓存或数据库。
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

更新操作

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 必要的时候刷新缓存。内部将commit时清空设置成了true。
    flushCacheIfRequired(ms);
    // 执行更新操作
    return delegate.update(ms, parameterObject);
}

update()方法中只有清空一级缓存的缓存的方法,而真正清空缓存的方法是在commit方法中,所以二级缓存的使用一定注意提交事务。二级缓存是委托了tcm对象管理管理缓存的。

使用Redis实现二级缓存

MyBatis的缓存是和整个应用运行在同一个JVM中的,共享同一块堆内存,但是无法解决数据量大和分布式缓存两个问题,此时需要使用外部缓存(Redis)代替本地Map缓存PerpetualCache。

相关配置

mapper.xml

<cache type="org.mybatis.caches.redis.RedisCache" />

添加redis.properties,配置Redis信息。

public final class RedisCache implements Cache {
  private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
  private String id;
  private static JedisPool pool;

  public RedisCache(final String id) {
    this.id = id;
    RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
	pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(),
			redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(),
			redisConfig.getDatabase(), redisConfig.getClientName());
  }
}

RedisCache实现Cache接口,使用jedis实现。数据存取采用hash结构。

举报

相关推荐

0 条评论