目录
- 1 MyBatis缓存概述
- 1.1 一级缓存的命中场景
- 1.1.1 缓存命中参数
- 1.1.2 触发清空缓存
- 1.2 一级缓存源码解析
- 1.3 一级缓存的清空
- 1.4 Mybatis集成Spring后一级缓存失效问题
- 1.4.1 解决
- 2 二级缓存
- 2.1 二级缓存需求
- 2.1.1 存储【核心功能】
- 2.1.2 溢出淘汰【核心功能】
- 2.1.3 其他功能
- 2.2 二级缓存责任链设计
- 2.2 二级缓存的使用
- 2.2.1 缓存空间的声明
- 2.2.2 缓存其它配置
- 2.2.3 二级缓存的命中条件
- 为什么提交之后才能命中缓存?
- 2.3 二级缓存结构
- 2.4 二级缓存执行流程
1 MyBatis缓存概述
MyBatis中存在两个缓存,一级缓存
和二级缓存
-
一级缓存
:会话级缓存
,生命周期仅存在于当前会话,不可以直接关关闭。但可以通过flushCache
和localCacheScope
对其做相应控制。 -
二级缓存
:应用级性缓存
,缓存对象存在于整个应用周期
,而且可以跨线程使用
。
1.1 一级缓存的命中场景
关于一级缓存的命中可大致分为两个场景,
满足特定命中参数
,第二不触发清空方法
1.1.1 缓存命中参数
- SQL与参数相同
- 同一个会话
- 相同的MapperStatement ID
- RowBounds行范围相同
1.1.2 触发清空缓存
- 手动调用
clearCache
- 执行提交回滚
- 执行
update
- 配置
flushCache=true
- 缓存作用域为
Statement
- 图示
1.2 一级缓存源码解析
一级缓存主要逻辑存在于
BaseExecutor
当中,当会话接收到查询请求之后,会交给执行器的Query
方法,在这会通过SQL、参数、分页条件等参数创建一个缓存key
,在基于这个key去PerpetualCachhe
中查找对应的缓存值,如果有值直接返回,没有则查询数据库,然后填充缓存
最终缓存的实现非常简单,就是一个HashMap
1.3 一级缓存的清空
缓存的清空对应
BaseExecutor中的clearLoaclCache()
方法,查看有哪些调用此方法,就能发现哪些场景会清空缓存了
如图
-
update
:执行任意增删改 -
select
:查询又分为两种情况,前置清空 -> flushCache=true
另一个则是后置清空,配置了缓存作用域为Statement查询结束会清空缓存
-
commit
:提交前清空 -
rollback
: 回滚前清空
clearLocalCache():是清空当前回话所有的一级缓存数据
1.4 Mybatis集成Spring后一级缓存失效问题
当项目集成Spring之后会发现一级缓存失效了,以为是Spring 的问题,真正的原因是Spring对SqlSession进行了封装通过SqlSessionTemplate,使得每次调用SQL都会创建一个SqlSession
,具体可以参见SqlSessionInterceptor
,一级缓存是在同一个会话中才会生效!
1.4.1 解决
给Spring添加事物,添加事物之后,
SqlSessionInterceptor
会去判断两次请求是否在同一事物当中,如果是就会共用一个SqlSession会话来解决
2 二级缓存
概述:二级缓存也称作是应用级缓存,与一级缓存不同的是它作用域范围是整个应用,而且可以夸线程使用,所以二级缓存会有更高的命中率,适合缓存一些修改较少的数据,数据访问流程上说先访问二级缓存,在访问一级缓存
2.1 二级缓存需求
二级缓存是一个完整的缓存解决方案,那应该包含哪些功能呢?这里为们主要分为核心功能,非核心功能两类。
2.1.1 存储【核心功能】
缓存数据存储在哪里?常用的方案如下:
-
内存
:最简单就是内存当中,不仅实现简单
,而且速度快
。弊端就是不能持久化
,且容量有限制
-
硬盘
:可以持久化,容量大
。但速度不如内存,一般结合内存一起使用。 -
第三方集成
:在分布式情况,如果想和其他节点共享缓存,只能第三方软件集成,比如Redis。
2.1.2 溢出淘汰【核心功能】
无论哪种存储都必须有一个容量,当容量满的时候就要清除,清除掉算法即溢出淘汰机制
。常见算法如下:
-
FIFO
:先进先出 -
LRU
:最近最少使用 -
WeakReference
:弱引用,将缓存对象进行弱引用包装,当Java进行垃圾回收的时候(GC),不论当前内存空间是否足够,这个对象都将会被回收。 -
SoftReference
:软引用,如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用
2.1.3 其他功能
-
过期清理
:清理存放过久的数据 -
线程安全
:保证缓存可以被多个线程使用 -
写安全
:当拿到缓存数据后,可对其进行修改,而不影响原本的缓存数据。通常采取做法是对缓存对象进行深拷贝
2.2 二级缓存责任链设计
这么多的功能,如何才能简单实现,并保证它的灵活性与扩展性呢?这里Mybatis抽象出Cache接口,其只定义了缓存中最基本的方法:
设置缓存
获取缓存
清除缓存
获取缓存数量
然后上述每一个功能都会对应一个组件类,并基于装饰者
加责任链
的模式,将各个组件进行串联,在执行缓存的基本功能时,其他的缓存逻辑会沿着这个责任链一次往下传递,如下图
优点
-
指责单一
:各个节点只负责自己的逻辑,不需要关心其他节点 -
扩展性强
:可根据需要扩展节点,删除节点,还可以调换顺序保证灵活性 -
松耦合
:各节点之间没强依赖其他节点。而是通过顶层的Cache接口进行间接依赖
2.2 二级缓存的使用
2.2.1 缓存空间的声明
二级缓存默认不开启,需要为其声明缓存空间才可以使用,通过@CacheNamespace
或指定的MappedStatement
声明之后该缓存为该Mapper所独有
,其他Mapper不能访问。如需多个Mapper共享一个缓存空间可以通过@CacheNamespaceRef
或引用同一个缓存空间。CacheNamespace
配置清单:
配置 | 说明 |
implementation | 指定缓存的存储实现类,默认是用HashMap存储在内存当中 |
eviction | 指定缓存溢出淘汰实现类,默认LRU,清除最少使用 |
flushInterval | 设置缓存定时全部清空时间 |
size | 设置缓存容量,超出后就会被eviction指定的算法进行淘汰 |
readWrite | true通过序列化复制,来保证缓存对象是可读写的,默认true |
blocking | 为每个key的访问添加阻塞锁,防止缓存击穿 |
properties | 为上述组件,配置额外参数,key对应组件中的字段名称 |
注:Cache中责任链的组成通过
@CacheNamespace
指导生成,具体逻辑参见CacheBuilder
/*
* Copyright 2009-2012 The MyBatis Team
*
* 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.mapping;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
import org.apache.ibatis.cache.decorators.FifoCache;
import org.apache.ibatis.cache.decorators.LoggingCache;
import org.apache.ibatis.cache.decorators.ScheduledCache;
import org.apache.ibatis.cache.decorators.SerializedCache;
import org.apache.ibatis.cache.decorators.SynchronizedCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
public class CacheBuilder {
private String id;
private Class<? extends Cache> implementation;
private List<Class<? extends Cache>> decorators;
private Integer size;
private Long clearInterval;
private boolean readWrite;
private Properties properties;
public CacheBuilder(String id) {
this.id = id;
this.decorators = new ArrayList<Class<? extends Cache>>();
}
public CacheBuilder implementation(Class<? extends Cache> implementation) {
this.implementation = implementation;
return this;
}
public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
if (decorator != null) {
this.decorators.add(decorator);
}
return this;
}
public CacheBuilder size(Integer size) {
this.size = size;
return this;
}
public CacheBuilder clearInterval(Long clearInterval) {
this.clearInterval = clearInterval;
return this;
}
public CacheBuilder readWrite(boolean readWrite) {
this.readWrite = readWrite;
return this;
}
public CacheBuilder properties(Properties properties) {
this.properties = properties;
return this;
}
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (cache.getClass().getName().startsWith("org.apache.ibatis")) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
}
return cache;
}
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorators.size() == 0) {
decorators.add(FifoCache.class);
}
}
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
private void setCacheProperties(Cache cache) {
if (properties != null) {
MetaObject metaCache = SystemMetaObject.forObject(cache);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String name = (String) entry.getKey();
String value = (String) entry.getValue();
if (metaCache.hasSetter(name)) {
Class<?> type = metaCache.getSetterType(name);
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
|| Integer.class == type) {
metaCache.setValue(name, Integer.valueOf(value));
} else if (long.class == type
|| Long.class == type) {
metaCache.setValue(name, Long.valueOf(value));
} else if (short.class == type
|| Short.class == type) {
metaCache.setValue(name, Short.valueOf(value));
} else if (byte.class == type
|| Byte.class == type) {
metaCache.setValue(name, Byte.valueOf(value));
} else if (float.class == type
|| Float.class == type) {
metaCache.setValue(name, Float.valueOf(value));
} else if (boolean.class == type
|| Boolean.class == type) {
metaCache.setValue(name, Boolean.valueOf(value));
} else if (double.class == type
|| Double.class == type) {
metaCache.setValue(name, Double.valueOf(value));
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
}
private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
try {
return cacheConstructor.newInstance(id);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
}
}
private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
try {
return cacheClass.getConstructor(String.class);
} catch (Exception e) {
throw new CacheException("Invalid base cache implementation (" + cacheClass + "). " +
"Base cache implementations must have a constructor that takes a String id as a parameter. Cause: " + e, e);
}
}
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
try {
return cacheConstructor.newInstance(base);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
}
}
private Constructor<? extends Cache> getCacheDecoratorConstructor(Class<? extends Cache> cacheClass) {
try {
return cacheClass.getConstructor(Cache.class);
} catch (Exception e) {
throw new CacheException("Invalid cache decorator (" + cacheClass + "). " +
"Cache decorators must have a constructor that takes a Cache instance as a parameter. Cause: " + e, e);
}
}
}
2.2.2 缓存其它配置
除@CacheNamespace
还可以通过其他参数来控制二级缓存
字段 | 配置域 | 说明 |
cacheEnable | 二级缓存全局开关,默认开启 | |
useCache | <select | update | insert | delete> | 指定的statement是否开启,默认关闭 |
flushCache | <select |update |insert |delete> | 执行sql前是否清空当前二级缓存空间,update默认true,query默认false |
2.2.3 二级缓存的命中条件
二级缓存的命中场景与一级缓存相似,不同的在于二级缓存可以跨线程使用,还有二级缓存的更新,必须是在会话提交后。
为什么提交之后才能命中缓存?
如上图两个会话在修改同一数据,当会二修改后,在讲其查询出来,假如它实时填充到二级缓存,而会话一就能通过缓存获取修改之后的数据,但实质修改的数据回滚了,并没有真正的提交给数据库。
所以为了保证数据一致性,二级缓存必须是会话提交之后才会真正填充,包括对缓存的清空,也必须是正常提交之后才生效
2.3 二级缓存结构
为了实现会话提交之后才更新缓存,Mybatis为每个会话设立了若干暂存区
,当前会话对指定的缓存空间变更,都存在对应的暂存区
,当会话提交
之后才会提交每个暂存区对应的缓存空间
。为了统一管理
这些暂存区
,每个会话都有唯一一个的事物管理器
,所以这里暂存区也可以叫做事物缓存
最后我们通过下图了解会话,暂存区,二级缓存空间的关系:
2.4 二级缓存执行流程
原本会话是通过Excutor
实现SQL调用,这里基于装饰器模式使用CachingExecutor
对SQL调用逻辑进行拦截。嵌入二级缓存相关逻辑
- 查询操作query
当会话调用query(),会基于查询语句、参数等数据组成缓存key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区
注:这里的查询是实时从缓存空间里读取的,而变更,只会记录在暂存区
- 更新操作update
当执行update操作时,同样会基于查询的语句和参数组成缓存key,然后在执行update之前清空缓存。这里只清空针对暂存区,同时记录清空的标记,以便会话提交之时,依据该标记去清空二级缓存空间
注:如果在查询操作中配置了 flushCache=true 也会执行相同的操作
- 提交操作commit
当会话执行commit之后,会将该会话下的所有暂存区的变更,更新到对应的二级缓存空间里去