反正闲来无事,不如我们来聊聊 MyBatis 缓存

372 阅读7分钟

刘亦菲-27.jpeg

前言

  MyBatis是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis的默认缓存配置,但是MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。个人在业务开发中也处理过一些由于MyBatis缓存引发的开发问题,带着个人的兴趣,希望从应用及源码的角度为读者梳理MyBatis缓存机制。

  Mybatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存指的是 MyBatis 中 sqlSession 对象的缓存,当我们执行查询以后,查询的结果会同时存入 sqlSession 中,再次查询的时候,先去 sqlSession 中查询,有的话直接拿出。当 sqlSession 消失时,MyBatis 的一级缓存也就消失了,当调用 sqlSession 的修改、添加、删除、commit()、close()等方法时,会清空一级缓存。而二级缓存指的是 MyBatis 中的 sqlSessionFactory 对象的缓存,由同一个 sqlSessionFactory 对象创建的 sqlSession 共享其缓存,但是其中缓存的是数据而不是对象。

一、MyBatis 缓存中的常用概念

常用概念说明
MyBatis 缓存它用来优化 SQL 数据库查询的,但是可能会产生脏数据。
SqlSession代表和数据库的一次会话,向用户提供了操作数据库的方法。
MappedStatement代表要发往数据库执行的指令,可以理解为是 SQL 的抽象表示。
Executor代表用来和数据库交互的执行器,接受 MappedStatment 作为参数。
namespace每个 Mapper 文件只能配置一个 namespace,用来做 Mapper 文件级别的缓存共享。
映射接口定义了一个接口,然后里面的接口方法对应要执行 SQL 的操作,具体要执行的 SQL 语句是写在映射文件中。
映射文件MyBatis 编写的 XML 文件,里面有一个或多个 SQL 语句,不同的语句用来映射不同的接口方法。
通常来说,每一张单表都对应着一个映射文件。

二、MyBatis 一级缓存

2.1 一级缓存原理

  在应用运行过程中,有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL 语句,如果不采取一些措施的话,每一次查询都会查询一次数据库。而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同。由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。为了解决这一问题,减少资源的浪费,MyBatis 提供了一级缓存的方案优化这部分场景。在某次表示会话的 SqlSession 中建立一个简单的缓存,程序执行多次查询,且查询条件完全相同,多次查询之间程序没有其他增删改操作,则第二次及后面的查询可以从缓存中获取数据,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

image

  每个 SqlSession 中持有了 Executor,每个 Executor 中有一个本地缓存(Local Cache)。当用户发起查询时,,都会尝试根据查询的条件在 Local Cache 进行查询,如果缓存命中的话,直接从缓存中取出并返回结果给用户;如果缓存没有命中的话,从数据库读取数据,将查询结果写入 Local Cache,最后返回结果给用户。Local Cache 其实是一个 HashMap 的结构:

private Map<Object, Object> cache = new HashMap<Object, Object>();

  如下图所示,有两个 SqlSession,分别为 SqlSession1 和 SqlSession2,每个 SqlSession 中都有自己的缓存,缓存是 hashmap 结构,存放的键值对。值是 SQL 查询的结果:

image

2.2 一级缓存配置

  我们来看看如何使用MyBatis一级缓存,只需要在 mybatis-config.xml 配置文件中,添加如下语句,就可以使用一级缓存。

<configuration>
    <settings>
        <setting name="localCacheScope" value="SESSION"/>
    </settings>
<configuration>

name=localCacheScope,value 有两种值:SESSION 和 STATEMENT,默认是SESSION级别。其中,SESSION 表示开启一级缓存功能,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。而 STATEMENT 表示缓存只对当前执行的这一个 SQL 语句有效,也就是没有用到一级缓存功能。

2.3 一级缓存工作流程

  那么,一级缓存的工作流程是怎样的呢?我们从源码层面来看下一级缓存执行的时序图,如下图所示。

image

2.4 一级缓存失效的场景

  • 不同的 SqlSession 对应不同的一级缓存。
  • 同一个 SqlSession 但是查询条件不同。
  • 同一个 SqlSession 两次查询期间执行了任何一次增删改操作。
  • 同一个 SqlSession 两次查询期间手动清空了缓存。

2.5 小结

  MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺。一级缓存的配置中,默认是 SESSION 级别,即在一个 MyBatis 会话中执行的所有语句,都会共享这一个缓存。而且MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement。

三、MyBatis 二级缓存

3.1 二级缓存概述

  MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度更加的细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。

  在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。

3.2 二级缓存原理

  在一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询。二级缓存开启后,同一个 namespace下的所有操作语句,都影响着同一个 Cache。

image

  每个 Mapper 文件只能配置一个 namespace,用来做 Mapper 文件级别的缓存共享。

<mapper namespace="mapper.StudentMapper"></mapper>

  二级缓存被同一个 namespace 下的多个 SqlSession 共享,是一个全局的变量。MyBatis 的二级缓存不适应用于映射文件中存在多表查询的情况,通常我们会为每个单表创建单独的映射文件,由于 MyBatis 的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。

3.3 缓存查询的顺序

image

  • 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。
  • 如果二级缓存没有命中,再查询一级缓存。
  • 如果一级缓存也没有命中,则查询数据库。
  • SqlSession 关闭之后,一级缓存中的数据会写入二级缓存。

3.4 二级缓存配置

  开启二级缓存需要在 mybatis-config.xml 中配置:

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

四、MyBatis 自定义缓存

  当 MyBatis 二级缓存不能满足要求时,可以使用自定义缓存替换。在实际开发中,笔者较少使用,了解一下就好,不建议深入。

4.1 自定义缓存概述

  自定义缓存需要实现 MyBatis 规定的接口:org.apache.ibatis.cache.Cache。这个接口里面定义了如下几个方法,我们需要自己去实现对应的缓存逻辑。

public interface Cache {
    String getId();

    void putObject(Object var1, Object var2);

    Object getObject(Object var1);

    Object removeObject(Object var1);

    void clear();

    int getSize();

    ReadWriteLock getReadWriteLock();
}

五、总结

  本篇分别介绍了 MyBatis 一级缓存、二级缓存、自定义缓存的原理和使用。

把今天最好的表现当作明天最新的起点…...~

image