Mybatis缓存
mybatis提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解:
①、一级缓存是SqlSession级别的缓存,默认开启。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
②、二级缓存是mapper级别的缓存,基于mapper文件的namespace,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
一级缓存
⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?
在执行openSession
的时候,会创建Executor
对象,根据执行器的类型ExecutorType
选择的是SIMPLE
类型,
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) {
// 创建缓存执行器
// 处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis会先去二级缓存中去查找
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
然后创建的SimpleExecutor
对象,在这个对象创建的过程中,是调用的父类BaseExecutor
对象,在父类实例化的过程中,创建PerpetualCache
缓存对象。一级缓存其实就是本地存放的⼀个map对象。
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;
}
SqlSession内部调用BaseExecutor查询,核心代码如下。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 去掉了不必要、延时加载等代码。
List<E> list;
// 查询缓存
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);
}
return list;
}
执行更新操作时,先清空缓存,再执行更新操作。
public int update(MappedStatement ms, Object parameter) throws SQLException {
// ...
clearLocalCache();
return doUpdate(ms, parameter);
}
二级缓存
需要在配置文件中显式配置启动二级缓存。
配置
核心配置文件:mybatis-config.xml
<settings>
<!--全局缓存开关,默认开启-->
<setting name="cacheEnabled " value="true"/>
</settings>
mapper.xml
<!--开启⼆级缓存,mapper.xml中所有标签默认才使用二级缓存-->
<cache></cache>
开启了⼆级缓存后,还需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。
flushCache和useCache属性
在statement中设置useCache=false可以禁⽤当前select语句的⼆级缓存。
flushCache在查询时默认false,在更新操作时默认true,清空二级缓存。
<select id="selectUserByUserId" flushCache="true" useCache="false" resultType="User" parameterType="int">
select * from user where id=#{id}
</select>
⼀般下执⾏完commit操作都需要刷新缓存,默认即可。
原理
原理与一级缓存基本相同
多个sqlsession
对同一个Mapper进行查询操作时是可以共享这个namespace
中的缓存数据,但是当我们去当前这个mapper
中的进行增、删、改或者commit
的时候,同样是会清空缓存。
原理
如上。CachingExecutor
对象是处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis会先去二级缓存中去查找
二级缓存Cache是从MappedStatement中获取到的,而MappedStatement又和每一个<insert>、<delete>、<update>、<select>
绑定并在MyBatis启动的时候存入Configuration中:
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, parameterObject, boundSql);
@SuppressWarnings("unchecked")
// 从缓存取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果二级缓存没有的话,回去进到这个查询方法中去一级缓存中找
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 同时放到tcm事务缓存对象中
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 {
// 必要的时候刷新缓存。内部将commit时清空设置成了true。
flushCacheIfRequired(ms);
// 执行更新操作
return delegate.update(ms, parameterObject);
}
update()
方法中只有清空一级缓存的缓存的方法,而真正清空缓存的方法是在commit
方法中,所以二级缓存的使用一定注意提交事务。二级缓存是委托了tcm
对象管理管理缓存的。
使用Redis实现二级缓存
MyBatis的缓存是和整个应用运行在同一个JVM中的,共享同一块堆内存,但是无法解决数据量大和分布式缓存两个问题,此时需要使用外部缓存(Redis)代替本地Map缓存PerpetualCache。
相关配置
mapper.xml
<cache type="org.mybatis.caches.redis.RedisCache" />
添加redis.properties,配置Redis信息。
public final class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
private String id;
private static JedisPool pool;
public RedisCache(final String id) {
this.id = id;
RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(),
redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(),
redisConfig.getDatabase(), redisConfig.getClientName());
}
}
RedisCache实现Cache接口,使用jedis实现。数据存取采用hash结构。