mybatis缓存机制
mybatis包含缓存机制,可以方便的配置和定制。
默认定义了一级缓存和二级缓存。
- 默认情况下,只有一级缓存开启(sqlSession级别的缓存,也称本地缓存)
- 二级缓存需要手动开启和配置,是基于namespace级别的缓存(全局缓存)
- 为了提高扩展性。Mybatis定义了缓存接口Cache,可以通过实现Cache接口来自定义二级缓存
根据ExecutorType的不同来创建不同的执行器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; }
|
一级缓存
一级缓存是sqlSession级别的缓存,默认开启,在同一次数据库会话期间查询到的数据会放在本地缓存中,以后获取相同的数据,只需要从缓存中取,没必要查数据库,减少数据库的访问,在commit时,会清空sqlSession的缓存
sqlSession中有一个HashMap,不同sqlSession键缓存数据互相不影响
在参数和sql完全相同的情况下,使用同一个sqlSession对象调用,就可以直接从一级缓存中获取
如何判断两次查询相同
根据cacheKey是否相同来进行判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); 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); } } if (configuration.getEnvironment() != null) { cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
|
清空一级缓存的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); flushStatements(); if (required) { transaction.commit(); } }
public void close(boolean forceRollback) { try { try { rollback(forceRollback); } finally { if (transaction != null) { transaction.close(); } } } catch (SQLException e) { log.warn("Unexpected exception on closing transaction. Cause: " + e); } finally { transaction = null; deferredLoads = null; localCache = null; localOutputParameterCache = null; closed = true; } }
public void clearLocalCache() { if (!closed) { localCache.clear(); localOutputParameterCache.clear(); } }
|
一级缓存失效的情况
- sqlSession不同
- sqlSession相同,查询条件不同(此时该数据在一级缓存中还没有)
- sqlSession相同,但是在两次查询之间执行了增删改操作(这次增删改可能会对当前数据有影响)
- sqlSession相同,手动清除了一级缓存 session.clearCache()
二级缓存
二级缓存是Mapper级别的(或者说是namespace级别的),一个namespace对应一个二级缓存,不同namespace查出的数据会放在不同的map中,namespace级别的缓存,可以跨sqlSession进行共享
开启二级缓存后,会使用CacheExecutor来装饰Executor,在查询数据时,先查询二级缓存,二级缓存没有再去查一级缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
|
二级缓存的使用
开启全局二级缓存配置
1 2 3 4
| <settings> <setting name="cacheEnabled" value="true"/> </settings>
|
在映射文件中配置使用二级缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
<cache eviction="FIFO" flushInterval="60000" readOnly="true" size="1024"/>
<select id="findOrderList" resultMap="baseMap" useCache="false">
|
由于可能会用到序列化和反序列化,所以使用缓存的对象要实现序列化接口(readOnly为false的时候需要用到序列化和反序列化)
否则会报java.io.NotSerializableException异常
注意:一定要在同一个sqlSessionFactory下的不同sqlSession下使用二级缓存,如果为不同的sqlSessionFactory,永远不可能命中二级缓存的(我测试的时候就犯糊涂了,找了半天配置的问题才反应过来)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test public void testTwoLevelCache(){ SqlSession session = sqlSessionFactory.openSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.selectUser(8); System.out.println(user); userMapper.updateUser(user); session.close(); SqlSession session1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = session1.getMapper(UserMapper.class); User user1 = userMapper1.selectUser(8); System.out.println(user1); System.out.println(user == user1);
session1.close(); }
|
二级缓存失效的情况
- 如果第一个sqlSession没有提交,第二个sqlSeesion是无法命中二级缓存中该数据的,(sqlSession提交的时候才会将数据存入到二级缓存)
- 两次查询之间包含了增删改操作(在增删改操作时默认会刷新缓存,导致缓存失效)
自定义缓存
实现 org.apache.ibatis.cache.Cache 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() { return null; }
}
|
以FifoCache为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public class FifoCache implements Cache {
private final Cache delegate; private final Deque<Object> keyList; private int size;
public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList<>(); this.size = 1024; }
@Override public String getId() { return delegate.getId(); }
@Override public int getSize() { return delegate.getSize(); }
public void setSize(int size) { this.size = size; }
@Override public void putObject(Object key, Object value) { cycleKeyList(key); delegate.putObject(key, value); }
@Override public Object getObject(Object key) { return delegate.getObject(key); }
@Override public Object removeObject(Object key) { return delegate.removeObject(key); }
@Override public void clear() { delegate.clear(); keyList.clear(); }
private void cycleKeyList(Object key) { keyList.addLast(key); if (keyList.size() > size) { Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } }
}
|
缓存相关配置总结
全局配置文件settings中配置 cacheEnabled=true 该配置只影响二级缓存,对于一级缓存没有影响
每个select标签都有useCache=”true” 默认为true,该配置只影响二级缓存,对于一级缓存没有影响
每个增删改标签都有flushCache=”true”,增删改操作执行后清除缓存,该清除会清除一级和二级缓存,默认true
如果在select上使用flushCache=”true”,则查询不会使用缓存,默认false
sqlSession.clearCache() 只是清除一级缓存,不会清除二级缓存
全局配置文件settings中配置localCacheScope 本地缓存作用域(只针对一级缓存),有两个取值SESSION|STATEMENT,默认是SESSION
可以使用STATEMENT来禁用一级缓存