JavaWeb 知识点

MyBatis源码系列--2.MyBatis 缓存详解

2019-04-29  本文已影响0人  WEIJAVA

缓存体系结构

缓存是一般的 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
所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存

一级缓存

一级缓存也叫本地缓存,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 来执行
查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

image.png

验证下二级缓存:
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

——学自咕泡学院

上一篇下一篇

猜你喜欢

热点阅读