Mybatis源码-缓存实现
目录
MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。
MyBatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的,而且不能关闭。
缓存的使用 #
至于一级缓存为什么不能关闭,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过<association>和<collection>建立级联映射、避免循环引用(circularreferences)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。MyBatis 提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqISession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。MyBatis的一级缓存,用户只能控制缓存的级别,并不能关闭。
MyBatis框架二级缓存的使用。MyBatis二级缓存的使用比较简单,只需要以下几步:
- MyBatis主配置文件中指定cacheEnabled属性值为true。
<configuration> <settings> <setting name="cacheEnabled" value="true"/> </settings> </configuration>
- 在MyBatisMapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性,例如:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
- 在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过flushCache属性指定Mapper执行后是否刷新缓存,例如:
<select id="listAllUser" flushCache="false" useCache="true" resultType="com.blog4java.mybatis.example.entity.User" > select <include refid="userAllField"/> from user </select>
可以发现二级缓存是基于命名空间的,每个mapper都是一个不同的缓存key,执行DML之后缓存会清空,但是二级缓存是不推荐使用的,Mybatis二级缓存,你确定要用么?-腾讯云开发者社区-腾讯云 (tencent.com)
缓存实现类 #
MyBatis中的缓存类采用装饰器模式设计,Cache接口有一个基本的实现类,即PerpetualCache类,该类的实现比较简单,通过一个HashMap实例存放缓存对象。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的 Id相同时,即认为缓存对象相同。另外,PerpetualCache类还重写了Object类的hashCode()方法,仅以缓存对象的Id作为因子生成hashCode。
我们可以使用MyBatis提供的缓存装饰器类对基础的PerpetualCache类的功能进行增强,使用不同的装饰器后,缓存对象则拥有对应的功能。
public void testCache() {
final int N = 100000;
Cache cache = new PerpetualCache("default");
cache = new LruCache(cache);
cache = new FifoCache(cache);
cache = new SoftCache(cache);
cache = new WeakCache(cache);
cache = new ScheduledCache(cache);
cache = new SerializedCache(cache);
cache = new SynchronizedCache(cache);
cache = new TransactionalCache(cache);
for (int i = 0; i < N; i++) {
cache.putObject(i, i);
((TransactionalCache) cache).commit();
}
System.out.println(cache.getSize());
}
一级缓存实现原理 #
public abstract class BaseExecutor implements Executor {
// ....
// Mybatis一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
// ....
}
protected BaseExecutor(Configuration configuration, Transaction transaction) {
// ...
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
// ...
}
// org.apache.ibatis.executor.BaseExecutor#createCacheKey可以看看缓存的key是怎么创建的。
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()); // Mapper Id
cacheKey.update(rowBounds.getOffset()); // 偏移量
cacheKey.update(rowBounds.getLimit()); // 条数
cacheKey.update(boundSql.getSql()); // SQL语句
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// 所有参数值
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);
}
}
// Environment Id
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
从上面的代码可以看出,缓存的Key与下面这些因素有关:
- Mapper的 Id,即Mapper命名空间与<select/update/insert/delete>标签的 Id组成的全局限定名。
- 查询结果的偏移量及查询的条数。
- 具体的SQL语句及SQL语句中需要传递的所有参数。
- MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的id属性值。
执行两次查询时,只有上面的信息完全相同时,才会认为两次查询执行的是相同的SQL语句,缓存才会生效。接下来我们看一下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 {
// 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
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;
}
BaseExecutor的update()方法
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);
}
在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
二级缓存实现原理 #
CachingExecutor负责缓存的实现,Configuration负责生成CachingExecutor。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 根据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);
}
// 如果cacheEnabled属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 执行拦截器链的拦截逻辑
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
CachingExecutor 是对其他执行器的增强。装饰器模式。
public class CachingExecutor implements Executor {
private final Executor delegate;
// 管理所有的二级缓存对象
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 调用createCacheKey()方法创建缓存Key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取MappedStatement对象中维护的二级缓存对象
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否需要刷新二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 从MappedStatement对象对应的二级缓存中获取数据
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果缓存数据不存在,则从数据库中查询数据
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 將数据存放到MappedStatement对象对应的二级缓存中
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 {
// 如果需要刷新,则更新缓存
// mapper的 flushcache属性 控制
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
}
Mybatis使用Redis缓存 #
MyBatis除了提供内置的一级缓存和二级缓存外,还支持使用第三方缓存(例如Redis、Ehcache)作为二级缓存。本节我们就来了解一下在MyBatis中如何使用Redis作为二级缓存以及它的实现原理。MyBatis官方提供了一个mybatis-redis模块,该模块用于整合Redis作为二级缓存。使用该模块整合缓存,首先需要引入该模块的依赖,如果项目通过Maven构建,则只需要向pom.xml文件中添加如下内容:
最后,需要在classpath 下新增redis.properties 文件,配置 Redis 的连接信息。下面是redis.properties配置案例:
host=127.0.0.1
port=6379
password=admin
maxActive=100
maxIdle=20
whenExhaustedAction=WHEN_EXHAUSTED_GROW
maxWait=10
testonBorrow=true
testonReturn=true
timeBetweenEvictionRunsMillis=10000
numTestsPerEvictionRun=1000
minEvictableIdleTimeMillis=100
softMinEvictableIdleTimeMillis=-1
需要注意的是,使用Redis作为二级缓存,需要通过<cache>标签的type属性指定缓存实现类为org.mybatis.caches.redis.RedisCache
。MyBatis启动时会解析Mapper配置信息,为每个命名空间创建对应的RedisCache实例,由于JedisPool实例是RedisCache类的静态属性,因此JedisPool实例是所有RedisCache对象共享的。
除了Redis外,MyBatis还提供了整合其他缓存的适配器。例如,ehcache-cache项目用于整合EhCache 缓存,oscache-cache项目用于整合OSCache缓存,memcached-cache项目用于整合Memcached缓存。