缓存这个东西在很多应用中都能看到它们的身影,这次就讲讲在Mybatis中的缓存是怎么应用的,虽然说吧Mybatis中的缓存基本不怎么用,用的更多是第三方组件redis、MongoDB、MemCache等等。
Mybatis的缓存是基于Map实现的,从缓存中读写数据是缓存模块的核心功能,但是除了这个功能Mybatis也提供了很多附加功能,比如防止缓存击穿、添加缓存清空策略等等,并且这些附加的功能属性可以随意组合到核心功能上。
缓存在Mybatis中的使用介绍
Myabtis中有两个缓存,一级缓存以及二级缓存,
一级缓存是默认开启的,而二级缓存是在配置文件中开启的,默认是开启,但是可以配置不开启,Mybatis配置参数定义
一级缓存的仅仅存在于同一SqlSession当中,如果关闭了SqlSession的话,那么这个缓存就没有了,
二级缓存是跨SqlSession的存在,同一个SqlSessionFactory是有效的,在多个SqlSession当中,也会存在多个二级缓存的存在,所以就容易出现数据脏读,个人建议二级缓存还是禁止比较好,使用第三方的缓存组件。
在对应的Mapper.xml当中使用:
eviction:使用什么类型的缓存,默认LRU缓存,缓存满后,将使用的最少的缓存去除;
flushInterval:刷新时间;
readOnly:是否只读;
size:缓存大小;
blocking:是否采用阻塞策略;
<cache eviction="FIFO" flushInterval="6000" readOnly="false" size="1024"></cache>
缓存的引用,直接引用对应的工作空间,引用后,就会直接使用这个引用的缓存:
<cache-ref namespace="xxx.xxx.xxxDao"></cache-ref>
设计模式
先来看看这个模块用到了哪些设计模式
装饰器模式(Decorator Pattern)
定义:允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
像这种组合附加功能的业务上也可以通过继承去组合相对应的属性,但是继承的方式是静态的,并不能控制增加新的属性,如果功能属性较多的话,那么使用继承就会有很多子类,可读性不强。
图示:
组件(Component):组件接口定义了全部组件类 和装饰器实现的行为;
组件实现类(ConcreteComponent):实现 Component接口,组件实现类就是被装饰器装饰的 原始对象,新功能或者附加功能都是通过装饰器添加 到该类的对象上的;
装饰器抽象类(Decorator):实现Component接 口的抽象类,在其中封装了一个Component 对象, 也就是被装饰的对象;
具体装饰器类(ConcreteDecorator):该实现类 要向被装饰的对象添加某些功能;
相对于继承来说,使用装饰器模式的灵活性更高,扩展性更强。
装饰器使用:
JDK中的IO输入流与输出流设计:
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt")));
在Servlet API当中的对request的封装类HttpServletRequestWrapper;
源码分析
Mybatis的缓存模块就是使用的装饰器模式
BlockingCache示例:
同一个缓存的接口Cache(这个接口是缓存模块的核心接口,定义了缓存的基本操作):
public interface Cache {/*** @return The identifier of this cache*/String getId();/*** @param key* Can be any object but usually it is a {@link CacheKey}* @param value* The result of a select.*/void putObject(Object key, Object value);/*** @param key* The key* @return The object stored in the cache.*/Object getObject(Object key);/*** @param key* The key* @return Not used*/Object removeObject(Object key);/*** Clears this cache instance.*/void clear();/*** Optional. This method is not called by the core.** @return The number of elements stored in the cache (not its capacity).*/int getSize();/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.** @return A ReadWriteLock*/default ReadWriteLock getReadWriteLock() {return null;}}
在不同属性的实现类分别封装一个Cache对象,实现了每个附加功能的组合。
FifoCache:一个先进先出的缓存,如果缓存的数量达到了临界点,则将缓存时间最长的那个缓存去除。
LoggingCache:附加了日志以及统计功能。
LruCache:如果缓存数据达到了临界点,会将访问次数最少的缓存数据清空。
ScheduledCache:会定时清除缓存的一个缓存。
SerializedCache:一个可以序列化以及反序列化的缓存。
SoftCache:软引用缓存,如果缓存被GC回收后,对应的缓存数据也会被清空。
WeakCache:弱引用缓存,如果缓存被GC回收后,对应的缓存数据也会被清空。
SynchronizedCache:同步运行的缓存。
TransactionalCache:一个存在事务提交和回滚的缓存。
BlockingCache:包含阻塞机制的缓存
在缓存模块中用到的缓存类实际上是PerpetualCache,以上的那些缓存类仅仅是附加功能,具体实现缓存功能的是PerpetualCache。
public class PerpetualCache implements Cache {private final String id;//数据被缓存的地方private final Map<Object, Object> cache = new HashMap<>();public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic boolean equals(Object o) {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}if (this == o) {return true;}if (!(o instanceof Cache)) {return false;}Cache otherCache = (Cache) o;return getId().equals(otherCache.getId());}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}return getId().hashCode();}}
一个比较简单的基于Map的缓存实现类。
缓存击穿
对于数据库来说,缓存是一个很好的东西,它能够挡住大部分的数据请求,但是如果在缓存中找不到对应的key和value时,那么这个这些请求就会去请求数据库,如果这个请求是在高并发的情况或者数据是“热数据”的情况,那么数据库访问那么也会变成高并发访问查询,这就是缓存击穿了。
所以在设计缓存模块的时候就应该考虑到这个问题,在Mybatis里面这个问题已经得到解决,BlockingCache就是解决方法。
我们先来看看缓存击穿的一些解决办法:
第一种:如果当前缓存被击穿后,不应该让所有的请求去请求数据库,而是只让一个请求去请求数据库,其他线程等待,当数据查询完毕并缓存进去后,再让其他线程去查询缓存。
但是这个方法存在一个问题,面对大量的请求来说,就会有效率问题。
第二种:既然以上方法存在效率问题,那么就可以给同等类型的key上锁,让相同的请求只派出一个请求去请求数据库,其他同等类型key的请求就等待,当请求到后,那么就释放锁其他线程回去缓存中获取数据了。
但是如果key太多的话,会比较浪费资源。
综上所述:对应这种缓存的话,个人觉得应该要与业务挂钩,取得一个平衡点取设计缓存模块。
现在我们来看看BlockingCache是怎么玩的,其实就是给每个key上锁然后,请求完在释放锁。
我目前的Mybatis版本是最新的3.5.7,可能其他版本并非使用的事CountDownLatch,而是使用的ReentrantLock,基本都是一样,基于AQS同步,想要了解线程相关的东西,可以去看看我之前发布博客哟。
CacheKey
现在来看一下缓存的key是怎么组成的,缓存模块中的key值并非直接的使用SQL语句中的唯一ID啥的。
在Mybatis设计缓存时,就将key包装成一个对象,只有这个对象的hashCode相等,这两个key才能一致。
构成CacheKey的因素有四个:
1、mappedStatment的id ;
2、指定查询结果集的范围(分页信息);
3、查询所使用的SQL语句;
4、用户传递给SQL语句的实际参数值;
update方法:
equals方法:
Mybatis的缓存基本讲的差不多了。