来来来, 给俏如来扎起. 感谢老铁们对俏如来的支持, 2021 一路有你, 2022 我们继续加油! 你的肯定是我最大的动力
博主在参加博客之星评比, 点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打 Call! 五星好评 感谢
前言
对于 Mybatis 的缓存在上一章节《吃透 Mybatis 源码 - Mybatis 执行流程》我们有提到一部分, 这篇文章我们对将详细分析一下 Mybatis 的一级缓存和二级缓存.
一级缓存
市面上流行的 ORM 框架都支持缓存, 不管是 Hibernate 还是 Mybatis 都支持一级缓存和二级缓存, 目的是把数据缓存到 JVM 内存中, 减少和数据库的交互来提高查询速度. 同时 MyBatis 还可以整合三方缓存技术.
Mybatis 一级缓默认开启, 是 SqlSession 级别的, 也就是说需要同一个 SqlSession 执行同样的 SQL 和参数才有可能命中缓存. 如:
同一个 SqlSession 执行同一个 SQL, 发现控制台日志只执行了一次 SQL 记录, 说明第二次查询是走缓存了. 但是要注意的是, 当 SqlSession 执行了 delete,update,insert 语句后, 缓存会被清除.
那么一级缓存在哪儿呢? 下面给大家介绍一个类.
Mybatis 中提供的缓存都是 Cache 的实现类, 但是真正实现缓存的是 PerpetualCache, 其中维护了一个
Map<Object, Object> cache = new HashMap<Object, Object>()
结构来缓存数据. 其他的缓存类采用了装饰模式对 PerpetualCache 做增强. 比如: LruCache 在 PerpetualCache 的基础上增加了最近最少使用的缓存清楚策略, 当缓存到达上限时候, 删除最近最少使用的缓存 (Least Recently Use). 代码如下
- public class LruCache implements Cache {
- // 对 PerpetualCache 做装饰
- private final Cache delegate;
下面对其他的缓存类做一个介绍
PerpetualCache : 基础缓存类
LruCache : LRU 策略的缓存 当缓存到达上限时候, 删除最近最少使用的缓存 (Least Recently Use),eviction="LRU"(默 认)
FifoCache : FIFO 策略的缓存 当缓存到达上限时候, 删除最先入队的缓存, 配置 eviction="FIFO"
SoftCache WeakCache : 带清理策略的缓存 通过 JVM 的软引用和弱引用来实现缓存, 当 JVM 内存不足时, 会自动清理掉这些缓存, 基于 SoftReference 和 WeakReference
SynchronizedCache : 同步缓存 基于 synchronized 关键字实现, 解决并发问题
ScheduledCache : 定时调度的缓存, 在进行 get/put/remove/getSize 等操作前, 判断 缓存时间是否超过了设置的最长缓存时间 (默认是 一小时), 如果是则清空缓存 - 即每隔一段时间清 空一次缓存
SerializedCache : 支持序列化的缓存 将对象序列化以后存到缓存中, 取出时反序列化
TransactionalCache : 事务缓存, 在二级缓存中使用, 可一次存入多个缓存, 移除多个缓存 . 通过 TransactionalCacheManager 中用 Map 维护对应关系.
一级缓存到底存储在哪儿?
一级缓存在 SimpleExecutor 的父类 BaseExecutor 执行器中, 如下
- public abstract class BaseExecutor implements Executor {
- private static final Log log = LogFactory.getLog(BaseExecutor.class);
- protected Transaction transaction;
- protected Executor wrapper;
- protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
- // 一级缓存
- protected PerpetualCache localCache;
PerpetualCache 缓存类源码如下
- public class PerpetualCache implements Cache {
- private final String id;
- // 缓存
- private Map<Object, Object> cache = new HashMap<Object, Object>();
那么一级缓存在什么时候创建的?
在 BaseExecutor 中的构造器中创建了一级缓存, 而执行器 Executor 是保存在 SqlSession 中的, 也就是说当创建 SqlSession 的时候, 就会创建 SimpleExecutor, 而在 SimpleExecutor 的构造器中会调用 BaseExecutor 的构造器来创建一级缓存. 见: org.apache.ibatis.executor.SimpleExecutor#SimpleExecutor
- public class SimpleExecutor extends BaseExecutor {
- // 执行器构造器
- public SimpleExecutor(Configuration configuration, Transaction transaction) {
- // 调用父类构造器
- super(configuration, transaction);
- }
下面是 BaseExecutor 的执行器 org.apache.ibatis.executor.BaseExecutor#BaseExecutor
- public abstract class BaseExecutor implements Executor {
- private static final Log log = LogFactory.getLog(BaseExecutor.class);
- protected Transaction transaction;
- protected Executor wrapper;
- // 一级缓存
- protected PerpetualCache localCache;
- protected PerpetualCache localOutputParameterCache;
- protected Configuration configuration;
- protected BaseExecutor(Configuration configuration, Transaction transaction) {
- this.transaction = transaction;
- this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
- // 创建一级缓存
- this.localCache = new PerpetualCache("LocalCache");
- this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
- this.closed = false;
- this.configuration = configuration;
- this.wrapper = this;
- }
一级缓存怎么存储的?
一级缓存是在执行查询的时候会先走二级缓存, 二级缓存么有就会走一级缓存, 以及缓存没有就会走数据库查询, 然后放入一级缓存和二级缓存. 我们来看一下源码流程 , 见: org.apache.ibatis.executor.CachingExecutor#query
- @Override
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameterObject);
- // 构建缓存的 Key
- CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
- // 执行查询
- return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
这里在尝试构建 Cachekey ,cachekey 时由: MappedStatement 的 id(如: cn.xx.xx.xxMapper.selectByid) , 分页, Sql, 参数值一起构建而成的, 一级二级缓存都是如此.
- @Override
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
- throws SQLException {
- // 开启了二级缓存才会存在 Cache
- Cache cache = ms.getCache();
- if (cache != null) {
- flushCacheIfRequired(ms);
- if (ms.isUseCache() && resultHandler == null) {
- ensureNoOutParams(ms, 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.putObject(cache, key, list); // issue #578 and #116
- }
- return list;
- }
- }
- return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
这里我们看到, 在执行 org.apache.ibatis.executor.CachingExecutor#query 查询的时候会先走二级缓存, 二级缓存没有会继续调用 org.apache.ibatis.executor.BaseExecutor#query 查询, 而 BaseExecutor#query 会尝试先走一级缓存
- 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 {
- // 如果一级缓存中没有, 走数据库查询数据
- 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;
- }
上面代码会先走一级缓存拿数据, 如果一级缓存没有, 就走数据库获取数据, 然后加入一级缓存 org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
- 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;
- }
到这里我们就看到了一级缓存和二级缓存的执行流程, 注意的是: 先执行二级缓存再执行一级缓存.
这里画一个一级缓存的图
二级缓存
第一步: 二级缓存需要在 mybatis-config.xml 配置中开启, 如下
<setting name="cacheEnabled" value="true"/>
当然其实该配置默认是开启的, 也就是默认会使用 CachingExecutor 装饰基本的执行器.
第二步骤: 需要在 mapper.xml 中配置 <cache/> 如下
- <mapper namespace="cn.whale.mapper.StudentMapper">
- <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
- size="1024"
- eviction="LRU"
- flushInterval="120000"
- readOnly="false"/>
... 省略...
解释一下上面的配置, 首先 < cache/> 是在某个 mapper.xml 中指定的, 也就是说二级缓存作用于当前的 namespace.
type : 代表的是使用什么类型的缓存, 只要是实现了 Cache 接口的实现类都可以
size : 缓存的个数, 默认是 1024 个对象
eviction : 缓存剔除策略 ,LRU - 最近最少使用的: 移除最长时间不被使用的对象 (默认);FIFO - 先进先出: 按对象进入缓存的顺序来移除它们 ;SOFT - 软引用: 移除基于垃圾回收器状态和软引用规则的对象; WEAK - 弱引用: 更积极地移除基于垃圾收集器状态和弱引用规则的对象
flushInterval : 定时自动清空缓存间隔 自动刷新时间, 单位 ms, 未配置时只有调用时刷新
readOnly : 缓存时候只读
blocking : 是否使用可重入锁实现 缓存的并发控制 true, 会使用 BlockingCache 对 Cache 进行装饰 默认 false
Mapper.xml 配置了之后, select() 会被缓存. update(),delete(),insert() 会刷新缓存, 下面是测试案例
可以看到, 这里使用了 2 个 SqlSesion 2 次执行了相同的 SQL, 参数相同, 看控制台日志只执行了一次 SQL, 说明是命中的二级缓存. 因为满足条件: 同一个 namespace 下的相同的 SQL 被执行, 尽管使用的 SqlSession 不是同一个.
但是你可能注意到一个细节, 就是 session.commit() 为什么要提交事务呢? 这就要说到二级缓存的存储结构了, 如果不执行 commit 是不会写入二级缓存的. 在 CachingExecutor 中有一个属性 private final TransactionalCacheManager tcm = new TransactionalCacheManager(); 看名字肯能够看出二级缓存和事务有关系. 结构如下
- public class CachingExecutor implements Executor {
- private final Executor delegate;
- // 二级缓存, 通过 TransactionalCacheManager 来管理
- private final TransactionalCacheManager tcm = new TransactionalCacheManager();
TransactionalCacheManager 中维护了一个 HashMap<Cache, TransactionalCache>()
- public class TransactionalCacheManager {
- // 二级缓存的 HashMap
- private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
在 TransactionCache 中维护了一个 Map<Object, Object> entriesToAddOnCommit;
- public class TransactionalCache implements Cache {
- private static final Log log = LogFactory.getLog(TransactionalCache.class);
- private final Cache delegate;
- private boolean clearOnCommit;
- // 二级缓存临时存储
- private final Map<Object, Object> entriesToAddOnCommit;
... 省略...
- // 写入二级缓存
- @Override
- public void putObject(Object key, Object object) {
- entriesToAddOnCommit.put(key, object);
- }
当执行查询的时候, 从数据库查询出来数据回写入 TransactionalCache 的 entriesToAddOnCommit 中, 我们来看一下二级缓存写入的流程, 见: org.apache.ibatis.executor.CachingExecutor#query
- @Override
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
- throws SQLException {
- // 如果 mapper.xml 配置了 <cache/> 就会创建 Cache
- Cache cache = ms.getCache();
- if (cache != null) {
- flushCacheIfRequired(ms);
- if (ms.isUseCache() && resultHandler == null) {
- ensureNoOutParams(ms, 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.putObject(cache, key, list); // issue #578 and #116
- }
- return list;
- }
- }
- return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
如果 mapper.xml 配置了 就会创建 Cache,Cache 不为 null, 才会走到二级缓存的流程, 此时代码来到 org.apache.ibatis.cache.TransactionalCacheManager#putObject
- public class TransactionalCacheManager {
- // 存储二级缓存
- private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
- public void putObject(Cache cache, CacheKey key, Object value) {
- // 通过 cache 为 key 拿到 TransactionalCache , 把数据 put 进去
- getTransactionalCache(cache).putObject(key, value);
- }
存储数据的是 TransactionalCache , 见 org.apache.ibatis.cache.decorators.TransactionalCache#putObject
- public class TransactionalCache implements Cache {
- private static final Log log = LogFactory.getLog(TransactionalCache.class);
- // 正在的二级缓存存储位置
- private final Cache delegate;
- private boolean clearOnCommit;
- // 临时的二级缓存存储位置
- private final Map<Object, Object> entriesToAddOnCommit;
- @Override
- public void putObject(Object key, Object object) {
- entriesToAddOnCommit.put(key, object);
- }
我们看到, 数据写到了 TransactionalCache#entriesToAddOnCommit 一个 Map 中. 只有在执行 commit 的时候数据才会真正写入二级缓存.
我们来看下 SqlSession.commit 方法是如何触发二级缓存真正的写入的, 见: org.apache.ibatis.session.defaults.DefaultSqlSession#commit()
- @Override
- public void commit() {
- commit(false);
- }
- @Override
- public void commit(boolean force) {
- try {
- // 调用执行器提交事务
- executor.commit(isCommitOrRollbackRequired(force));
- dirty = false;
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error committing transaction. Cause:" + e, e);
- } finally {
- ErrorContext.instance().reset();
- }
- }
代码来到 org.apache.ibatis.executor.CachingExecutor#commit
- @Override
- public void commit(boolean required) throws SQLException {
- // 提交事务
- delegate.commit(required);
- // 调用 org.apache.ibatis.cache.TransactionalCacheManager#commit 提交事务
- tcm.commit();
- }
代码来到 org.apache.ibatis.cache.TransactionalCacheManager#commit
- public void commit() {
- for (TransactionalCache txCache : transactionalCaches.values()) {
- // 调用 TransactionalCache#commit
- txCache.commit();
- }
- }
代码来到 org.apache.ibatis.cache.decorators.TransactionalCache#commit
- public class TransactionalCache implements Cache {
- private static final Log log = LogFactory.getLog(TransactionalCache.class);
- // 真正的二级缓存存储位置, 本质是一个 PerpetualCache
- private final Cache delegate;
- // 临时存储二级缓存
- private final Map<Object, Object> entriesToAddOnCommit;
- public void commit() {
- if (clearOnCommit) {
- delegate.clear();
- }
- // 这里在写入缓存, 保存到 TransactionalCache 中的 delegate 字段, 本质是一个 PerpetualCache
- flushPendingEntries();
- // 把 entriesToAddOnCommit 清除掉
- reset();
- }
- private void flushPendingEntries() {
- for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
- // 从 entriesToAddOnCommit 中拿到临时的缓存数据, 写入缓存, 最终会写入 PerpetualCache#cache 字段中
- delegate.putObject(entry.getKey(), entry.getValue());
- }
- for (Object entry : entriesMissedInCache) {
- if (!entriesToAddOnCommit.containsKey(entry)) {
- delegate.putObject(entry, null);
- }
- }
- }
- private void reset() {
- clearOnCommit = false;
- // 清除 entriesToAddOnCommit
- entriesToAddOnCommit.clear();
- entriesMissedInCache.clear();
- }
所以我们总结一下二级缓存的写入流程, 二级缓存通过 TransactionalCacheManager 中的一个 Map<Cache, TransactionalCache > 管理的, 当执行 query 查询处数据的时候, 会把数据写入 TransactionalCache 中的 Map<Object, Object> entriesToAddOnCommit 中临时存储. 当执行 commit 的时候才会把 entriesToAddOnCommit 中的数据写入 TransactionalCache 中的 Cache delegate , 其本质和一级缓存一样, 也是一个 PerpetualCache.
当我们做第二次 query 的时候会尝试通过 TransactionalCacheManager#getObject 从二级缓存获取数据
- public class TransactionalCacheManager {
- private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
- // 获取二级缓存
- public Object getObject(Cache cache, CacheKey key) {
- return getTransactionalCache(cache).getObject(key);
- }
然后会从 TransactionalCache 中的 delegate 中获取缓存
- public class TransactionalCache implements Cache {
- private static final Log log = LogFactory.getLog(TransactionalCache.class);
- // 二级缓存
- private final Cache delegate;
... 省略...
- @Override
- public Object getObject(Object key) {
- // issue #116
- // 从二级缓存获取数据
- Object object = delegate.getObject(key);
- if (object == null) {
- entriesMissedInCache.add(key);
- }
- // issue #146
- if (clearOnCommit) {
- return null;
- } else {
- return object;
- }
- }
所以记得, 二级缓存一定要 commit 才会起作用. 下面花了一个一级缓存和二级缓存的结构图
三方缓存框架
除了使用 Mybatis 自带的缓存, 也可以使用第三方缓存方式, 比如: 比如 ehcache 和 Redis 下面以 Redis 为例 , 首先导入 mybatis 整合 Redis 的依赖
- <dependency>
- <groupId>org.mybatis.caches</groupId>
- <artifactId>mybatis-Redis</artifactId>
- <version>1.0.0-beta2</version>
- </dependency>
第二步骤: 在 mapper.xml 配置缓存
- <cache type="org.mybatis.caches.redis.RedisCache"
- eviction="FIFO"
- flushInterval="60000"
- size="512" readOnly="true"/>
这里 type 使用了 RedisCache,RedisCache 也是实现了 Cache 接口的, 接着我们需要配置 Redis 的链接属性, 默认 RedisCache 类会读取名字为 : Redis.properties 的配置文件
- host=127.0.0.1
- password=123456
- port=6379
- connectionTimeout=5000
- soTimeout=5000
- database=0
再次执行测试代码, 查看 Redis 效果如下
博主在参加博客之星评比, 点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打 Call! 五星好评 感谢
来源: https://blog.csdn.net/u014494148/article/details/122313499