博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
mybaits源码分析(五) 一级缓存、二级缓存最详细讲解
阅读量:4075 次
发布时间:2019-05-25

本文共 16484 字,大约阅读时间需要 54 分钟。

mybaits源码分析(五) 一级缓存、二级缓存详解

前言:上一篇讲解了mybaits数据源,这一篇讲解一下mybaits一级缓存、二级缓存的基本使用,以及主要实现。

本篇主要分为下面几个部分:

一级缓存、二级缓存的使用及测试

mybaits缓存相关类的介绍

一级缓存详解

二级缓存详解

一、一级缓存、二级缓存的使用及测试

在mybaits中,一级缓存是默认开启的,缓存的生命周期是sqlsession级别的,二级缓存全局配置是默认开启的,但是需要另外在namespace中也开启才可以使用二级缓存,二级缓存的生命周期是sqlsessionFactory的,缓存操作的范围是每个mapper对应一个cache(这也是为什么需要在mapper配置的namespace中开启才生效的原因)

1、在SqlMapper中加上下面配置, cacheEnabled负责开启二级缓存,logImpl负责打印sql(我们测试可以根据是否打印真实sql来判断是否走了缓存)

2、测试

/**	 * 一级缓存测试 :	 * 	测试前,需要加下面logImpl这个打印sql语句的配置		 
// 是为了打印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(); } /** * 二级缓存测试 * 二级缓存全局配置默认开启,但是需要每个名称空间配置
, * 即需要全局和局部同时配置,缓存生命周期是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方式中加锁,保证只有一个线程操作缓存

FifoCacheLruCache:当缓存到达上限时候,通过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
cache = new HashMap
();

PerpetualCache 就是一个map缓存,这个是mybaits默认的缓存实现,一级缓存使用的就是这个类,如果二级缓存想使用第三方cache,都有现成的jar包可以使用,这里不进行叙述。

2)FifoCache  

FifoCache是一个实现缓存先进先出的缓存包装,其实现原理是利用LinkedList先进先出的机制。

private final Cache delegate;  private LinkedList keyList;  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 Map
keyMap; 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 Map
entriesToAddOnCommit; // 待移除元素的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";		List
parameterMappings = 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的代码如下:

public 
List
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方法。

public 
List
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方法。

private 
List
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 Map
transactionalCaches = 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、二级缓存缓存逻辑

public 
List
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/

你可能感兴趣的文章
项目导入时报错:The import javax.servlet.http.HttpServletRequest cannot be resolved
查看>>
linux对于没有写权限的文件如何保存退出vim
查看>>
Windows下安装ElasticSearch6.3.1以及ElasticSearch6.3.1的Head插件
查看>>
IntelliJ IDEA 下的svn配置及使用的非常详细的图文总结
查看>>
【IntelliJ IDEA】idea导入项目只显示项目中的文件,不显示项目结构
查看>>
ssh 如何方便的切换到其他节点??
查看>>
JSP中文乱码总结
查看>>
Java-IO-File类
查看>>
Java-IO-java的IO流
查看>>
Java-IO-输入/输出流体系
查看>>
Java实现DES加密解密
查看>>
HTML基础
查看>>
Java IO
查看>>
Java NIO
查看>>
Java大数据:Hbase分布式存储入门
查看>>
大数据学习:Spark RDD操作入门
查看>>
大数据框架:Spark 生态实时流计算
查看>>
大数据入门:Hive和Hbase区别对比
查看>>
大数据入门:ZooKeeper工作原理
查看>>
大数据入门:Zookeeper结构体系
查看>>