MyBatis系列之Mybatis缓存深入了解

Mybatis缓存 

走进Mybats的缓存包中,我们发现它只有一个cache的接口和一个缓存子类的实现。

MyBatis系列之Mybatis缓存深入了解_第1张图片

再看看它上面有个装饰器的包,装饰者模式当然就是给功能做一些加强的,那么我们来看看它有啥加强功能,blockingCache,带锁的缓存,从里面可以看到它是重入锁实现的,loggingCache,日志缓存等。

MyBatis系列之Mybatis缓存深入了解_第2张图片

好我们可以看出它的构成主要是一个perpretualCache和10的装饰类来增强,那什么时候要用什么包装呢?为什么要这样?

首先我们要了解下MyBatis的一级缓存和二级缓存。

 

一级缓存

一级缓存作用范围:会话级别(SqlSession)

既然一级缓存的作用范围是会话级别的,那么这个缓存是不是应该放在DefaultSqlSession中呢?

MyBatis系列之Mybatis缓存深入了解_第3张图片

我们进入这个类看看,发现并没有这个成员变量,它只能被放到成员的两个对象中了,那全局配置类是不可能放的,放了不就变成了全局配置嘛,所以猜测肯定是放在了Executor执行器中。

那么之前我们说过Executor执行器有三种,通过注解也能指定,分别为simple(简单)、reuse(可复用)、batch(可复用可批量),那我们去执行器中看看吧。

MyBatis系列之Mybatis缓存深入了解_第4张图片

找到执行器Executor,发现它的子类有个公共的父类,BaseExecutor,再看看里面的成员属性,果然有PerpetualCache,所以,其实它的缓存构建方式是,当我们请求一个SqlSession,它会生成一个执行器,而执行器中会生成缓存对象。

MyBatis系列之Mybatis缓存深入了解_第5张图片

这也验证了上面的结论,一级缓存是存在sqlsession的执行器中的,它的作用域也就在一次会话中。

我们也可以用代码来证明一下。

/**
     * 测试一级缓存需要先关闭二级缓存,localCacheScope设置为SESSION
     * @throws IOException
     */
    @Test
    public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            Blog blog = mapper0.selectBlogById(1);
            System.out.println(blog);

            System.out.println("第二次查询,相同会话,获取到缓存了吗?");
            System.out.println(mapper1.selectBlogById(1));

            System.out.println("第三次查询,不同会话,获取到缓存了吗?");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));

        } finally {
            session1.close();
        }
    }

MyBatis系列之Mybatis缓存深入了解_第6张图片 

 从上面的代码我们可以看出只有同一个session中的sql查询才会去缓存中寻找,不同session缓存是不能共用,它会重新去发送SQL语句。

 

如何关闭一级缓存

只需要把localCacheScope的范围缩小到一个statement就行了

一级缓存失效

当我们在同一个会话中,查询缓存之后,又对数据做了修改,此时缓存就会失效。

我们可以给某个标签加上flushCache为true来清楚缓存,但查询的flushCache默认为false,默认不清楚,增删改的flushCache默认为true,自动清空缓存,如果有必要,查询也可以设置为true来情况缓存。用代码来实现一下。

 

/**
     * 一级缓存失效
     * @throws IOException
     */
    @Test
    public void testCacheInvalid() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlogById(1));

            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("2019年1月6日14:39:58");
            mapper.updateByPrimaryKey(blog);
            session.commit();

            // 相同会话执行了更新操作,缓存是否被清空?
            System.out.println("在执行更新操作之后,是否命中缓存?");
            System.out.println(mapper.selectBlogById(1));

        } finally {
            session.close();
        }
    }

 MyBatis系列之Mybatis缓存深入了解_第7张图片

 我们从结果中可以看出,增删改操作后,缓存会自动清除。

 

 一级缓存的脏数据产生

开启两个对话,当对话一读取了一条数据在缓存中,之后对话二对这条数据进行更新,对话一不会自动更新缓存,导致读取一级缓存中的老数据。看下代码。

/**
     * 因为缓存不能跨会话共享,导致脏数据出现
     * @throws IOException
     */
    @Test
    public void testDirtyRead() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));

            // 会话2更新了数据,会话2的一级缓存更新
            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("after modified 112233445566");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            mapper2.updateByPrimaryKey(blog);
            session2.commit();

            // 其他会话更新了数据,本会话的一级缓存还在么?
            System.out.println("会话1查到最新的数据了吗?");
            System.out.println(mapper1.selectBlogById(1));
        } finally {
            session1.close();
            session2.close();
        }
    }

MyBatis系列之Mybatis缓存深入了解_第8张图片

从代码的结果来看,它确实读到了脏数据。那这种情况要怎么来解决呢?此时,我们就需要一个作用范围更广的缓存,也就是二级缓存。

 

二级缓存

二级缓存作用域:namespace级别,只要是同一个命名空间下的调用都能使用二级缓存,所以二级缓存应该作用于一级缓存之前。

MyBatis系列之Mybatis缓存深入了解_第9张图片

 那么二级缓存应该放到什么对象中来维护呢?显然SqlSession中的全局配置对象是不能放的,本线程执行器放的又是一级缓存,它到底应该放在哪里呢?它往上一级的工厂嘛?显然不合理,工厂就是来产生类的,不合适不是。

所以,mybatis想到,把二级缓存放到执行器的一个装饰者类中。

MyBatis系列之Mybatis缓存深入了解_第10张图片

通过代码我们可以看出,Executor有个子类叫做CachingExecutor ,它用这个包装类包装了执行器,里面还有一个事务缓存管理器,这样就能达到比sqlsession更大范围的缓存管理了。

private final Executor delegate; //被包装的对象,也就是真正的执行器,还是要把sql丢在这里处理
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

MyBatis系列之Mybatis缓存深入了解_第11张图片

二级缓存的工作模型。

MyBatis系列之Mybatis缓存深入了解_第12张图片 

 

 配置二级缓存

首先,mybatis的二级缓存和一级缓存是默认开启的,在全局配置文件中可以设置开关,但是如果只是在全局配置是不够的,还需要在具体的mapper中来配置,既然二级缓存的范围是在namspace下,那么它自然应该配置在mapper中了。

MyBatis系列之Mybatis缓存深入了解_第13张图片 

MyBatis系列之Mybatis缓存深入了解_第14张图片 

我们只需要在mapper下声明一个标签就能默认开启二级缓存了,当然我们也可以配置一些具体的属性。

MyBatis系列之Mybatis缓存深入了解_第15张图片

1、size:默认缓存上限1024。

2、eviction:默认缓存淘汰策略LRU, Least Recently Use,最近最少使用的缓存淘汰掉(FIFO,先进先出;WEAK,基于弱引用的回收策略;SOFT基于软引用的淘汰策略)。

3、flushInterval:缓存多久清空一次。

4、readOnly:缓存对象是否只读,默认是false,通过读写copy序列化来操作,更加安全;true,直接返回对象,速度更快更不安全。

 

二级缓存解决跨会话共享问题

当我们开启二级缓存以后,首先看看能不能解决缓存数据共享问题,看代码。

 /**
     * 测试二级缓存一定要打开二级缓存开关
     * @throws IOException
     */
    @Test
    public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            // 事务不提交的情况下,二级缓存会写入吗?
            session1.commit();

            System.out.println("第二次查询");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));
        } finally {
            session1.close();
        }
    }

MyBatis系列之Mybatis缓存深入了解_第16张图片

显然,二级缓存,可以跨会话共享。

 

二级缓存解决读到脏数据问题

那还会出现一级缓存的读到脏数据现象吗,看代码?

/**
     * 测试二级缓存一定要打开二级缓存开关
     * @throws IOException
     */
    @Test
    public void testCacheInvalid() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        SqlSession session3 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            BlogMapper mapper3 = session3.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            session1.commit();

            // 是否命中二级缓存
            System.out.println("是否命中二级缓存?");
            System.out.println(mapper2.selectBlogById(1));

            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("2020-4-2");
            mapper3.updateByPrimaryKey(blog);
            session3.commit();

            System.out.println("更新后再次查询,是否命中二级缓存?");
            // 在其他会话中执行了更新操作,二级缓存是否被清空?
            System.out.println(mapper2.selectBlogById(1));

        } finally {
            session1.close();
            session2.close();
            session3.close();
        }
    }

MyBatis系列之Mybatis缓存深入了解_第17张图片

 

MyBatis系列之Mybatis缓存深入了解_第18张图片 

 MyBatis系列之Mybatis缓存深入了解_第19张图片

ok,完美解决了读到脏数据的问题,当其它会话更新操作之后,二级缓存会被清空,这样就需要再次去查询数据库,确保不会读到更改之前的脏数据。 

 

单方法关闭二级缓存

只需要在方法中设置是否启用缓存即可。

 

什么时候要去开启二级缓存呢?

1、以查询为主的业务:二级缓存是在同个namespace里面共享,可以支持跨会话,任意一个会话执行了增删改,都会清空二级缓存,如果频繁被清空就没有意义了。

2、只操作单表的业务:一个mapper操作一个表,如果其它mapper也要操作到我们这个表,那么也会清空二级缓存。

所以发现,增删改的操作都会清空二级缓存,这样就非常鸡肋了,一般线上项目中会使用一个单独的缓存中间件来实现,比如redis。

 

你可能感兴趣的:(MyBatis系列之Mybatis缓存深入了解)