缓存体系结构
缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。
但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)。
所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。
缓存实现类 | 描述 | 作用 | 装饰条件 |
基本缓存 | 缓存基本实现类 | 默认是 PerpetualCache,也可以自定义比如 RedisCache、EhCache 等,具备基本功能的缓存类 | 无 |
LruCache | LRU策略的缓存 | 当缓存达到上限时,删除最近最少使用的缓存 | eviction="LRU" |
FifoCache | FIFO策略的缓存 | 当缓存达到上限时,删除最先保存的缓存 | eviction="FIFO" |
SoftCache WeakCache | 带清理策略的缓存 | 通过 JVM 的软引用和弱引用来实现缓存,当 JVM内存不足时,会自动清理掉这些缓存,基于SoftReference 和WeakReference | eviction="SOFT" eviction="WEAK" |
LoggingCache | 带日志功能的缓存 | 比如:输出缓存命中率 | 基本 |
SynchronizedCache | 同步缓存 | 基于 synchronized 关键字实现,解决并发问题 | 基本 |
BlockingCache | 阻塞缓存 | 通过在 get/put 方式中加锁,保证只有一个线程操作缓存,基于 Java 重入锁实现 | blocking=true |
SerializedCache | 支持序列化的缓存 | 将对象序列化以后存到缓存中,取出时反序列化 | readOnly=false(默 认) |
ScheduledCache | 定时调度的缓存 | 在进行 get/put/remove/getSize 等操作前,判断 缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每隔一段时间清空一次缓存 | flushInterval 不为空 |
TransactionalCache | 事务缓存 | 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 | 在TransactionalCach eManager 中用 Map 维护对应关系 |
一级缓存
一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。
DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在 Executor 里面维护——SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中持有了 PerpetualCache。
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;
}
我们接下来看下源码中是如何存入和取出的:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
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;
}
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;
}
在查询之前,先创建CacheKey,然后通过CacheKey从localCache中找,如果没找到则从数据库中查询,然后再存入localCache中。
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
更新时会清空缓存。。。
因为一级缓存是不可以跨会话共享的,所以如果在会话1缓存了,会话2更新的话,会导致会话1查询到脏数据
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
既然是跨会话共享,那么肯定是在SqlSession的外层,通过源码我们可以看出,是通过包装类CachingExecutor包装了创建的Executor来实现的(cacheEnabled默认为true):
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) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
开启二级缓存的方法:
1.在 mybatis-config.xml 中配置了(可以不配置,默认是 true)
<setting name="cacheEnabled" value="true"/>
2.在 Mapper.xml 中配置<cache/>标签:
<!-- 声明这个 namespace 使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" <!—最多缓存对象个数,默认 1024--> eviction="LRU" <!—回收策略--> flushInterval="120000" <!—自动刷新时间 ms,未配置时只有调用时刷新--> readOnly="false"/> <!— 默认是 false(安全),改为 true 可读写时,对象必须支持序列化 -->
cache 属性详解:
属性 | 含义 | 取值 |
type | 缓存实现类 | 需要实现 Cache 接口,默认是PerpetualCache |
size | 最多缓存对象个数 | 默认 1024 |
eviction | 回收策略(缓存淘汰算法) | LRU – 最近最少使用的:移除最长时间不被使用的对象(默认)。 FIFO – 先进先出:按对象进入缓存的顺序来移除它们。 |
flushInterval | 定时自动清除缓存间隔 | 自动刷新时间,单位 ms,未配置时只有调用时刷新 |
readOnly | 是否只读 | true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。 false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是 false。 改为 false 可读写时,对象必须支持序列化。 |
blocking | 是否使用可重入锁实现 缓存的并发控制 | true,会使用 BlockingCache 对 Cache 进行装饰 默认 false |
Mapper.xml 配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。
只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置<cache>,决定了在启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor query 方法里面的判断:
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, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?
我们可以在单个 Statement ID 上显式关闭二级缓存(默认是 true):
<select id="selectBlog" resultMap="BaseResultMap" useCache="false"/>
这里需要注意一下:如果不提交事务,二级缓存是会失效的,原因:CachingExecutor只有一个TransactionCacheManager实例tcm,在tcm.putObject方法时,只是将数据放入tcm下的transactionalCaches属性中:
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
而transactionalCaches的putObject的方法将数据放入entriesToAddOnCommit属性中:
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
而只有调用CachingExecutor的commit方法才放入到Cache中:
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
tcm的commit:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
TransactionalCache的commit方法,将entriesToAddOnCommit数据提交:
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
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)) {
delegate.putObject(entry, null);
}
}
}
最终调用了PurpetualCache的putObject方法存入Cache中:
public Object getObject(Object key) {
return cache.get(key);
}
CachingExcutor获取二级缓存:
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, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
会先从tcm中是否能获取到缓存,如果没有则从数据库中获取。。。
第三方缓存做二级缓存:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
Mapper.xml 配置,type 使用 RedisCache:
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly ="true"/>
redis.properties 配置:
host= localhost
port= 6379
connectionTimeout= 5000
soTimeout= 5000
database=0 0