cnfox

解读Mybatis缓存机制
缓存及缓存机制 传统的关系型数据库,十分强调数据的一致性,并为此降低读写性能付出了巨大的代价,虽然关系...
扫描右侧二维码阅读全文
31
2019/08

解读Mybatis缓存机制

缓存及缓存机制

 传统的关系型数据库,十分强调数据的一致性,并为此降低读写性能付出了巨大的代价,虽然关系型数据库存储数据和处理数据的可靠性很不错,但一旦面对海量数据的处理的时候效率就会变得很差,特别是遇到高并发读写的时候性能就会下降的非常厉害。比如这样一个场景:使用sql语句(select * from user where id =1;)查询了数据库中id为1的数据,此时数据库会进行查询操作,并返回一个User类型的数据,没有进行增删改的操作,然后再次使用sql语句查询数据库,程序会重复上面的步骤.在这种频繁的查询操作下,对数据库来说是一个巨大的挑战.
 所以我们引入了缓存(cache),将经常不被修改并且频繁查询的数据放入内存中,减少对数据库的频繁读写操作,降低数据库的压力.当要读取数据时,会首先从内存中查找需要的数据,如果找到了则直接执行,找不到的话则从数据库中找。由于内存的运行速度比从数据库中查询快得多,故缓存的作用就是帮助查询更快地运行。

Mysql缓存机制就是缓存sql 文本及缓存结果,用KV形式(是一种以键值对存储数据的一种形式,可以理解为一个大的map,每个键都会对应一个唯一的值。)保存再服务器内存中,如果运行相同的sql,服务器直接从缓存中去获取结果,不需要在再去解析、优化、执行sql。 如果这个表修改了,那么使用这个表中的所有缓存将不再有效,查询缓存值得相关条目将被清空。
 对于频繁更新的表,查询缓存不合适,对于一些不变的数据且有大量相同sql查询的表,查询缓存会节省很大的性能。

Mybatis缓存机制

 和大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持;
 一级缓存是Mybatis默认开启的,是基于 PerpetualCache 的 HashMap 本地缓存,存储作用域为 Session,当 Session flush (会话刷新)或 close 之后,该Session中的所有 Cache 就将清空。
 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache、Hazelcast等。

一级缓存

 在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

一级缓存

 每个SqlSession中持有了Executor(执行器),每个Executor中有一个LocalCache(本地缓存)。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement(在MyBatis启动时,会解析这些包含SQL的XML文件,并将其包装成为MapperStatement对象,并将MapperStatement注册到全局的configuration对象上),在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示。

一级缓存 具体实现类的类关系图

代码流程

  1. 第一次执行select完毕会将查到的数据写入SqlSession内的HashMap中缓存起来
  2. 第二次执行select会从缓存中查数据,如果select相同切传参数一样,那么就能从缓存中返回数据,不用去数据库了,从而提高了效率

注意事项:

  • 如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前SqlSession缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读
  • 当一个SqlSession结束后那么他里面的一级缓存也就不存在了,mybatis默认是开启一级缓存,不需要配置

一级缓存的开启与关闭

  • 一级缓存默认开启
  • 关闭一级缓存需要在mybatis配置文件的settings标签设置
<setting name="localCacheScope" value="SESSION"/>

导致一级缓存不命中的原因

  • 一级缓存关闭;
  • 一级缓存开启,但是使用了不同的SqlSession进行查询;
  • 使用相同的SqlSession,但是查询条件发生了变化;
  • 使用了相同的查询条件,但是两次查询之间SqlSession执行了commit、clearCache或close(关闭之后查询会报错)操作;
  • 使用了相同的查询条件,但是两次查询之间SqlSession执行了insert、update或delete操作,此时无论三种标签的
    flushCache 属性是否为 false,都会清空 sqlSession的缓存;
  • select标签的flushCache属性为true

清空一级缓存的操作

  • SqlSession调用commit方法;
  • SqlSession调用clearCache方法;
  • select标签的flushCache属性为true;

SqlSession执行了insert、update或delete操作,此时无论三种标签的flushCache属性是否为 false;

一级缓存实验

实验1
 开启一级缓存,范围为会话级别,调用三次Getbyid
代码如下所示:

public static void main(String[] args) throws IOException {
    }
    SqlSession ss;
    Userdao udao;
    //定义一个解析工具
    @Before
    public void  setup() throws IOException {
        
        String resource = "MybatisConfig.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        ss = sqlSessionFactory.openSession();
        udao=ss.getMapper(Userdao.class);

    }
    @Test
    public void Getbyid() {
        System.out.println(udao.Getbyid1(1));
        System.out.println(udao.Getbyid1(1));
        System.out.println(udao.Getbyid1(1));
        System.out.println(udao.Getbyid1(1));
    }
    @After
    public void finish() {
        ss.close();
    }

执行结果:

实验1执行结果

实验2
 增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。
代码如下

public static void main(String[] args) throws IOException {
    }
    SqlSession ss;
    Userdao udao;
    //定义一个解析工具
    @Before
    public void  setup() throws IOException {
        
        String resource = "MybatisConfig.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        ss = sqlSessionFactory.openSession();
        udao=ss.getMapper(Userdao.class);

    }
    @Test
    public void Getbyid() {
        System.out.println(udao.Getbyid1(1));
        System.out.println(udao.Getbyid1(1));
        User user = new User();
        user.setId(1);
        user.setSex("男");
        System.out.println("更新数据"+udao.UpdateUser(user));
        System.out.println(udao.Getbyid1(1));
        System.out.println(udao.Getbyid1(1));
    }
    @After
    public void finish() {
        ss.close();
    }

执行结果:

实验2执行结果

实验3
 开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。
执行代码:

public static void main(String[] args) throws IOException {
    }
    SqlSession ss;
    Userdao udao;
    SqlSession ss2;
    Userdao udao2;
    //定义一个解析工具
    @Before
    public void  setup() throws IOException {
        
        String resource = "MybatisConfig.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        ss = sqlSessionFactory.openSession();
        udao=ss.getMapper(Userdao.class);
        ss2 = sqlSessionFactory.openSession();
        udao2=ss2.getMapper(Userdao.class);

    }
    
    @Test
    public void Getbyid() {
        System.out.println(udao.Getbyid(1));
        System.out.println(udao.Getbyid(1));
        
        System.out.println(udao2.Getbyid(1));
        System.out.println(udao2.Getbyid(1));
    }
    @After
    public void finish() {
        ss.close();
    }

执行结果:

实验3执行结果

一级缓存小结

  • MyBatis一级缓存的生命周期和SqlSession一致。
  • MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺
  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

二级缓存

 在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。二级缓存是mapper(接口)级别的缓存,也就是同一个namespace的mappe.xml(映射文件),当多个SqlSession使用同一个Mapper操作数据库的时候,得到的数据会缓存在同一个二级缓存区域.开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。

二级缓存工作流程

 二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
 当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

二级缓存的配置

 要正确的使用二级缓存,需完成如下配置的。在MyBatis的全局配置文件中开启二级缓存。

<settings>
        <setting name="cacheEnabled" value="true"/>默认是false:关闭二级缓存
<settings>

在MyBatis的映射配置文件XML中配置cache或者 cache-ref 。
cache标签用于声明这个namespace使用二级缓存,并且书写属性可以自定义配置。

  • type:cache使用的类型,默认是PerpetualCache(Mybatis自带的二级缓存),也可以使用第三方的插件
  • eviction: 定义回收的策略,常见的有FIFO,LRU。
  • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
  • size: 最多缓存对象的个数。
  • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>当前mapper下所有语句开启二级缓存

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。

<cache-ref namespace="mapper.StudentMapper"/>

po 类(实体类)实现 Serializable 序列化接口

public class User implements Serializable{....}

二级缓存实验

实验1
测试二级缓存效果,不提交事务,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。

执行代码:

@Test
public void testCacheWithoutCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
 
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
 
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

执行结果:

二级缓存实验1
实验2
测试二级缓存效果,当提交事务时,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。

执行代码:

@Test
public void testCacheWithCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
 
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
 
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

执行结果:

二级缓存实验2
实验3
测试update操作是否会刷新该namespace下的二级缓存。

执行代码:

@Test
public void testCacheWithUpdate() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        SqlSession sqlSession3 = factory.openSession(true); 
 
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
 
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
 
        studentMapper3.updateStudentName("方方",1);
        sqlSession3.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

执行结果:

二级缓存实验3

Photo by Anthony Da Cruz on Unsplash

Last modification:September 4th, 2019 at 07:23 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment