二级缓存原理
1. 二级缓存
1.1 定义
二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改比较少的数据。
说到二级缓存,简单说一下一级缓存,我们日常用到的mybatis基本上都是一级缓存。
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement
,在Local Cache
行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。
1.2 扩展性需求
二级缓存的生命周期是整个应用,所以必须限制二级缓存的容量,在这里 MyBatis 使用的是溢出淘汰机制。而一级缓存是会话级的。生命周期非常短暂是没有必要实现这些功能的。相比较之下,二级缓存机制更加完善。
1.3 结构
二级缓存在结构设计上采用装饰器+责任链模式
二级缓存如何组装这些装饰器呢?
CacheBuilder 是二级缓存的构建类,里面定义了一些上图装饰器类型的属性,一级构建组合这些装饰器的行为。
public Cache build() {
this.setDefaultImplementations();
Cache cache = this.newBaseCacheInstance(this.implementation, this.id);
this.setCacheProperties((Cache)cache);
if (PerpetualCache.class.equals(cache.getClass())) {
Iterator var2 = this.decorators.iterator();
while(var2.hasNext()) {
Class<? extends Cache> decorator = (Class)var2.next();
cache = this.newCacheDecoratorInstance(decorator, (Cache)cache);
this.setCacheProperties((Cache)cache);
}
cache = this.setStandardDecorators((Cache)cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache((Cache)cache);
}
return (Cache)cache;
}
private void setDefaultImplementations() {
if (this.implementation == null) {
this.implementation = PerpetualCache.class;
if (this.decorators.isEmpty()) {
this.decorators.add(LruCache.class);
}
}
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (this.size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", this.size);
}
if (this.clearInterval != null) {
cache = new ScheduledCache((Cache)cache);
((ScheduledCache)cache).setClearInterval(this.clearInterval);
}
if (this.readWrite) {
cache = new SerializedCache((Cache)cache);
}
Cache cache = new LoggingCache((Cache)cache);
cache = new SynchronizedCache(cache);
if (this.blocking) {
cache = new BlockingCache((Cache)cache);
}
return (Cache)cache;
} catch (Exception var3) {
throw new CacheException("Error building standard cache decorators. Cause: " + var3, var3);
}
}
1.4 SynchronizedCache 线程同步缓存区
实现线程同步功能,与序列化缓存区共同保证二级缓存线程安全。如
blocking=false
关闭则 SynchronizedCache 位于责任链的最前端,否则就位于 BlockingCache 后面而 BlockingCache 位于责任链的最前端,从而保证了整条责任链是线程同步的。
1.5 LoggingCache 统计命中率以及打印日志
public class LoggingCache implements Cache {
private final Log log;
private final Cache delegate;
protected int requests = 0;
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(this.getId());
}
public Object getObject(Object key) {
++this.requests;//执行一次查询加一次
Object value = this.delegate.getObject(key);//查询缓存中是否已经存在
if (value != null) {
++this.hits;//命中一次加一次
}
if (this.log.isDebugEnabled()) {//开启debug日志
this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());
}
return value;
}
private double getHitRatio() {//计算命中率
return (double)this.hits / (double)this.requests;//命中次数:查询次数
}
}
1.6 ScheduledCache 过期清理缓存区
@CacheNamespace(flushInterval=100L)
设置过期清理时间默认为 1 小时,若设置 flushInterval 为 0 代表永远不进行清除。
public class ScheduledCache implements Cache {
private final Cache delegate;
protected long clearInterval;
protected long lastClear;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
this.clearInterval = 3600000L;
this.lastClear = System.currentTimeMillis();
}
public void clear() {
this.lastClear = System.currentTimeMillis();
this.delegate.clear();
}
private boolean clearWhenStale() {
//判断当前时间与上次清理时间差是否大于设置的过期清理时间
if (System.currentTimeMillis() - this.lastClear > this.clearInterval) {
this.clear();//一旦进行清理便是清理全部缓存
return true;
} else {
return false;
}
}
}
1.7 LruCache (最近最少使用)防溢出缓存区
内部使用链表实现最近最少使用防溢出机制
public void setSize(final int size) {
this.keyMap = new LinkedHashMap<Object, Object>(size, 0.75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
protected boolean removeEldestEntry(Entry<Object, Object> eldest) {
boolean tooBig = this.size() > size;
if (tooBig) {
LruCache.this.eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
//每次访问都会遍历一次key进行重新排序,将访问元素放到链表尾部。
public Object getObject(Object key) {
this.keyMap.get(key);
return this.delegate.getObject(key);
}
1.8 FifoCache(先进先出)防溢出缓存区
内部使用队列存储 key 实现先进先出防溢出机制
public class FifoCache implements Cache {
private final Cache delegate;
private final Deque<Object> keyList;
private int size;
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList();
this.size = 1024;
}
public void putObject(Object key, Object value) {
this.cycleKeyList(key);
this.delegate.putObject(key, value);
}
public Object getObject(Object key) {
return this.delegate.getObject(key);
}
private void cycleKeyList(Object key) {
this.keyList.addLast(key);
if (this.keyList.size() > this.size) {//比较当前队列元素个数是否大于设定值
Object oldestKey = this.keyList.removeFirst();//移除队列头元素
this.delegate.removeObject(oldestKey);//根据移除元素的key移除缓存区中的对应元素
}
}
}
1.9 二级缓存的使用(命中条件)
- 会话提交后
- sql 语句、参数相同
- 相同的 statementID
- RowBounds 相同
设置为自动提交事务并不会命中二级缓存
2. 二级缓存配置
2.1 配置
2.2 二级缓存为什么提交后才能命中缓存
会话一与会话二原本是两条隔离的事务,但由于二级缓存的存在导致彼此可见会发生脏读。若会话二的修改直接填充到二级缓存,会话一查询时缓存中存在即直接返回数据,此时会话二回滚会话一读到的数据就是脏数据。为了解决这一问题 MyBatis 二级缓存机制引入了
事务管理器(暂存区)
,所有变动的数据都会暂存到事务管理器的暂存区中,只有执行提交动作后才会真正的将数据从暂存区填充到二级缓存中
- 会话:事务暂存管理器:暂存区=1:1:N
- 暂存区:缓存区=1:1(一个暂存区对应唯一一个缓存区)
- 会话关闭,事务缓存管理器也会关闭,暂存区也会被清空
- 一个事务缓存管理器管理多个暂存区
- 有多少个暂存区取决于访问了多少个 Mapper 文件(缓存的 key 是 Mapper 文件全路径 ID)
2.3 二级缓存执行流程
- 查询是实时查询缓存区的。
- 所有对二级缓存的实时变动都是通过暂存区来实现的
- 暂存区清理完会进行标识,但此时二级缓存中数据并未清理,只有执行 commit 后才会真正清理二级缓存中的数据。
- 查询会实时查询缓存区,若暂存区清理标识为 true 就算从缓存区中查询到数据也会返回一个 null,重新查询数据库(暂存区清理标识位 true 也会返回 null 是为了防止脏读,一旦提交清空掉二级缓存中的数据此时读取到的就是脏数据,因此返回 null 重新查询数据库得到的才是正确数据)
若开启二级缓存进行查询方法的时候会走到类 CacheingExecutor 中的 query 方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();//获得Cache
if (cache != null) {
this.flushCacheIfRequired(ms);//判断是否配置了flushCache=true,若配置了清空暂存区
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);//获得缓存
if (list == null) {//若为空查询数据库并将数据填充到暂存区
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
根据上一步中的
tcm.getObject(cache,key)
方法查询二级缓存
public Object getObject(Object key) {
2 Object object = this.delegate.getObject(key);//查询二级缓存
3 if (object == null) {//为空也是为了先设置一个值防止缓存穿透
4 this.entriesMissedInCache.add(key);
5 }
6 //判断暂存区清空标识是否为true,若为true直接返回null重新查询数据库防止脏读
7 return this.clearOnCommit ? null : object;
8 }