mybatis 04 二级缓存

写在最前

一级缓存,也叫会话级缓存,在BaseExecutor中实现,并且是默认启用
二级缓存,也叫应用级缓存,它是跨线程的,由CachingExecutor实现,需要开发者自行打开
在访问顺序上靠前,加之又是应用级缓存,因此,二级缓存会有更高的命中率~~


Cache接口

个人认为,Mybatis最精妙之处在于二级缓存的代码设计
Cache模块采用了装饰器+责任链的模式,可读性和扩展性都很强,值得学习~~


封包的手法也可做学习借鉴,Cache接口、impl实现类、decorators装饰器

Cache的实现类及其作用

装饰器 描述
SynchronizedCache 同步锁,用于保证对指定缓存区的操作都是同步的
BlockingCache 阻塞器,基于key加锁,防止缓存穿透
ScheduledCache 时效检查,用于验证缓存有效器,并清除无效数据
LruCache 溢出算法,淘汰闲置最久的缓存
FifoCache 溢出算法,淘汰加入时间最久的缓存
WeakCache 溢出算法,基于java弱引用规则淘汰缓存
SoftCache 溢出算法,基于java软引用规则淘汰缓存
LoggingCache 统计器,记录缓存命中率
SerializedCache 序列化
PerpetualCache 实际存储,内部采用HashMap进行存储

责任链中的Cache们

SynchronizedCache

这个缓存实现类无其他逻辑,只是加入了synchronized关键字
可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法
不同的对象实例, synchronized方法是不相干扰的,
也就是说,其它线程可同时访问另一个对象实例中的synchronized方法

LruCache

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的淘汰算法

溢出淘汰策略,它内部维护了一个长度为1024的LinkedHashMap,命中的元素向尾部移动,链表的头部就是最近最少
补充知识:

  • HashMap和双向链表合二为一,即为LinkedHashMap
  • 为什么用LinkedHashMap,而不是直接用HashMap?链表本身机制非常适合增(改)和删,只要在链表中加一个节点即可

SerializedCache

底层默认是java的序列化算法,java的序列法算法效率不高,类似dubbo的hession方式效率就很高

为什么要序列化?二级缓存是跨线程的,若不序列化,可能被其他程序修改掉,很不安全。
所以要注意,及时是同一个线程,先后从二级缓存里获取同一个id的user对象,他们也是不相等的

System.out.println(user1==user2)  -> false

序列化和反序列化都是需要时间的,如果我们想要更快的读和写的话,在@CacheNamespace
注解中将readWrite设置为false即可,注意:此时数据线程不安全

ScheduleCache

虽然叫Schedule,但它不是定时器。通过get\putObject的时候检查下有效期,超过一定时间后,清空整个二级缓存

可以看到,超时时间默认为一小时。项目中涉及时间的,可用java自带的TimeUnit

LoggingCache

里面没有逻辑代码,就是记录下缓存的命中率:hitRatio=命中次数/请求次数:

BlockingCache

已经有SynchronizedCache了,为什么还要BlockingCache?后面再研究~

PerpetualCache

责任链的最底层是PerpetualCache,转换成字节存储存储到内存中,它是MyBatis提供的默认方案

缓存如何使用

如何使用开启

可以过两种方式:@CacheNamespace或Mappper映射文件。看下底层核心实现:
@CacheNamespace: 从代码可以看出,MyBatis已经提供了默认二级缓存实现PerpetualCache

若有必要,我们可以自定义实现类:

MapperAnnotationBuilder: 通过parceCache加载二级缓存

缓存策略

在配置阶段

  • cacheEnabled,全局缓存开关,默认true
  • useCahce,当前statement是否使用缓存,默认true
  • flushCache,这个比较复杂:
    查询场景下,默认为false;更新场景下,默认为true。它是清空当前session中某一个namespace的缓存,具体结合下文TransactionalCacheManager掌握

在开发阶段

  • 相同的statement id
  • 相同的sql与参数
  • 返回行rowbounds相同
  • 会话手动commit后(给会话设置自动提交没用)
  • 没有使用ResultHandler来自定义返回数据
  • 在调用存储过程中不能使用出参,即Parameter中mode=out|inout

缓存相互引用

不同的CacheNamespace有着独立的缓存容器,只有该CacheNamespace下的statement才能访问该缓存。但表与表之间是存在关联的。而对应的Mapper又是独立的。这时我们就可以通过缓存引用,让多个Mapper共享一个缓存。具体做法是设定@CacheNamespaceRef

对比一级缓存

MyBatis一二级缓存的CacheKey是一致的
和一级缓存不同,只有提交之后才可命中(为什么?容易存在脏读,见下图)

整体架构

事物缓存管理器(TransactionalCacheManager)

二级缓存是在事务提交后才会写入,目的是为了防止其它会话脏读缓存
所以,会话与二级缓存中间会有一个事物缓存管理器,会话其间查询的数据会放到管理器的暂存区。tcm的生命周期与会话保持一致~
当事务提交后,暂存区数据会被写入指定(红\绿为不同的CacheNamespace)二级缓存区域

暂存区(TransactionalCache)

暂时存放待缓存的数据区域,和缓存区是一一对应的
如果会话会涉及多个二级缓存的访问,那么对应暂存区也会有多个
暂存区生命周期与会话保持一致

缓存区

缓存区是通过Mapper声明而获得,默认每个Mapper都有独立的缓存区
其作用是真正存放数据和实现缓存的业务逻辑,如序列化、防止缓存穿透、缓存效期等


转载请注明来源。 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。 可以在下面评论区评论,也可以邮件至 sharlot2050@foxmail.com。

文章标题:mybatis 04 二级缓存

字数:1.5k

本文作者:夏来风

发布时间:2020-06-06, 10:37:24

原始链接:http://www.demo1024.com/blog/mybatis-04-cachelv2/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。