本文共 16484 字,大约阅读时间需要 54 分钟。
mybaits源码分析(五) 一级缓存、二级缓存详解
前言:上一篇讲解了mybaits数据源,这一篇讲解一下mybaits一级缓存、二级缓存的基本使用,以及主要实现。
本篇主要分为下面几个部分:
一级缓存、二级缓存的使用及测试
mybaits缓存相关类的介绍
一级缓存详解
二级缓存详解
一、一级缓存、二级缓存的使用及测试
在mybaits中,一级缓存是默认开启的,缓存的生命周期是sqlsession级别的,二级缓存全局配置是默认开启的,但是需要另外在namespace中也开启才可以使用二级缓存,二级缓存的生命周期是sqlsessionFactory的,缓存操作的范围是每个mapper对应一个cache(这也是为什么需要在mapper配置的namespace中开启才生效的原因)
1、在SqlMapper中加上下面配置, cacheEnabled负责开启二级缓存,logImpl负责打印sql(我们测试可以根据是否打印真实sql来判断是否走了缓存)
2、测试
/** * 一级缓存测试 : * 测试前,需要加下面logImpl这个打印sql语句的配置测试结果,会发出一次sql语句,一级缓存默认开启,缓存生命周期是SqlSession级别 */ @Test public void test2() throws Exception { InputStream in = Resources.getResourceAsStream("custom/sqlMapConfig3.xml"); SqlSessionFactory factory2 = new SqlSessionFactoryBuilder().build(in); SqlSession openSession = factory2.openSession(); UserMapper mapper = openSession.getMapper(UserMapper.class); User user1 = mapper.findUserById(40); // 会发出sql语句 System.out.println(user1); User user2 = mapper.findUserById(40); // 会发出sql语句 System.out.println(user2); openSession.close(); } /** * 二级缓存测试 * 二级缓存全局配置默认开启,但是需要每个名称空间配置 // 是为了打印sql语句用途 // 二级缓存默认就是开启的 , * 即需要全局和局部同时配置,缓存生命周期是SqlSessionFactory级别。 */ @Test public void test3() throws Exception { InputStream in = Resources.getResourceAsStream("custom/sqlMapConfig3.xml"); SqlSessionFactory factory2 = new SqlSessionFactoryBuilder().build(in); SqlSession openSession = factory2.openSession(); UserMapper mapper = openSession.getMapper(UserMapper.class); User user1 = mapper.findUserById(40); System.out.println(user1); openSession.close(); // 关闭session openSession = factory2.openSession(); mapper = openSession.getMapper(UserMapper.class); User user3 = mapper.findUserById(40); // 二级缓存全局和局部全部开启才会打印sql System.out.println(user3); }
从测试结果可以看到,mybaits在没有开启二级缓存的时候,一个sqlsession相同的查询再次执行,在没有close的情况下,会查一级缓存,在二级缓存开启的情况下,如果close后,还是能够使用缓存(使用的是二级缓存),缓存机制是:第一次查询,会先查二级缓存,没有找一级缓存,再没有查数据库,数据库查出来先放到一级缓存,再放到二级缓存,第二次查询时,会先查二级缓存,而二级缓存有货,就返回了。
二、mybaits缓存相关类的介绍
mybaits一级缓存二级缓存他们的顶级接口都是一样的,都是cache类,而默认mybaits的cache实现是一个hashMap的包装,另外附带很多对cache类实现的包装。
1、下面先看看顶级接口的Cache类和包结构。
public interface Cache { String getId(); void putObject(Object key, Object value); Object getObject(Object key); Object removeObject(Object key); void clear(); int getSize(); ReadWriteLock getReadWriteLock();}
其中除PerpetualCache以外,其他的Cache实现都是使用了装饰器模式,由底层的PerpetualCache完成实际的缓存,并在此基础上添加了其他功能。
SynchronizedCache:通过在get/put方式中加锁,保证只有一个线程操作缓存
FifoCache、LruCache:当缓存到达上限时候,通过FIFO或者LRU(最早进入内存)策略删除缓存 (这二个可以在namespace开始<cache>时配置缓存失效策略时用途)
ScheduledCache:在进行get/put/remove/getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每隔一段时间清空一次缓存
SoftCache/WeakCache:通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理掉这些缓存
TranscationCache: 事务化的包装,即一次对此缓存的操作,不会马上更新到delegate缓存中,会把操作分成移除和添加维护到map容器,待commit方法被调用,真实操作缓存。
2、缓存实现类的主要实现逻辑
1) PerpetualCache
public class PerpetualCache implements Cache { private String id; private Map
PerpetualCache 就是一个map缓存,这个是mybaits默认的缓存实现,一级缓存使用的就是这个类,如果二级缓存想使用第三方cache,都有现成的jar包可以使用,这里不进行叙述。
2)FifoCache
FifoCache是一个实现缓存先进先出的缓存包装,其实现原理是利用LinkedList先进先出的机制。
private final Cache delegate; private LinkedListkeyList; private int size; public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList (); this.size = 1024; } public void putObject(Object key, Object value) { cycleKeyList(key); delegate.putObject(key, value); } // 主要实现就是添加缓存的时候,顺便添加到list中,这样如果添加前,判断list的尺寸大于size // 就移除list中的第一个,并且delegate缓存也移除。 private void cycleKeyList(Object key) { keyList.addLast(key); if (keyList.size() > size) { Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } }
3) LruCache
LruCache是实现了LRU淘汰机制的缓存包装,其主要原理就是利用,LinkList的三个参数构造。
new LinkedHashMap<Object, View>(size, 0.75f, true),其中第三个参数accessOrder的作用是如果元素被访问的情况下,是否把元素添加到链表的尾部。结合LinkedHashMap的一个protected的removeEldestEntry方法,可以实现LRU(即最久没访问的移除)。
private MyCache delegate; private MapkeyMap; public LRUCache(MyCache delegate) { super(); this.delegate = delegate; setSize(1024); } private void setSize(int initialCapacity) { keyMap = new LinkedHashMap (initialCapacity, 0.75f, true) { private static final long serialVersionUID = 4267176411845948333L; protected boolean removeEldestEntry(Map.Entry eldest) { boolean tooBig = size() > initialCapacity; // 大于尺寸 if (tooBig) { // ture 移除delegate缓存 delegate.removeObject(eldest.getKey()); } return tooBig; // 返回true会自动移除LinkHashMap的缓存 } }; } // 其他操作的时候KeyMap进行同步
4)、SynchronizedCache
SynchronizedCache就是同步加锁的一个包装,这个就简单了,即对缓存状态变更有依赖的实现方法全部加锁。
@Override public synchronized void putObject(Object key, Object value) { delegate.putObject(key, value); } @Override public synchronized Object getObject(Object key) { return delegate.getObject(key); } @Override public synchronized Object removeObject(Object key) { return delegate.removeObject(key); }
5)、TranscationCache
TranscationCache的包装就是内部维护了二个map,一个map装remove缓存的操作,一个map装put缓存的操作,待commit的时候,遍历二个map,执行缓存住的操作,如果reset的时候,就清空这二个临时map。下面此cahce的成员变量,和二个内部类(这二个内部类就是包装缓存操作的)。
/** * 对需要添加元素的包装,并且传入了delegate缓存,调用此commit就可以用delegate缓存put进添加元素 */ private static class AddEntry { private Object key; private Object value; private MyCache delegate; public AddEntry(Object key, Object value, MyCache delegate) { super(); this.key = key; this.value = value; this.delegate = delegate; } public void commit() { this.delegate.putObject(key, value); } } /** * 对需要移除的元素的包装,并且传入了delegate缓存,调用此commit就可以用delegate移除元素。 */ private static class RemoveEntry { private Object key; private MyCache delegate; public RemoveEntry(Object key, MyCache delegate) { super(); this.key = key; this.delegate = delegate; } public void commit() { this.delegate.removeObject(key); } } // 包装的缓存 private MyCache delegate; // 待添加元素的map private MapentriesToAddOnCommit; // 待移除元素的map private Map entriesToRemoveOnCommit;
下面是核心的实现
// 添加的时候,操作的是二个map,既然添加,那么临时remove的map需要remove这个key @Override public void putObject(Object key, Object value) { this.entriesToRemoveOnCommit.remove(key); this.entriesToAddOnCommit.put(key, new AddEntry(key, value, delegate)); } @Override public Object getObject(Object key) { return this.delegate.getObject(key); } // 移除的时候,操作的是二个map,既然移除,那么临时add的map需要remove这个可以。 @Override public Object removeObject(Object key) { this.entriesToAddOnCommit.remove(key); this.entriesToRemoveOnCommit.put(key, new RemoveEntry(key, delegate)); return this.delegate.getObject(key); // 这里是为了获得返回值,注意不能用removeObject } @Override public void clear() { this.delegate.clear(); reset(); } // 提交 public void commit() { delegate.clear(); // delegate移除 for (RemoveEntry entry : entriesToRemoveOnCommit.values()) { entry.commit(); // 移除remove里的元素 } for (AddEntry entry : entriesToAddOnCommit.values()) { entry.commit(); // 添加add里的元素 } reset(); } public void rollback() { reset(); }
3、缓存key的实现
mybaits缓存的key是基于一个CacheKey的类实现的,其核心机制如下:
* 缓存Key的实现机制:通过update方法,计算hashcode,并且添加到内部的list集合,判断是否相同,也是依据内部list元素全部相同。
* 创建mybaits的CacheKey的机制:同样语句、内部分页参数offset、内部分页参数limit、预编译sql语句、参数映射。/** * CacheKey的组装 */ public static void main(String[] args) { String mappedStatementId = "MappedStatementId"; // 用字符串描述mybaits的cachekey的元素。 String rowBounds_getOffset = "rowBounds_getOffset"; String rowBounds_getLimit = "rowBounds_getLimit"; String buondSql_getSql = "buondSql_getSql"; ListparameterMappings = new ArrayList<>(); parameterMappings.add("param1"); parameterMappings.add("param2"); CacheKey cacheKey = new CacheKey(); // 创建CacheKey cacheKey.update(mappedStatementId); // 添加元素到CacheKey cacheKey.update(rowBounds_getOffset); cacheKey.update(rowBounds_getLimit); cacheKey.update(buondSql_getSql); cacheKey.update(parameterMappings); System.out.println(cacheKey); }
三、一级缓存详解
上面我们已经对缓存的类接口和其实现已经了解了,现在可以探究下一级缓存在mybaits中是怎么使用的了。
说一级缓存前,我们回顾下,sqlsession实际上是调用Executor进行操作的,而BaseExecutor是Executor的基本实现,它有其他几个实现,我们用的是SimpleExecutor,另外还有一个
CachingExecutor是实现二级缓存的。我们先从看创建session时的缓存缓存创建,然后BaseExecutor开始看一级缓存。
1、缓存创建过程(缓存创建是在创建SqlSession的时候,我们直接从SqlSsession的openSessionFromDataSource看起)
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
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类型创建不同的Executor executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); // 默认的Executor } if (cacheEnabled) { // 如果开启全局二级缓存 executor = new CachingExecutor(executor); // 就创建CachingExecutor } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
上面虽然是Executor的创建过程,实际上就是一级缓存的创建过程,一级缓存就是在Executor一个Cache的成员变量。二级缓存是实现是由CachingExecutor对Executor的包装,后续在详细分析。
2、查询入口 BaseExecutor.query开始分析
SqlSession的所有查询都是调用的Executor的query方法实现,其实现类BaseExecutor的代码如下:
publicList query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 缓存key创建 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
这个query主要是创建缓存key(上面已经讲过创建逻辑),和取出sql。再看query的调用的重载query方法。
publicList query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); // 如果配置需要清除就清除本地缓存 } List list; try { queryStack++; list = resultHandler == null ? (List ) 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(); //处理循环引用? } deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); // 如果是statement的本地缓存,就直接清除! } } return list; // 取到返回 }
在这个query方法,就是先取出本地缓存localCache(这个就是PerpetualCache),如果找到就返回,没有找到就查数据库方法queryFromDatabase。另外一级缓存可以配置成statement范围,即每次查询都会清除本地缓存。我们再看queryFromDatabase方法。
privateList queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 查询语句 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); // 查询出来放入一级缓存 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
另外如果update等操作,都会移除本地缓存。
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); }
四、二级缓存的实现
上面说了二级缓存主要是靠CachingExecutor的包装,那么我们直接分析这个类就可以了解二级缓存了。
1、 CachingExecutor成员和TransactionalCacheManager详解
public class CachingExecutor implements Executor { private Executor delegate; private TransactionalCacheManager tcm = new TransactionalCacheManager(); // TransactionalCache的管理类
TransactionalCacheManager:用于管理CachingExecutor使用的二级缓存对象,只定义了一个transactionalCaches字段
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
它的key是CachingExecutor使用的二级缓存对象,value是对应的TransactionalCache对象,下面看下它的实现。
public class TransactionalCacheManager { // 装未包装缓存和包装成Transaction缓存的map映射 private MaptransactionalCaches = new HashMap (); // 操作缓存多了一个Cache参数,实际上是调用Transaction的对应方法 public void clear(Cache cache) { getTransactionalCache(cache).clear(); } public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } // 全部缓存commit public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } // 全部缓存rollback public void rollback() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.rollback(); } } // 创建一个TransactionalCache,并把原cache为key放入map维护 private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; }}
2、二级缓存缓存逻辑
publicList 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 list = (List ) tcm.getObject(cache, key); if (list == null) { // 没有就查询 list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks } return list; // 有就返回 } } return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
上面就是简单的先查询二级缓存,如果有就返回,没有就查BaseExcute的doquery (其是先查一级缓存,找到返回,没有查数据库,查数据库时放回到一级缓存)。
其他更新、commit等操作会清除二级缓存 (这个等于是一个简单实现,等于如果只有查询,二级缓存是一直有效,如果有更新,就要清空所有的二级缓存)。
另外:
二级缓存的Cache,默认是每个namespace共享一个Cache的,这个Cache的实现,肯定也是用LRUCache等和SynchronizedCache包装过的。
这个是在 Cache cache = ms.getCache(); 这个断点查看的,具体怎么配置包装,可到Mapper.xml解析的相关流程中分析。
五:一级、二级缓存使用总结
1、一级缓存配置:
从代码可以看出,一级缓存是默认打开的,并没有任何设置或判断语句控制是否执行一级缓存查询、添加操作。所以,无法关闭掉一级缓存。
2、二级缓存配置:
二级缓存的配置有三个地方:
a、全局缓存开关,mybatis-config.xml
<settings><setting name="cacheEnabled" value="true"/></settings>
b、各个namespace下的二级缓存实例 mapper.xml
<cache/>或 引用其它namespace的缓存<cache-ref namespace="com.someone.application.data.SomeMapper"/>
c、<select>节点中配置useCache属性
默认为true,设置false时,二级缓存针对该条select语句不会生效
3、缓存范围
a、一级缓存范围
一级缓存的范围是可以配置的:
<settings><setting name="localCacheScope" value="STATEMENT"/></settings>
范围选项有:SESSION和STATEMENT,默认值为SESSION
SESSION:这种情况下会缓存一个会话(SqlSession)中执行的所有查询
STATEMENT:本地会话仅用在语句执行上,在完成一次查询后就会清空掉一级缓存。
b、二级缓存范围
二级缓存范围是namespace,即同一个namespace下的多个SqlSession共享同一个缓存
4、使用建议
不建议使用二级缓存,二级缓存是名称空间范围共享的,生命周期虽然是sqlsesisonFacotry,但是任何更新都会清空所有二级缓存,另外二级缓存在连表查询时也是存在问题的,(比如你连接了不是此名称空间的表,那个表的数据被更改,此名称空间的二级缓存是不知道的),以及其他一些问题,所以不建议使用。
一级缓存在一些极端情况下,可能存在脏数据,使用建议一个是改成STATEMENT范围,另外一个就是不要在业务逻辑上用一个sqlsession重复的查询同样的一个语句。
end!
转载地址:http://nruni.baihongyu.com/