美文网首页JavaWeb 知识点
MyBatis源码系列--2.MyBatis 缓存详解

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

作者: WEIJAVA | 来源:发表于2019-04-29 11:26 被阅读0次

    缓存体系结构

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

    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

    ——学自咕泡学院

    相关文章

      网友评论

        本文标题:MyBatis源码系列--2.MyBatis 缓存详解

        本文链接:https://www.haomeiwen.com/subject/nxbenqtx.html