MyBatis源码系列--2.MyBatis 缓存详解
缓存体系结构
缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。
public class PerpetualCache implements Cache {
private Map<Object, Object> cache = new HashMap();
}
除此之外,还有很多緩存的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。
image.png
但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)
image.png
所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存
- 基本缓存
默认是 PerpetualCache,也可以自定义比如 RedisCache、EhCache 等,具备基本功能的缓存类 - 淘汰算法缓存
1.LruCache,当缓存到达上限时候,删除最近最少使用的缓存(Least Recently Use)
2.FifoCache ,当缓存到达上限时候,删除最先入队的缓存
3.SoftCache/WeakCache ,带清理策略的缓存 通过 JVM 的软引用和弱引用来实现缓存,当 JVM
内存不足时,会自动清理掉这些缓存,基于 SoftReference 和 WeakReference - 装饰器缓存
1.LoggingCache ,带日志功能的缓存 比如:输出缓存命中率
2.SynchronizedCache ,同步缓存 基于 synchronized 关键字实现,解决并发问题
3.BlockingCache 阻塞缓存 通过在 get/put 方式中加锁,保证只有一个线程操作缓存,基于 Java 重入锁实现
4.SerializedCache 支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化
5.ScheduledCache 定时调度的缓存 在进行 get/put/remove/getSize 等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每隔一段时间清空一次缓存
6.TransactionalCache 事务缓存 在二级缓存中使用,可一次存入多个缓存,移除多
个缓存
一级缓存
一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。
缓存对象 PerpetualCache 是放在SqlSession的默认实现类DefaultSqlSession 的Executor 里面维护
public class DefaultSqlSession implements SqlSession {
private final Executor executor;
}
而具体的PerpetualCache 对象是在Executor 的几个实现类SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中持有了 PerpetualCache
public abstract class BaseExecutor implements Executor {
//一级缓存对象
protected PerpetualCache localCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
...
this.localCache = new PerpetualCache("LocalCache");
...
}
}
在同一个会话里面,多次执行相同的 SQL 语句,会直接从内存取到缓存的结果,不会再发送 SQL 到数据库。
但是不同的会话里面,即使执行的 SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存.
image.png
验证下一级缓存:
1、在同一个 session 中共享
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlog(1));//第一次获取,会打印sql语句,代表从数据库获取
System.out.println(mapper.selectBlog(1));//第二次获取,不会打印sql语句,从缓存中获取
2、不同 session 不能共享
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlog(1));//第一次获取,会打印sql语句,代表从数据库获取
System.out.println(mapper.selectBlog(1));//第二次获取,不会打印sql语句,从缓存中获取
SqlSession session1 = sqlSessionFactory.openSession();
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);//不同的session,会打印sql语句,从数据库获取
System.out.println(mapper.selectBlog(1));//第三次次获取,不会打印sql语句,从缓存中获取
3、同一个会话中,update(包括 delete)会导致一级缓存被清空
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlog(1));//第一次获取,会打印sql语句,代表从数据库获取
mapper.updateByPrimaryKey(blog);//根据id=1去更新
session.commit();//
System.out.println(mapper.selectBlogById(1));//第二次获取,还会打印sql语句,因为缓存被清空了
4、其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)
// 会话 2 更新了数据,会话 2 的一级缓存更新
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog);
session2.commit();
// 会话 1 读取到脏数据,因为一级缓存不能跨会话共享
System.out.println(mapper1.selectBlog(1));
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步
作用域
如果开启了二级缓存,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才会去取一级缓存。
二级缓存放在哪个对象中维护呢?
MyBatis 用了一个装饰器的类来维护,就是 CachingExecutor。如果启用了二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。
CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接
返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行
查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
验证下二级缓存:
1、事务不提交,二级缓存不存在
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存不会写入
// session1.commit();
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
//如果上面commit以后,虽然是不同session,但是因为开启了二级缓存,不会打印sql,直接从二级缓存中获取,
//如果没commit,还是会从数据库获取
System.out.println(mapper2.selectBlogById(1));
注:为什么事务不提交,二级缓存不生效?
因为二级缓存使用 TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache 的getObject()、putObject和 commit()方法,TransactionalCache里面又持有了真正的 Cache 对象,比如是经过层层装饰的 PerpetualCache。在 putObject 的时候,只是添加到了 entriesToAddOnCommit 里面,只有它的commit()方法被调用的时候才会调用 flushPendingEntries()真正写入缓存。它就是在DefaultSqlSession 调用 commit()的时候被调用的。
2、 在其他的 session 中执行增删改操作,验证缓存会被刷新
Blog blog = new Blog();
blog.setBid(1);
blog.setName("357");
mapper3.updateByPrimaryKey(blog);
session3.commit();
// 执行了更新操作,二级缓存失效,会打印 SQL 查询,从数据库获取
System.out.println(mapper2.selectBlogById(1))
注:为什么增删改操作会清空缓存?
在 CachingExecutor 的 update()方法里面会调用 flushCacheIfRequired(ms),isFlushCacheRequired 就是从标签里面渠道的 flushCache 的值。而增删改操作的flushCache 属性默认为 true。
第三方缓存做二级缓存
除了 MyBatis 自带的二级缓存之外,我们也可以通过实现 Cache 接口来自定义二级缓存。
MyBatis 官方提供了一些第三方缓存集成方式,比如 ehcache 和 redis:
https://github.com/mybatis/redis-cache
——学自咕泡学院