研究一天,终于把MyBatis的一级缓存和二级缓存搞清楚了

 介绍

MyBatis 现在是面试必问的,其中最主要的除了一些启动流程,基础语法,那么就是缓存问题了。

大家都知道 MyBatis 是有二级缓存,底层都是用HashMap实现的,key为CacheKey对象(后续说原因),value为从数据库中查出来的值,其中:

  • 一级缓存默认是开启的;
  • 二级缓存是要手动配置开启的。

但是,由于二级缓存的一些弊端,所以并不建议在实际生产中使用(有脏读问题),而是在外部实现自己的缓存,比如:使用更加成熟可靠的 Redis 。

关于这部分的博客介绍已经很多,站在前辈的肩膀上,加上自己深入浅出的理解,和大家分享一下经验:


一、缓存分类

为了验证 MyBatis 一级缓存的效果,我们创建一个实例表 - studunt,并且创建对应的POJO类和增改的方法,具体可以在entity包和mapper包中查看(此处略)。

注意,下面的每个单元测试后都请恢复被修改的数据。

CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `age` tinyint(3) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

1. 一级缓存

一级缓存的生命周期与SqlSession相同,如果你对SqlSession不熟悉,你可以把它类比为JDBC编程中的Connection,即数据库的一次会话。

1.1 源码分析:

我们直接从SqlSession接口入手,看看有没有创建缓存或者与缓存有关的属性或者方法,分析了一圈,你可能会得到这样一个方法调用流程:

在这里插入图片描述

扫描二维码关注公众号,回复: 13748669 查看本文章

要想了解缓存,就必须得了解一下Executor,这个Executor是干嘛的呢?你可以理解为要执行的SQL都会经过这个类的方法,在这个类的方法中调用StatementHandler最终执行SQL

Executor的实现是一个典型的装饰者模式,UML图如下:

在这里插入图片描述

 相信你已经看出来,SimpleExecutor,BatchExecutor是具体组件实现类,而CachingExecutor是具体的装饰器。可以看到具体组件实现类有一个父类 BaseExecutor,而这个父类是一个模板模式的典型应用,操作一级缓存的操作都在这个类中,而具体的操作数据库的功能则让子类去实现。

至此终于搞明白了,一级缓存的所有操作都在 BaseExecutor 这个类中啊,看看具体操作:

1.1.1 底层都是用HashMap

流程走到Perpetualcache中的clear()方法之后,会调用其cache.clear()方法,点进去发现,cache其实就是 private Map cache = new HashMap();也就是一个Map。这个Map存储了一级缓存数据,key为CacheKey对象(后续说原因),value为从数据库中查出来的值。

 1.1.2 query()

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

当执行select操作,会先生成一个CacheKey,如果根据CacheKey能从HashMap中拿到值则放回,如果拿不到值则先查询数据库,从数据库中查出来后再放到HashMap中。

1.1.3 update()

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        clearLocalCache();
        return doUpdate(ms, parameter);
    }

当执行update操作时,可以看到会调用clearLocalCache()方法,而这个方法则会清空一级缓存,即清空HashMap。

1.2 实验验证:

下面,我们就用几个小实验来验证下我们上面得出的猜想。实验中,id为1的学生名称是【凯伦】。

1.2.1 实验一:开启一级缓存,范围为会话级别,调用两次getStudentById()

    @Test
    public void test_level_1_cache() throws Exception {
        // 1.读取数据库配置文件,加载入内存
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        // 2.对加载入内存的配置内容进行构建、解析
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 3.根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 4.验证
         StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 4.1 从第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        System.out.println(studentMapper.getStudentById(1));
        // 4.2 第二次、三次查询,由于是同一个sqlSession,会在缓存中查询结果
        //     如果有,则直接从缓存中取出来,不和数据库进行交互
        System.out.println(studentMapper.getStudentById(1));
        sqlSession.close();
    }

执行结果:

在这里插入图片描述

 我们可以看到:只有第一次真正查询了数据库,后续的查询并没有与数据库有直接交互,而是使用了一级缓存。

1.2.2 实验二:同样是对 studunt 表进行两次查询,只不过两次查询之间增加了一次对数据库的修改操作

    @Test
    public void test_level_1_cache() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 4.验证
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 4.1 第一次查询
        System.out.println(studentMapper.getStudentById(1));
        // 4.2 新增一条数据
        System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
        // 4.3 第二次查询
        System.out.println(studentMapper.getStudentById(1));
        sqlSession.close();
    }

执行结果:

在这里插入图片描述

 我们可以看到:在修改操作后执行的相同查询,都查询了数据库,这时一级缓存失效。

1.2.3 实验三:开启两个SqlSession,验证一级缓存只在它的会话内部共享

    @Test
    public void testLocalCacheScope() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        // 4. 验证
	    System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentById(1));
	    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
	    System.out.println("studentMapper2更新数据" + studentMapper2.updateStudentName("小明",1));
	    System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentById(1));
	    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

2. 二级缓存

前面说了一级缓存的实现在 BaseExecutor 中,而二级缓存的实现在 CachingExecutor 中。

下面详细介绍一下:

在这里插入图片描述

 二级缓存的原理和一级缓存原理一样:第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。

但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。

2.1 开启二级缓存

步骤一:mybatis-config.xml

<settings>
	<setting name="cacheEnabled" value="true"/>
</settings>

这个是二级缓存的总开关,只有当该配置项设置为true时,后面两项的配置才会有效果。

从Configuration类的newExecutor方法可以看到,当cacheEnabled为true,就用缓存装饰器装饰一下具体组件实现类,从而让二级缓存生效。

步骤二:mapperr.xml 映射文件

mapper.xml 映射文件中如果配置了 <cache> 和 <cache-ref> 中的任意一个标签,则表示开启了二级缓存功能,没有的话表示不开启。

<cache type="" eviction="FIFO" size="512"></cache>

二级缓存的部分配置如上,PerpetualCache 这个类是 mybatis 默认实现缓存功能的类。type 就是填写一个全类名,我们不写就使用 mybatis 默认的缓存,也可以去实现Cache接口来自定义缓存。

二级缓存是用 Cache 表示,一级缓存是用HashMap表示。这就说明二级缓存的实现类你可以可以自己提供的,不一定得用默认的HashMap(二级缓存默认是用HashMap实现),Mybatis 能和 Redis,Memcache 整合的原因就在这。

属性说明,eviction表示缓存清空策略,可填选项如下:

 可以看到在Mybatis中换缓存清空策略就是换装饰器。还有就是如果面试官让你写一个FIFO算法或者LRU算法,这不就是现成的实现吗?

2.2 实验验证

2.2.1 实验一:当提交事务时,sqlSession1查询完数据后,sqlSession2相同的查询会从缓存中获取数据

    @Test
    public void testCacheWithCommitOrClose() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
        
        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper1读取数据: " + studentMappe1r.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

执行结果:

在这里插入图片描述

我们可以看到:sqlsession2的查询,使用了缓存,缓存的命中率是0.5。

2.2.2 实验二:update操作会刷新该namespace下的二级缓存

    @Test
    public void testCacheWithUpdate() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true); 
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true); 
        SqlSession sqlSession3 = sqlSessionFactory.openSession(true); 
        
        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
        
        System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
        
        studentMapper3.updateStudentName("方方",1);
        sqlSession3.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

执行结果:

在这里插入图片描述

我们可以看到:在sqlSession3更新数据库,并提交事务后,sqlsession2的StudentMapper namespace下的查询走了数据库,没有走Cache。 


总结

  • MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享;
  • MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻;
  • 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

1. 关于一级缓存

  • MyBatis 默认“开启”一级缓存;
  • 一级缓存底层其实是基于 HashMap的本地内存缓存;
  • 一级缓的作用域是 session(会话),session 关闭或者刷新的时候缓存清空;
  • 不同的 sqlsession 之间缓存相互独立,互不影响。

2. 关于二级缓存

  • MyBatis 默认“不开启”二级缓存,需要我们手动开启;
  • 二级缓存是 mapper 级别的缓存;
  • 同一个 namespace 下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

3. 建议

  • 生产中,建议使用 MyBatis 的默认配置,即:默认开启一级缓存,默认关闭二级缓存,让 MyBatis 只完成 ORM 框架内的操作(如:数据库的对象映射,CRUD等) ;
  • 将全局的缓存交给第三方插件来做,如:redis,mamcache等。

猜你喜欢

转载自blog.csdn.net/weixin_44259720/article/details/122058116
今日推荐