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作为二级缓存