前面我们说了一级缓存,但是实际上我们说那玩意其实不咋实用。于是既然设计了缓存体系,就不可能弄个不实用的给人们。所以这里就引出二级全局缓存。
全局缓存就是无视sqlSession,你可以理解为一个分布式的缓存。作为全局的访问。
一、二级缓存
1、开启方式
二级缓存默认是不开启的,所以他需要你手动去开启。开启方式需要满足下面四个条件。
1、需要在核心配置文件,我的是sqlMapConfig.xml中指定,在<setting 中的配置。其实最新版mybatis这个条件其实可以不用,因为在configuration中的cache默认就是true。
2、需要在mapper.xml中引进二级缓存Cache的标签。
3、需要在查询标签<select中引入useCache属性打开。,其实这个也是可以不用配置的,我们这里就先配置上。
4、需要有事务的存在。
第一步和第三步可以不写。
于是我们就来操作验证一下,我们首先在UserMapper.xml中配置第二步和第三步。
<mapper namespace="com.yx.dao.IUserDao"><cache></cache><select id="findAll" resultType="com.yx.domain.User" useCache="true">SELECT * FROM `user`</select>
</mapper>
然后在代码中开启事务的提交。
@Test
public void test3() throws IOException {// 读取配置文件转化为流InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = factory.openSession();SqlSession sqlSession2 = factory.openSession();//这⾥不调⽤SqlSession的api,⽽是获得了接⼝对象,调⽤接⼝中的⽅法。使用JDK动态代理产生代理对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);// 第一次执行查询List<User> userList = userDao.findAll();userList.forEach(user -> {System.out.println(user.toString());});// 事务提交sqlSession.commit();System.out.println("*************************************************");// 第二次执行相同的查询List<User> userList2 = userDao2.findAll();userList2.forEach(user -> {System.out.println(user.toString());});// 事务提交sqlSession2.commit();
}
我们看下输出:
Opening JDBC Connection
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
Created connection 203819996.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c260bdc]
==> Preparing: SELECT * FROM `user`
==> Parameters:
<== Columns: id, name
<== Row: 1, 张飞
<== Row: 2, 关羽
<== Row: 3, 孙权
<== Total: 3
User{id=1, username='张飞'}
User{id=2, username='关羽'}
User{id=3, username='孙权'}
*************************************************
As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
Cache Hit Ratio [com.yx.dao.IUserDao]: 0.5// 缓存命中
User{id=1, username='张飞'}
User{id=2, username='关羽'}
User{id=3, username='孙权'}
我们看到缓存被命中了,第二次新的sqlsession中并没有执行sql,也没有创建连接,可见mybatis内部是维护着二级缓存的。而且我们看到命中率是0.5,其实就是我们查了两次,只有第二次命中了,就是百分之五十。
二、源码设计
1、装饰器的使用
我们前面说过装饰器模式,https://blog.csdn.net/liuwenqiang1314/article/details/135583787?spm=1001.2014.3001.5501
在这个二级缓存这里可以看到一个类起主要作用,就是CachingExecutor,他是Executor接口的实现类。而他和其他的SimpleExecutor,BatchExecutor有啥区别呢。
我们上一章知道BatchExecutor和SimpleExecutor通过继承BaseExecutor这个适配器来进行操作数据库。
而CachingExecutor是直接实现了Executor,实际上他是一个装饰器,他用来装饰其他的Executor。怎么证明呢。我们来想想Executor是在哪里创建呢,我们前面说Configuration的时候,知道Configuration不仅仅包罗万象,而且创建了很多基本操作类,其中就包括Exector。我们可以看到他的源码位于。
org.apache.ibatis.session.Configuration#newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;// 根据参数类型创建不同的executor ,默认是SimpleExecutorif (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}/*** 我们看到这里有个配置就是当开启这个cache的时候,就去创建缓存的 CachingExecutor* 这里我们说cacheEnabled默认值就是true,protected boolean cacheEnabled = true;* 这个变量其实就是mybatis核心配置文件里面的那个setting里面配置的缓存开启,* 最后封装在Configuration的cacheEnabled里面,不过默认为true,不用配就开了*/if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}
我们前面说过装饰器的时候,我们说就是套娃,这里可以看到他把上面创建的BatchExecutor,或者是SimpleExecutor或者是ReuseExecutor。为了方便说下面我们统一叫做SimpleExecutor。所以这里就套娃了,他把SimpleExecutor套入CachingExecutor做装饰器的增强。
于是我们就跟入这个增强类也就是CachingExecutor来看一下这个增强发生的地方。
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();// 套娃就发生在这里,我们看到他把SimpleExecutor交给了CachingExecutor 的delegatepublic CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}
我们看到他把SimpleExecutor交给了CachingExecutor 的delegate,那么增强到底发生在哪里呢,既然缓存是针对查询的,那么我们就去看CachingExecutor的查询方法。也就是org.apache.ibatis.executor.CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 去MappedStatement中获取缓存,开启了缓存,其实就是UserMapper.xml中的<cache></cache>这个配置开启了没有// 因为MappedStatement封装的就是mapper文件,所以这个变量就是封装在MappedStatement中的Cache cache = ms.getCache();// 如果存在,也就是开启了if (cache != null) {// 如果你在sql中配置了flushCache="true",那就会来刷新缓存,实时查询,保证一致性。但是一般不配置,不然缓存其实就没用了,但是有些查询可能需要这个实时性,那就需要在这类sql上配置。而且update操作就会刷新缓存。避免脏数据,而且清空的就是这个MappedStatement ,其他的并不清空。flushCacheIfRequired(ms);// 如果你的sql标签上指定了useCache=true,现在也是默认的了,不用管了if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")// 此时有缓存,直接就拿了,tcm就是装饰器增强的业务,list是为了尊重sql的排序,sql查出来啥顺序就是啥顺序,set和map可能会导致顺序和sql的不一致List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {// 这里是你开启了<cache></cache>,然后发现没缓存,其实就是第一次,那也来这里查一次库list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 设置第一次查的结果去缓存tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}// 如果没开启<cache></cache>,那就去查SimpleExecutor这个装饰器被装饰的Executor中的查询方法,return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
2、何时创建cache
我们说我们配置了cache标签才会使用到,所以那一定是在解析这个标签的时候才触发的。但是我们又知道在核心配置文件中的那个已经没用了,我们主要是在mapper中配置的那个cache才是有用的,于是我们就应该去找到解析mapper封装MappedStatement 的地方去找。路子就是
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);中的build方法,往下
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
然后其中的mapperElement(root.evalNode(“mappers”));是解析mapper文件的,我们点进去。其中的mapperParser.parse();一看就是解析的,点进去。
configurationElement(parser.evalNode(“/mapper”));这句一看就是找到根目录开始解析。点进去。
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {try {String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.isEmpty()) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);cacheRefElement(context.evalNode("cache-ref"));// 处理缓存的,点进去cacheElement(context.evalNode("cache"));parameterMapElement(context.evalNodes("/mapper/parameterMap"));resultMapElements(context.evalNodes("/mapper/resultMap"));sqlElement(context.evalNodes("/mapper/sql"));buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);}}
cacheElement(context.evalNode(“cache”));就是判断这个缓存标签的,我们就知道这里是处理的,点进去看看。
private void cacheElement(XNode context) {if (context != null) {String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);String eviction = context.getStringAttribute("eviction", "LRU");Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);Long flushInterval = context.getLongAttribute("flushInterval");Integer size = context.getIntAttribute("size");boolean readWrite = !context.getBooleanAttribute("readOnly", false);boolean blocking = context.getBooleanAttribute("blocking", false);Properties props = context.getChildrenAsProperties();// 我们看到这里开始使用新的缓存,其实就是这里创建的。builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}}
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {// 这里是个建造者模式Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();configuration.addCache(cache);currentCache = cache;return cache;}
此时就创建出来缓存对象了。也就是我们前面说的装饰器模式的那个Cache,不过后面你会套娃,给他不断增强。虽然默认的也没有。我们来看下他怎么build的,我们点进去build的实现。
public Cache build() {setDefaultImplementations();// 如果你自己实现了cache,这里就会执行你的那个缓存方式,创建你的cache,比如你用redis,就会创建redis方式的cache,至于怎么创建,后面我们再看// 底层就是反射,你要是没实现,那就还是他自己的PerpetualCacheCache cache = newBaseCacheInstance(implementation, id);// cache的seeting有多个配置,这里设置进去你自己配置的多个,后面我们来看,类似这样/*<cache><property name="" value=""/><property name="" value=""/></cache>*/setCacheProperties(cache);// issue #352, do not apply decorators to custom caches// 你没自定义扩展,这里就是默认的PerpetualCache就进这里if (PerpetualCache.class.equals(cache.getClass())) {// 针对你自己的decorators,来进行增强装饰,你也可以自己配置for (Class<? extends Cache> decorator : decorators) {cache = newCacheDecoratorInstance(decorator, cache);setCacheProperties(cache);}// 没有你自己的,那就直接进行装饰,装饰的方法也是根据你的配置eviction="FIFO"就是把默认的换成FIFO/*** <cache size="" flushInterval="" blocking="" readOnly="">* <property name="" value=""/>* <property name="" value=""/>* </cache>*//**MetaObject metaCache = SystemMetaObject.forObject(cache);if (size != null && metaCache.hasSetter("size")) {metaCache.setValue("size", size);}// 配的定时清理就是任务的装饰器,下面有各自的,可以多配,顺序套娃即可,你加了才会给你配置,不加就是默认那两个LoggingCache,SynchronizedCacheif (clearInterval != null) {cache = new ScheduledCache(cache);((ScheduledCache) cache).setClearInterval(clearInterval);}if (readWrite) {cache = new SerializedCache(cache);}// 这两个是都有的cache = new LoggingCache(cache);cache = new SynchronizedCache(cache);if (blocking) {// 你赔了这个就是走的阻塞队列装饰增强cache = new BlockingCache(cache);}return cache;*/cache = setStandardDecorators(cache);}// 如果不是默认的,那就给你加强一个装饰器,就是日志装饰器的套娃,其实就是一旦你不默认打日志出来让你看看else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {cache = new LoggingCache(cache);}return cache;}
所以你能看到他会整合你的配置,进行对应的增强,创建出新的cache对象(可能是redis这种),后面再通过装饰器模式,进行功能的增强。这样,cache对象就创建出来了,最终放到mappstatement里面。于是我们就知道CachingExecutor 在使用缓存的时候,需要通过MappedStatement 来获取缓存。Cache cache = ms.getCache();
也就是org.apache.ibatis.executor.CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql){// 去MappedStatement中获取缓存,开启了缓存,其实就是UserMapper.xml中的<cache></cache>这个配置开启了没有// 因为MappedStatement封装的就是mapper文件,所以这个变量就是封装在MappedStatement中的Cache cache = ms.getCache();
三、一级二级缓存的时机
我们现在知道一级缓存和二级缓存了,那么问题来了,当我开启二级缓存的时候,是先触发一级缓存呢还是二级缓存呢。我们来看下创建Exector的地方。
org.apache.ibatis.session.Configuration#newExecutor
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);}/*** 我们看到这里有个配置就是当开启这个cache的时候,就去创建缓存的 CachingExecutor* 这里我们说cacheEnabled默认值就是true,protected boolean cacheEnabled = true;* 这个变量其实就是mybatis核心配置文件里面的那个setting里面配置的缓存开启,* 最后封装在Configuration的cacheEnabled里面,不过默认为true,不用配就开了*/if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}
我们看到最后其实当你开启二级缓存的时候他会给你创建出来的是executor = new CachingExecutor(executor);就是CachingExecutor。
CachingExecutor包装了(套娃了)SimpleExecutor,而SimpleExecutor的查询功能是放在适配器BaseExecutor中的。所以他的处理链路是。
CachingExecutor->BaseExecutor->SimpleExecutor
CachingExecutor是去查二级缓存的,BaseExecutor是去查一级缓存的,所以其实是先二级再一级。可以debug看一下。
1、debug验证
断点跟这个方法就看到了。今天乙流,就不弄了,很简单的。
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)