mybatis缓存
一级缓存
一级缓存是同一session内缓存,随着session的关闭而被清除。
先看下效果
String resource = "mybatis-config.xml";
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.findUserByNo("001");
System.out.println(user1);
User user2 = mapper.findUserByNo("001");
System.out.println(user2);
System.out.println(user1.equals(user2));
执行两次相同的mapper.findUserByNo方法,观察日志只向数据库发送一次查询请求。并且user1.equals(user2)是完全相同的两个对象。证明缓存命中,第二次查询读的缓存。
命中条件
必须同一会话session这个就不用多说了
必须是相同的mapper方法,相同的参数
mapper.findUserByNo("001");
mapper.findUserByNo("002");
这样不同的参数是不会命中缓存
中间没有执行过更新(update,insert,delete)操作
mapper.findUserByNo("001");
mapper.updateUser("002");
mapper.findUserByNo("001");
更新操作会清空缓存
session.clearCache()会清空缓存
源码实现
****CacheExecutor主要用来对Executor进行包装完成缓存的处理
cacheKey的创建
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建cacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
创建cacheKey方法。CacheKey有一个update方法,加入新参数会重新构造计算该类的hashcode方法。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//语句id。就是mapper方法的全路径 包名+类名+方法名
cacheKey.update(ms.getId());
//分页条件
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//sql语句
cacheKey.update(boundSql.getSql());
//sql参数
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因子
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// 最后将运行环境值加
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
从key的构造可以看出调用必须是同一个mapper方法并且参数值相等。
缓存的存入和获取
在BaseExecutor.query方法中可以看到使用的PerpetualCache来存储缓存,其内部也是维护了一个map用来存储缓存数据。
BaseExecutor中操作缓存方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
queryStack++;
//判断是否指定resultHandler,否则从本地缓存根据cacheKey获取数据
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--;
}
...
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);
}
//将查询结果存放到cache中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
public void clearLocalCache() {
if (!closed) {//清空本地缓存
localCache.clear();
localOutputParameterCache.clear();
}
}
在query方法中会先根据cacheKey进行缓存查找,如果找不到在使用queryFromDatabase方法进行数据库查询,数据库查询完后会将结果加入缓存。另外在Executor的update方法(update、insert、delete最后都会交给update方法),session的clearCache方法最后都会调用clearLocalCache来清空缓存。所以上面说缓存失效的几种场景就很好理解了。
生命周期
来看下几个对象的关系
public class DefaultSqlSession implements SqlSession {
// 这里会是一个CacheExecutor实例
private final Executor executor {
/**
*CacheExecutor的delegate是一个SimpleExecutor,SimpleExecutor继承自BaseExecutor
*/
Executor delegate {
//缓存对象
PerpetualCache localCache;
}
};
}
从上面的对象关系可以看出,缓存是session对象一个属性。会随着session的关闭二消失。
使用场景
有什么用呢?
二级缓存
开启使用
上面说的一级缓存是在同一session会话中,很有局限性,当session关闭时候缓存就消失了。二级缓存是session间共享的。
在mapper里添加配置开启二级缓存
<mapper namespace="com.test.mapper.UserMapper" >
<cache ></cache>
</mapper>
String resource = "mybatis-config.xml";
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
//开启第一个session
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.findUserByNo("001");
System.out.println(user1);
sqlSession.close();//关闭第一个session
//开启第二个session
sqlSession = sessionFactory.openSession();
mapper = sqlSession.getMapper(UserMapper.class);
User user2 = mapper.findUserByNo("001");
System.out.println(user2);
System.out.println(user1.equals(user2));
上面的程序执行开启两个session,执行相同的mapper方法,参数也一致。然后会发现只会向数据库发出一次查询请求,第二次走的缓存。
一个查询首先会从二级缓存查找,然后在从一级缓存查找,最后走数据库查询。
缓存的存储
具体代码看CacheExecutor.query方法主要逻辑
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从statment获取缓存,这里就是二级缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
//从缓存中获取
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); // 将查询结果放入缓存
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里有两个主要的变量tcm和cache。 cache是从MappedStatement获取的,而statement又是configuration初始化的时候创建的,因此是和mybatis同生命周期的,全局的。 这里cache实例的结构是这样的
cache是层层delegate包装。最后存储结构还是map。这些Cache类都实现了Cache接口。Cache三个主要接口方法就是putObject、getObject和removeObject。
SynchronizedCache是在缓存操作时候方法都加上了synchronized。
LoggingCache在缓存命中记录命中率。
SerializedCache对缓存数据对象都进行序列化和反序列化操作。这时候你就要知道为什么mapper返回的对象都要实现序列化接口了。
LruCache实现LRU(least recently used)算法。缓存过大时候移除策略。
PerpetualCache这个就是是包装了下map。
tcm变量是TransactionalCacheManager类实例。里面存有本地session所持有各statment的cache。
Map<Cache, TransactionalCache> transactionalCaches;
key是Cache类型,代表不同的二级缓存。value是TransactionalCache。这个也实现了Cache接口。当session提交或rollback时候,会将session内所持有的缓存(tcm中保存)依次进行提交到二级缓存或清空。
TransactionalCacheManager类的代码
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
//清空
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
//提交
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
//回滚清空
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
//根据二级缓存对象获取当前session中是否有本地缓存
private TransactionalCache getTransactionalCache(Cache cache) {
//如果map没有就new一个TransactionalCache,然后cache会传入作为其delegate
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
TransactionalCache提交到二级缓存方法
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//这里的delegate是具体的二级缓存,transactionalCaches对应的key
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
这样二级缓存的存储和获取就都明白了。会过头来看下二级缓存是怎么初始化的呢。这个时候就要看下mybatis初始化过程,主要在解析mapper文件的时候
这里主要在XMLMapperBuilder类中进行处理。入口是parse方法,然后调用configurationElement方法,
private void configurationElement(XNode context) {
String namespace = context.getStringAttribute("namespace");
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
//解析cache配置
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
private void cacheElement(XNode context) {
if (context != null) {//解析所有的cache节点配置内容
//基础缓存类型,这里是PerpetualCache类
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//下面是所有的cache配置
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
//创建缓存实例
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();//将所有的配置属性设置好后,build创建实例
configuration.addCache(cache);
currentCache = cache;
return cache;
}
再来看CacheBuilder的build过程
public Cache build() {
setDefaultImplementations();
//第一步是基础实现PerpetualCache
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
//这里所有的装饰cache,默认配置会有一个LruCache
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
//下面对cache进行一些标准装饰
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
private Cache setStandardDecorators(Cache cache) {
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配置readWrite为true,使用SerializedCache进行装饰
cache = new SerializedCache(cache);
}
//添加log装饰
cache = new LoggingCache(cache);
//添加同步装饰
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
}
看到这里就会明白为什么cache会包装这么多层了。我们看到LruCache和SerializedCache是可以通过配置去掉的。其它的几个装饰好像都是标准不能去掉的。
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="false"/>
eviction配置对应缓存清理策略,默认是LRU会用到LruCache
readOnly对应是否是只读,默认false。可读写就会用到SerializedCache。
size缓存大小,默认1024。超过就会调用LruCache清理
flushInterval刷新间隔,默认是不设置,如果设置就会启用ScheduledCache。
缓存的清除
默认情况下select不会清空缓存,update,insert和delete都会清空statment的缓存。这是默认配置。可以在mapper文件的statment语句通过flushCache属性配置。
回到CacheExecutor类,flushCacheIfRequired用来清空缓存。query和update方法都会调用该方法。
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);
...
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
//根据flushCache配置是否清空缓存
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
这里tcm的key是共享的二级缓存Cache实例引用,在session内清了,也相当于整体清理了。引用也有一定好处。一处改处处改。这也是为什么要使用SynchronizedCache包装的原因吧。
使用场景
一些公共配置,字典,菜单,权限机构等等使用二级缓存可以提高效率。
总结
一级缓存是在同一个session可见。对在同一个session内多次相同的查询生效。二级缓存是session间共享的。缓存首先会从二级缓存查找,然后是一级缓存。由缓存CacheKey的构建可以知道,必须是调用同一个mapper的相同方法并且实例参数一致才被判断为相同查询。二级缓存的存储是以MappedStatement为单位的,也就是一个select标签方法。同一个session可能同时持有多个二级缓存,二级缓存的更新是在session提交或close的时候将本地缓存更新到二级缓存。