cache定义的两种使用方式
上面说了要想使用二级缓存,需要在每个DAO对应的Mapper.xml文件中定义其中的查询语句需要使用cache来缓存数据的。
这有两种方式可以定义,一种是通过cache元素定义,一种是通过cache-ref元素来定义。
需要注意的是
对于同一个Mapper来讲,只能使用一个Cache,当同时使用了和时,定义的优先级更高(后面的代码会给出原因)。
Mapper使用的Cache是与我们的Mapper对应的namespace绑定的,一个namespace最多只会有一个Cache与其绑定。
cache元素定义
使用cache元素来定义使用的Cache时,最简单的做法是直接在对应的Mapper.xml文件中指定一个空的元素(看前面的代码),这个时候Mybatis会按照默认配置创建一个Cache对象,准备的说是PerpetualCache对象,更准确的说是LruCache对象(底层用了装饰器模式)。
具体的可看org.apache.ibatis.builder.xml.XMLMapperBuilder中的cacheElement()方法解析cache元素的逻辑。
... private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } else { this.builderAssistant.setCurrentNamespace(namespace); this.cacheRefElement(context.evalNode("cache-ref")); this.cacheElement(context.evalNode("cache"));//执行在后面 this.parameterMapElement(context.evalNodes("/mapper/parameterMap")); this.resultMapElements(context.evalNodes("/mapper/resultMap")); this.sqlElement(context.evalNodes("/mapper/sql")); this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } } catch (Exception var3) { throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3); } } ... private void cacheRefElement(XNode context) { if (context != null) { this.configuration.addCacheRef(this.builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); CacheRefResolver cacheRefResolver = new CacheRefResolver(this.builderAssistant, context.getStringAttribute("namespace")); try { cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException var4) { this.configuration.addIncompleteCacheRef(cacheRefResolver); } } } private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false).booleanValue(); Properties props = context.getChildrenAsProperties(); this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props); //如果同时存在<cache>和<cache-ref>,这里的设置会覆盖前面的cache-ref的缓存 } }
空cache元素定义会生成一个采用最近最少使用算法最多只能存储1024个元素的缓存,而且是可读写的缓存,即该缓存是全局共享的,任何一个线程在拿到缓存结果后对数据的修改都将影响其它线程获取的缓存结果,因为它们是共享的,同一个对象。
cache元素可指定如下属性,每种属性的指定都是针对都是针对底层Cache的一种装饰,采用的是装饰器的模式。
blocking:默认为false,当指定为true时将采用BlockingCache进行封装,blocking,阻塞的意思,使用BlockingCache会在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据,详情可参考BlockingCache的源码。
简单理解,也就是设置true时,在进行增删改之后的并发查询,只会有一条去数据库查询,而不会并发
eviction:eviction,驱逐的意思。也就是元素驱逐算法,默认是LRU,对应的就是LruCache,其默认只保存1024个Key,超出时按照最近最少使用算法进行驱逐,详情请参考LruCache的源码。如果想使用自己的算法,则可以将该值指定为自己的驱逐算法实现类,只需要自己的类实现Mybatis的Cache接口即可。除了LRU以外,系统还提供了FIFO(先进先出,对应FifoCache)、SOFT(采用软引用存储Value,便于垃圾回收,对应SoftCache)和WEAK(采用弱引用存储Value,便于垃圾回收,对应WeakCache)这三种策略。
这里,根据个人需求选择了,没什么要求的话,默认的LRU即可
flushInterval:清空缓存的时间间隔,单位是毫秒,默认是不会清空的。当指定了该值时会再用ScheduleCache包装一次,其会在每次对缓存进行操作时判断距离最近一次清空缓存的时间是否超过了flushInterval指定的时间,如果超出了,则清空当前的缓存,详情可参考ScheduleCache的实现。
readOnly:是否只读
默认为false。当指定为false时,底层会用SerializedCache包装一次,其会在写缓存的时候将缓存对象进行序列化,然后在读缓存的时候进行反序列化,这样每次读到的都将是一个新的对象,即使你更改了读取到的结果,也不会影响原来缓存的对象,即非只读,你每次拿到这个缓存结果都可以进行修改,而不会影响原来的缓存结果;
当指定为true时那就是每次获取的都是同一个引用,对其修改会影响后续的缓存数据获取,这种情况下是不建议对获取到的缓存结果进行更改,意为只读(不建议设置为true)。
这是Mybatis二级缓存读写和只读的定义,可能与我们通常情况下的只读和读写意义有点不同。每次都进行序列化和反序列化无疑会影响性能,但是这样的缓存结果更安全,不会被随意更改,具体可根据实际情况进行选择。详情可参考SerializedCache的源码。
size:用来指定缓存中最多保存的Key的数量。其是针对LruCache而言的,LruCache默认只存储最多1024个Key,可通过该属性来改变默认值,当然,如果你通过eviction指定了自己的驱逐算法,同时自己的实现里面也有setSize方法,那么也可以通过cache的size属性给自定义的驱逐算法里面的size赋值。
type:type属性用来指定当前底层缓存实现类,默认是PerpetualCache,如果我们想使用自定义的Cache,则可以通过该属性来指定,对应的值是我们自定义的Cache的全路径名称。
cache-ref元素定义
cache-ref元素可以用来指定其它Mapper.xml中定义的Cache,有的时候可能我们多个不同的Mapper需要共享同一个缓存的
是希望在MapperA中缓存的内容在MapperB中可以直接命中的,这个时候我们就可以考虑使用cache-ref,这种场景只需要保证它们的缓存的Key是一致的即可命中,二级缓存的Key是通过Executor接口的createCacheKey()方法生成的,其实现基本都是BaseExecutor,源码如下。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (this.closed) { throw new ExecutorException("Executor was closed."); } else { 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(int i = 0; i < parameterMappings.size(); ++i) { ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; 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 = this.configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } return cacheKey; } }
打个比方我想在MenuMapper.xml中的查询都使用在UserMapper.xml中定义的Cache,则可以通过cache-ref元素的namespace属性指定需要引用的Cache所在的namespace,即UserMapper.xml中的定义的namespace,假设在UserMapper.xml中定义的namespace是cn.chenhaoxiang.dao.UserMapper,则在MenuMapper.xml的cache-ref应该定义如下。
<cache-ref namespace="cn.chenhaoxiang.dao.UserMapper"/>
这样这两个Mapper就共享同一个缓存了
自定义cache就不介绍了。
测试二级缓存
查询测试
/** * Created with IntelliJ IDEA. * User: 陈浩翔. * Date: 2018/1/10. * Time: 下午 10:15. * Explain: */ @RunWith(SpringJUnit4ClassRunner.class) //配置了@ContextConfiguration注解并使用该注解的locations属性指明spring和配置文件之后 @ContextConfiguration(locations = {"classpath:spring.xml","classpath:spring-mybatis.xml"}) public class MyBatisTestBySpringTestFramework { //注入userService @Autowired private UserService userService; @Test public void testGetUserId(){ String userId = "4e07f3963337488e81716cfdd8a0fe04"; User user = userService.getUserById(userId); System.out.println(user); //前面说到spring和MyBatis整合 User user2 = userService.getUserById(userId); System.out.println("user2:"+user2); } }
接下来我们把Mapper中的cache元素删除,不使用二级缓存
再运行测试
对二级缓存进行了以下测试,获取两个不同的SqlSession(前面有说,Spring和MyBatis集成,每次都是不同的SqlSession)执行两条相同的SQL,在未指定Cache时Mybatis将查询两次数据库,在指定了Cache时Mybatis只查询了一次数据库,第二次是从缓存中拿的。
Cache Hit Ratio 表示缓存命中率。
开启二级缓存后,每执行一次查询,系统都会计算一次二级缓存的命中率。
第一次查询也是先从缓存中查询,只不过缓存中一定是没有的。
所以会再从DB中查询。由于二级缓存中不存在该数据,所以命中率为0.但第二次查询是从二级缓存中读取的,所以这一次的命中率为1/2=0.5。
当然,若有第三次查询,则命中率为1/3=0.66
0.5这个值可以从上面开启cache的图看出来,0.0的值未截取到~漏掉了~
注意:
增删改操作,无论是否进行提交sqlSession.commit(),均会清空一级、二级缓存,使查询再次从DB中select。
说明:
二级缓存的清空,实质上是对所查找key对应的value置为null,而非将
二级缓存的使用原则
只能在一个命名空间下使用二级缓存
由于二级缓存中的数据是基于namespace的,即不同namespace中的数据互不干扰。在多个namespace中若均存在对同一个表的操作,那么这多个namespace中的数据可能就会出现不一致现象。
在单表上使用二级缓存
如果一个表与其它表有关联关系,那么久非常有可能存在多个namespace对同一数据的操作。而不同namespace中的数据互补干扰,所以就有可能出现多个namespace中的数据不一致现象。
查询多于修改时使用二级缓存
在查询操作远远多于增删改操作的情况下可以使用二级缓存。因为任何增删改操作都将刷新二级缓存,对二级缓存的频繁刷新将降低系统性能。