预备知识
对 Mybatis 的执行流程有一定的了解。
一级缓存
在读多写少的情况下,减少了网络的开销,显著提升性能。
配置
public enum LocalCacheScope {
SESSION,STATEMENT
}
对应到配置文件
<setting name="localCacheScope" value="SESSION"/>
支持 SESSION 和 STATEMENT 两种。其中
SESSION :指在当前事务中都使用查询缓存。
STATEMENT:指在当前 Sql 语句中使用查询缓存
注:如果是 SESSION 的情况下,如果有多个 SqlSession 同时操作操作数据库的同一张表,就可能出现脏读。
实现原理
一级缓存的实现在 BaseExecutor:每一个SqlSession中持有了自己的Executor,每一个Executor(都继承自 BaseExecutor)中有一个Local Cache。
当用户发起查询时,Mybatis会根据当前执行的MappedStatement生成一个key,去Local Cache中查询,
- 如果缓存命中的话
- 如果缓存没有命中的话,将写一个占位符,这样后续的读,就可以利用缓存了。之后查询 DB,将结果写入Local Cache。
- 如果配置为 STATEMENT,清除缓存
最后返回结果给用户。
public abstract class BaseExecutor implements Executor {
//本地缓存,一个 Executor 对应一个 localCache
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
//key 为 Statement Id + Offset + Limmit + Sql + Params
@Override
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
// STATEMENT 级别,清除缓存
clearLocalCache();
}
}
return list;
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
final Object cachedParameter = localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
final MetaObject metaParameter = configuration.newMetaObject(parameter);
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
final String parameterName = parameterMapping.getProperty();
final Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
//这里缓存更新是有讲究的。
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);
}
//将数据库数据写入缓存。如果在此之前将来了新的查询,将返回 EXECUTION_PLACEHOLDER
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
@Override
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.");
}
//每次更新的时候都会清空缓存。update,delete, insert 都调用 update 来完成。
clearLocalCache();
return doUpdate(ms, parameter);
}
}
一级缓存演示&失效情况
同一次会话期间只要查询过的数据都会保存在当前SqlSession的一个Map中
- key:hashCode+查询的SqlId+编写的sql查询语句+参数
- 一级缓存失效的四种情况
1、不同的SqlSession对应不同的一级缓存
2、同一个SqlSession但是查询条件不同
3、同一个SqlSession两次查询期间执行了任何一次增删改操作
4、同一个SqlSession两次查询期间手动清空了缓存
验证
配置为 SESSION
- 同一个事务 sqlSession 3次查询同一个表,检查后续请求是否查询缓存
- 三个事务,每个事实的 sqlSession 各查询同一张表一次。检查后续请求是否查询缓存
- 两个 sqlSession,一个事务 sqlSession1 连续两次查询表,sqlSession2 在 sqlSession1 的两次查询中间修改表
- 两个 sqlSession,两个事务 sqlSession1 各一次查询表,sqlSession2 在两次查询中间修改表
配置为 STATEMENT
- 同一个事务 sqlSession 3次查询同一个表,检查后续请求是否查询缓存
- 三个事务,每个事实的 sqlSession 各查询同一张表一次。检查后续请求是否查询缓存
- 两个 sqlSession,一个事务 sqlSession1 连续两次查询表,sqlSession2 在 sqlSession1 的两次查询中间修改表
- 两个 sqlSession,两个事务 sqlSession1 各一次查询表,sqlSession2 在两次查询中间修改表
总结
- Mybatis一级缓存的生命周期和SqlSession一致。
- 不管是 SESSION 还是 STATEMENT 在跨事务都不共享缓存。因为在事务提交的时候会清空缓存。
- Mybatis的缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念,同时只是使用了默认的hashmap,也没有做容量上的限定。
- Mybatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,有操作数据库写的话,会引起脏数据,建议是把一级缓存的默认级别设定为Statement,即不使用一级缓存。
Cache 接口
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
}
实现类:
- PerpetualCache:HashMap 保存 key,value
装饰器类
- blockingCache : 基于 key 的锁。每个 key 一个锁。注:只有当前线程才能释放锁。也可以通过设置超时时间,避免一直阻塞等待锁。问题:putObject 为什么没有加锁?
- FifoCache:FIFO 的淘汰策略。默认队列大小为 1024。
- LoggingCache:用日志记录查询的命中率
- LruCache:利用 LinkHashMap 实现 Lru 算法。由于 LinkHashMap 实现了 LRU 算法,这里非常取巧
- ScheduledCache:每间隔 clearInterval(默认一小时)将所有缓存清空。
- SerializedCache:将 value 用 ObjectInputStream,ByteArrayInputStream 序列化,对 key 没有序列化。
- SoftCache:第一次将值存储在软引用对象中,每次操作(put 和 remove)时都将已经被 GC 的 valuel 删除。每次获取(get),对应的软引用没有被删除,就加入Deqeue队列。Deque 是一个默认长度为 256 的先进先出队列。这种实现机制非常类似 Java 的 GC 的简化版。
- SynchronizedCache:每个操作前面加了 synchronized 关键词
- TransactionalCache:类似数据库事务机制。将 put 缓存在临时 Map 中,当调用 commit 的时候,才清除临时Map,写入真正 cache 中。
- WeakCache:第一次将值存储在弱引用对象中,每次操作(put 和 remove)时都将已经被 GC 的 valuel 删除。每次获取(get),对应的弱引用没有被删除,就加入Deqeue队列。Deque 是一个默认长度为 256 的先进先出队列。这种实现机制非常类似 Java 的 GC 的简化版。
- TransactionalCacheManager:保存 Cache 和 TransactionalCache 的映射关系
Cache 建造者
CacheBuilder。
二级缓存
- 二级缓存(second level cache),全局作用域缓存;二级缓存默认不开启,需要手动配置
- MyBatis提供二级缓存的接口以及实现,缓存实现要求 POJO实现Serializable接口
- 二级缓存在 SqlSession 关闭或提交之后才会生效
配置
<setting name="cacheEnabled" value="true"/>
每个 Mapper 支持 CacheNameSpace 和 CacheNameRef 来配置,先解析配置文件,后解析注解,注解的配置会覆盖配置文件的配置。配置文件先解析 cache-ref 标签,后解析 cache 标签。 注解先解析CacheNameSpace,后解析 CacheNameRef。CacheNameRef 会覆盖 CacheNameSpace的配置。因此,配置文件和接口注释是不能够配合使用的。
CacheNameSpace
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
Class<? extends org.apache.ibatis.cache.Cache> implementation() default PerpetualCache.class;
Class<? extends org.apache.ibatis.cache.Cache> eviction() default LruCache.class;
long flushInterval() default 0;
int size() default 1024;
boolean readWrite() default true;
boolean blocking() default false;
/**
* Property values for a implementation object.
* @since 3.4.2
*/
Property[] properties() default {};
}
private void parseCache() {
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
private Properties convertToProperties(Property[] properties) {
if (properties.length == 0) {
return null;
}
Properties props = new Properties();
for (Property property : properties) {
props.setProperty(property.name(),
PropertyParser.parse(property.value(), configuration.getVariables()));
}
return 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();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
支持配置文件和注解两种,配置文件先解析,之后注解,注解会覆盖配置文件。最终还是调用 CacheBuilder 来创建 Cache。
方式一:配置文件
解析 cache 标签
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.TestMapper">
<cache type="com.example.MybatisRedisCache" eviction="FIFO" flushInterval="60000" readOnly="false" size="1024" blocking="false">
<property name="key1" value="value1" />
<property name="key2" value="value2" />
</cache>
</mapper>
-
type:默认只实现了 PerPetualCache,默认也只能是PerPetualCache,如果你实现了自己的 Cache,可以配置。比如,你可以实现用 Redis 或 ECache,实现 Cache 接口,即可。
-
eviction:实现了 LruCache、FifoCache、SoftCache、WeakCache
-
flushInterval:多长时间将所有缓存全部清空。实际为 ScheculedCache
-
size:缓存数量
-
readOnly:默认 false,如果为 false,对 value 进行序列化。否则不进行序列化
-
blocking:默认 false。是否是阻塞式的,如果拿不到锁,一直阻塞。
方式二:注解方式
@CacheNamespace(implementation = MybatisRedisCache.class, size=256, flushInterval=10000, readWrite=true, blocking=false, eviction=FiFoCache.class, properties={"eviction":"LRU", "flushInterval":"100",})
public interface TestMapper(
@Select("select t_user.* from t_user where t_user.t_user_id = #{tUserId}")
@Options(useCache = true)
List<PeoplePo> getUser(PeopleVo p);
}
CacheNamespaceRef
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespaceRef {
/**
* A namespace type to reference a cache (the namespace name become a FQCN of specified type)
*/
Class<?> value() default void.class;
/**
* A namespace name to reference a cache
* @since 3.4.2
*/
String name() default "";
}
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
//不能都不配置
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
//不能都配置
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
//value 的优先级高于 name 的优先级
String namespace = (refType != void.class) ? refType.getName() : refName;
try {
assistant.useCacheRef(namespace);
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
}
}
}
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//从已有的 Cache 中查找
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
用来引用已配置的 Cache。支持配置文件和注解两种,配置文件先解析,之后注解,注解会覆盖配置文件。
方式一:配置文件
CacheNamespaceRef 为 cache-ref 标签下的 namespace
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.TestMapper">
<cache-ref namespace="MybatisRedisCache"</cache-ref>
</mapper>
方式二:注解方式
支持 value 和 name 两种方式,但只能配置一种,value 的优先级高于 name 的优先级。
@CacheNamespaceRef(implementation = MybatisRedisCache.class)
public interface TestMapper(
@Select("select t_user.* from t_user where t_user.t_user_id = #{tUserId}")
@Options(useCache = true)
List<PeoplePo> getUser(PeopleVo p);
}
配置缓存的方式
1、全局setting的cacheEnable:
- 配置二级缓存的开关。一级缓存一直是打开的。
2、select标签的useCache属性:
- 配置这个select是否使用二级缓存。一级缓存一直是使用的
3、sql标签的flushCache属性:
- 增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。
- 查询默认flushCache=false。
4、sqlSession.clearCache():
- 只是用来清除一级缓存。
5、二级缓存需要手动开启和配置,他是基于namespace级别的缓存。当在某一个作用域 (一级缓存Session/二级缓存Namespaces) 进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
缓存初始化
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;
}
有了前面的基础,这里理解起来就非常容易了。
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);
}
@Override
public Transaction getTransaction() {
return delegate.getTransaction();
}
@Override
public void close(boolean forceRollback) {
try {
//issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
@Override
public boolean isClosed() {
return delegate.isClosed();
}
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
flushCacheIfRequired(ms);
return delegate.queryCursor(ms, parameter, rowBounds);
}
@Override
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.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//加入二级缓存。问题:如果一级缓存返回 PLACEHOLDER,这里是否始终PLACEHOLDER,导致返不期望的结果?
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//缓存没有命中,从代理中查。继续查询一级缓存。
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public List<BatchResult> flushStatements() throws SQLException {
return delegate.flushStatements();
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
@Override
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
if (required) {
tcm.rollback();
}
}
}
private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
}
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
return delegate.isCached(ms, key);
}
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
delegate.deferLoad(ms, resultObject, property, key, targetType);
}
@Override
public void clearLocalCache() {
delegate.clearLocalCache();
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
@Override
public void setExecutorWrapper(Executor executor) {
throw new UnsupportedOperationException("This method should not be called");
}
}
验证
-
不提交事务,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。
-
当提交事务时,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。
-
同一个事务 sqlSession 3次查询同一个表,检查后续请求是否查询缓存
-
两个 sqlSession,一个事务 sqlSession1,sqlSession2 各连续两次查询表,sqlSession3 在 sqlSession1 的两次查询中间修改表,
SqlSession1 SqlSession2 SqlSession3 查询 查询 查询 提交 查询 更新 查询 -
两个 sqlSession,两个事务 sqlSession1 各一次查询表,sqlSession2 在两次查询中间修改表
总结
- Mybatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到Mapper级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
- Mybatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用的条件比较苛刻。
- 在分布式环境下,由于默认的Mybatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将Mybatis的Cache接口实现,有一定的开发成本,不如直接用Redis,Memcache实现业务上的缓存就好了
总结
mybatis 的 cache 设计还是很赞的。尤其是初始化部分 CacheBuilder。
但是 mybatis 的缓存总体上来说不如直接上 Redis 好。原因在于一不小心就会出现脏读。