mybatis源码(九)mybatis一级缓存的使用
mybatis源码(九)mybatis一级缓存的使用
mybatis缓存分为两种:一级缓存和二级缓存
1.一级缓存:是sqlSession级别的缓存,同时mybatis的一级缓存不支持关闭
例如通过<association>和<collection>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。
a.mybatis一级缓存localCacheScope属性
-
- SESSION :缓存对整个sqlSession有效。只有当执行了update语句的时候,缓存才会被清除
- STATEMENT :缓存仅对当前执行的语句有效。当语句执行完毕后,缓存才会被清除
SqlSession提供了面向用户的API,真正执行操作的是Executor组件。
b.mybatis一级缓存实现原理:
mybatis的一级缓存实现了Cache接口。里面维护了一个map集合。
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(); /** * Optional. As of 3.2.6 this method is no longer called by the core. * * Any locking needed by the cache must be provided internally by the cache provider. * * @return A ReadWriteLock */ ReadWriteLock getReadWriteLock(); }
mybaits一级缓存PerpetualCache类的源码如下,可以看到缓存就是一个map集合
public class PerpetualCache implements Cache { private final String id; private Map<Object, Object> cache = new HashMap<Object, Object>(); public PerpetualCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public int getSize() { return cache.size(); } @Override public void putObject(Object key, Object value) { cache.put(key, value); } @Override public Object getObject(Object key) { return cache.get(key); } @Override public Object removeObject(Object key) { return cache.remove(key); } @Override public void clear() { cache.clear(); } @Override public ReadWriteLock getReadWriteLock() { return null; } @Override public boolean equals(Object o) { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } if (this == o) { return true; } if (!(o instanceof Cache)) { return false; } Cache otherCache = (Cache) o; return getId().equals(otherCache.getId()); } @Override public int hashCode() { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } return getId().hashCode(); } }
Cache接口的实现类如下:

Cache接口使用装饰者模式,可以对某些Cache进行增强,比如说如下测试代码
@Test 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()); }
也可以通过CacheBuilder生成器模式创建缓存对象,代码如下:
@Test public void testCacheBuilder() { final int N = 100000; Cache cache = new CacheBuilder("com.blog4java.mybatis.example.mapper.UserMapper") .implementation( PerpetualCache.class) .addDecorator(LruCache.class) .clearInterval(10 * 60L) .size(1024) .readWrite(false) .blocking(false) .properties(null) .build(); for (int i = 0; i < N; i++) { cache.putObject(i, i); } System.out.println(cache.getSize()); }
mybatis中一级缓存的执行代码如下:
BaseExecutor的部分源码如下:
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// Mybatis一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
// 创建一级缓存对象
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
MyBatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象 (CacheKey对象决定了缓存的Key与哪些因素有关系)。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。CacheKey对象 通过BaseExecutor类的createCacheKey()方法创建,代码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取BoundSql对象,BoundSql是对动态SQL解析生成的SQL语句和参数映射信息的封装
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建CacheKey,用于缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 调用重载的query()方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
@Override
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可能与下面的因素有关
1.mapper的id。由mapper的命名空间的+<select|insert|update|delete> 的id值组合在一起
2.查询结果的偏移量和查询的条数
3.具体的sql语句及sql语句中需要传递的参数
4.mybatis的主配置文件中。通过<envienoment>标签配置的环境信息的对应的id的属性值
执行两次查询语句时,只有上面的信息完全相同时,才会认为两次查询执行相同的sql语句。缓存才会生效
执行查询的方法
@SuppressWarnings("unchecked")
@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 {
// 缓存中获取不到,则调用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类的query()方法中, 首先根据缓存Key从localCache属性中查找是否有缓存对象,如果查找不到,则调用queryFromDatabase()方法从数据库中获取数据,然后将数据
写入localCache对象中。如果localCache中缓存了本次查询的结果,则直接从缓存中获取。需要注意的是,如果localCacheScope属性设置为STATEMENT,则每次查询操作完成后,都会调用clearlocalCache()
方法清空缓存。除此之外,MyBatis会在执行完任意 更新语句后清空缓存,我们可以看一下BaseExecutor类的update()方法,代码如下:
@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.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
// 可以看到,MyBatis在调 用doUpdate()方法完成更新操作之前,首先会调用clearlocalCache()方法清空缓存。
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
3.mybatis支持使用redis、ehcache作为二级缓存

浙公网安备 33010602011771号