Mybatis缓存的使用和源码分析

Mybatis 缓存使用

Mybatis中缓存分为一级缓存和二级缓存,二级缓存又称为全局缓存,默认一级缓存和二级缓存都是开启的,只是二级缓存的使用需要配置才能生效,在Mybatis中一级缓存是SqlSession级别也就是会话级别的,而二级缓存是Mapper级别的可以跨SqlSession会话。

我们看看一级缓存的使用,查询用户信息:


private SqlSessionFactory sqlSessionFactory;

@Before
public void before() {
    //第一步获取配置文件,并将其读取到流中
    String resource = "mybatis-config.xml";
    InputStream in = null;
    try {
        in = Resources.getResourceAsStream(resource);
    } catch (Exception e) {
        e.printStackTrace();
    }
    //获取到sessionFactory
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
}
@Test
public void firstCache() {
    //第二步,读取数据
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
		// 第一次查询
        User user1 = mapper.selectById(1);
		// 第二次查询
        User user2 = mapper.selectById(1);
        System.out.println(user1 == user2);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

输出:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 19, 李四
<==      Total: 1
true
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
Returned connection 8641687 to pool.

可以看到,这里只进行了一次查询,并且结果值返回的true,说明在JVM内存中只创建了一个对象出来。

Mybatis中一级缓存也会失效,什么时候失效呢?那就是在进行更新操作的时候就会导致一级缓存失效,比如:

 @Test
    public void firstCache() {
        //第二步,读取数据
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {

            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
			/// 第一次查询
            User user1 = mapper.selectById(1);
            // 执行更新操作,破环一级缓存
            mapper.updateUser(new User().setId(1).setName("李四").setAge(19));
            sqlSession.commit();
            // 第二次查询
            User user2 = mapper.selectById(1);
            System.out.println(user1 == user2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

输出:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 19, 李四
<==      Total: 1
==>  Preparing: update user set name = ?, age = ? where id = ? 
==> Parameters: 李四(String), 19(Integer), 1(Integer)
<==    Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 19, 李四
<==      Total: 1
false
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]
Returned connection 8641687 to pool.

Process finished with exit code 0

可以看到这里的执行了两次的查询,中间执行了一次更新的sql语句输出,并且这个对象不相等,返回结果为false,说明在JVM内存中创建了两个对象。

接下来看看二级缓存的使用,二级缓存开启需要在mybatis-config.xml中开启,在settings标签中开启:

<settings>
    <!-- 打印sql日志 -->
    <setting name="logImpl" value="STDOUT_LOGGING" />
    <!--开启二级缓存-->
    <setting name="cacheEnabled" value="true"/>
</settings>

开启之后还需要在xxxMapper.xml中配置标签:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.mapper.UserMapper">
    <!--配置二级缓存-->
    <cache />
    <select id="selectUserByName" resultType="mybatis.model.User">
        select *
        from user
        <if test="name != null">
            where name = #{name}
        </if>
    </select>
    <select id="selectById" resultType="mybatis.model.User">
        select *
        from user
        where id = #{id}
    </select>
    <update id="updateUser">
        update user
        set name = #{name},
            age  = #{age}
        where id = #{id}
    </update>
    <select id="selectAll" resultType="mybatis.model.User">
    select * from user
    </select>
</mapper>

测试二级缓存:

 /**
     * 二级缓存测试
     */
    @Test
    public void secondCache() {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();

        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);

        // 测试二级缓存开始
        User user1 = mapper1.selectById(1);
        sqlSession1.close();
        System.out.println(user1);
		
        // 第二次查询
        User user2 = mapper2.selectById(1);
        System.out.println(user2);

        System.out.println(user1 == user2);


    }

输出结果如下:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a12036]
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a12036]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a12036]
Returned connection 10559542 to pool.
User(id=1, age=18, name=灵犀)
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.5
User(id=1, age=18, name=灵犀)
false
Disconnected from the target VM, address: '127.0.0.1:4672', transport: 'socket'

Process finished with exit code 0

第二次查询没有执行sql语句,并且日志打印了缓存命中率为0.5,并且这两个对象不相等,这是为啥呢?

原因是二级缓存的使用必须要求缓存对象实现序列化接口,因为二级缓存的实现是通过将数据序列化在保存的,当第二次查询的时候,如果缓存中有那就将数据再反序列化出来,由于反序列化时每次都是重新创建的对象,因此即使是缓存命中也不相等,缓存命中为0.5的原因是第一次没有命中,第二次命中了,请求了2次因此 1/2 得到的就是0.5。

所以在使用二级缓存的时候,使用的对象必须要实现序列化接口,否则就会报错:


org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: mybatis.model.User

	at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:94)
	at org.apache.ibatis.cache.decorators.SerializedCache.putObject(SerializedCache.java:55)
	at org.apache.ibatis.cache.decorators.LoggingCache.putObject(LoggingCache.java:49)
	at org.apache.ibatis.cache.decorators.SynchronizedCache.putObject(SynchronizedCache.java:43)
	at org.apache.ibatis.cache.decorators.TransactionalCache.flushPendingEntries(TransactionalCache.java:116)
	at org.apache.ibatis.cache.decorators.TransactionalCache.commit(TransactionalCache.java:99)
	at org.apache.ibatis.cache.TransactionalCacheManager.commit(TransactionalCacheManager.java:44)
	at org.apache.ibatis.executor.CachingExecutor.close(CachingExecutor.java:61)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
	at com.sun.proxy.$Proxy15.close(Unknown Source)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.close(DefaultSqlSession.java:263)
	at mybatis.MybatisApplication.secondCache(MybatisApplication.java:73)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.io.NotSerializableException: mybatis.model.User
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at java.util.ArrayList.writeObject(ArrayList.java:768)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1155)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:90)
	... 43 more

Mybatis中的缓存介绍

Mybatis中内置了很多的缓存,比如永久缓存、LRU缓存、FIFO缓存等,这些缓存都是用来为一级缓存和二级缓存使用的Mybatis中的缓存在实现时使用了装饰器模式进行实现,列举下Mybatis中的缓存常见的种类:

  • BlockingCache 阻塞缓存,当获取不到数据时会将缓存key锁定,其他线程会一直等待直到缓存被写入
  • LruCache 最近最少使用缓存,当缓存数量达到一定数量时,缓存就会进行淘汰,将最近最少是使用的缓存给淘汰掉
  • FifoCache 先进先出缓存,缓存谁先进来,谁就先获取到
  • LoggingCache 用于打印日志的缓存,记录缓存的命中率
  • TranslationalCache 事务缓存,专用于二级缓存的,当事务提交时缓存就会保存,当事务回滚时就会将缓存个清除掉,所以二级缓存的生效前提是必须事务的提交
  • SerializedCache 序列化缓存,二级缓存使用就会装饰这个缓存,用于对象的序列化,通过ObjectOutputStreamObjectInputStream进行序列化和反序列化使用
  • PerpetualCache 永久缓存,也就是缓存的最终实现,其他的缓存都会通过委派,最终交给永久缓存进行数据的保存,缓存会保存在Map中

当然还有WeakCache弱引用缓存,SynchronizedCache同步缓存,SoftCache软引用缓存,ScheduledCache调度缓存

默认情况下一级缓存和二级缓存都是使用的默认实现永久缓存进行保存数据的,这些数据都是保存在Map中,但是二级缓存可以指定缓存的类型,比如我们可以将数据缓存到redis中。

二级缓存使用Redis保存数据

Mybatis中已经有这个实现,只需引入依赖即可使用:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

然后在 标签上配置:

<cache type="org.mybatis.caches.redis.RedisCache"/>

然后还需要配置redis.properties文件,里面指定redis的配置,注意这个文件名字不能修改

host=192.168.64.129
port=6379
password=
database=0

执行方法得到输出:

==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Returned connection 10711284 to pool.
User(id=1, age=18, name=灵犀)
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.5
User(id=1, age=18, name=灵犀)
false

使用redis客户端连接工具查看是否缓存数据:

发现key为:

677706599:5423390838:mybatis.mapper.UserMapper.selectById:0:2147483647:select *
        from user
        where id = ?:1:development

value为:

\xAC\xED\x00\x05sr\x00\x13java.util.ArrayListx\x81\xD2\x1D\x99\xC7a\x9D\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x01w\x04\x00\x00\x00\x01sr\x00\x12mybatis.model.User\xF4\xBB\x11O\xFF\xA4\xDC<\x02\x00\x03I\x00\x03ageI\x00\x02idL\x00\x04namet\x00\x12Ljava/lang/String;xp\x00\x00\x00\x12\x00\x00\x00\x01t\x00\x06\xE7\x81\xB5\xE7\x8A\x80x

说明缓存是生效了明确保存到了redis中,再次查询:

Cache Hit Ratio [mybatis.mapper.UserMapper]: 1.0
User(id=1, age=18, name=灵犀)
Cache Hit Ratio [mybatis.mapper.UserMapper]: 1.0
User(id=1, age=18, name=灵犀)
false

直接走缓存了,二级缓存失效的原理跟一级缓存一样,只有有更新操作并且提交了事务,那么都会将缓存给清空导致缓存失效。

比如:

@Test
public void secondCache() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);

    // 测试二级缓存开始
    User user1 = mapper1.selectById(1);
    sqlSession1.close();
    System.out.println(user1);

    // 调用更新操作,破环二级缓存
    mapper3.updateUser(new User().setId(1).setName("灵犀").setAge(18));
    sqlSession3.commit();

    // 第二次查询
    User user2 = mapper2.selectById(1);
    System.out.println(user2);

    System.out.println(user1 == user2);


}

输出:

==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Returned connection 10711284 to pool.
User(id=1, age=18, name=灵犀)
Opening JDBC Connection
Checked out connection 10711284 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
==>  Preparing: update user set name = ?, age = ? where id = ? 
==> Parameters: 灵犀(String), 18(Integer), 1(Integer)
<==    Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 28524404.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1b33f74]
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
User(id=1, age=18, name=灵犀)
false

Process finished with exit code 0

缓存源码解析

由于一级缓存是不需要配置的,默认就是使用的永久缓存,而二级缓存需要配置并且可以指定使用不同的缓存实现,所以先看二级缓存配置加载过程,二级缓存的配置加载实际上就是解析<cache>标签或者是解析@CacheNamespace注解,从源码入口找到解析mapper.xml的地方,会找到Configuration#addMapper方法,最终会在MapperRegistry类中开始解析:

 public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 解析dao接口上标注的注解比如@CacheNamespace、@Select、@Insert、@Update、@Delete
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

点进去会进入到MapperAnnotaionBuilder类中,这里会进行xml的解析和注解的解析

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 加载xml资源并解析,这里xml配置文件是放在dao接口包下的,并且名称也是跟dao接口名一致,否则无法解析,一般来说不会这样做
      loadXmlResource();
      configuration.addLoadedResource(resource);
      // 设置当前的namespace为类名称
      assistant.setCurrentNamespace(type.getName());
      // 解析缓存
      parseCache();
      parseCacheRef();
      // 遍历所有的方法,并解析注解
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          // 解析ResultMap注解
          parseResultMap(method);
        }
        try {
          // 解析方法
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

我们使用的xml配置,那么直接看解析xml的地方,点进去:

private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // #1347
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
      if (inputStream == null) {
        // Search XML mapper that is not in the module but in the classpath.
        try {
          inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
        } catch (IOException e2) {
          // ignore, resource is not required
        }
      }
      if (inputStream != null) {
        // 创建一个xmlMapper的解析器
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }

再点进去,进入到XmlMapperBuilder类中:

 public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析mapper配置
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    // 解析ResultMap
    parsePendingResultMaps();
    parsePendingCacheRefs();
    // 解析sql
    parsePendingStatements();
  }

由于<cache> 标签是配置再mapper标签下,所以点击configurationElement方法:

  private void configurationElement(XNode context) {
    try {
      // 获取namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 设置当前的namespace
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析缓存配置标签
      cacheElement(context.evalNode("cache"));
      // 解析参数map
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析resultMap
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析sql标签
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析curd标签
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

我们只关心<cache>标签,点进去:

 private void cacheElement(XNode context) {
    if (context != null) {
      // 获取 缓存type类型,默认是 PerpetualCache 永久缓存
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 获取过期的缓存淘汰策略,默认是LRUCache 最近最少缓存
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      // 缓存刷新间隔
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      // 默认可读可写
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      // 默认非阻塞
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      // 创建缓存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

可以看到默认使用的是永久缓存,如果没有配置type指定缓存的话,点击useNewCache方法:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    // 建造者模式创建缓存对象
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;

这里使用了建造者模式创建缓存,点击build方法进入到CacheBuilder中:

public Cache build() {
    setDefaultImplementations();
    // 创建缓存对象,默认创建的是PerpetualCache 永久缓存
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        // 反射创建出缓存对象
        cache = newCacheDecoratorInstance(decorator, cache);
        // 设置默认的属性
        setCacheProperties(cache);
      }
      // 设置标准的缓存装饰器
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }

这里通过反射创建出缓存的类型,最终设置了一个标准的缓存装饰,调用了setStandardDecorators方法:

private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      // 如果周期清除不为空,那么将缓存装饰为调度缓存对象
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        // 读写,默认是支持读写的,创建一个序列化的缓存,所以在开启二级缓存的时候需要将查询的对象实体实现序列化接口,否则会报错
        cache = new SerializedCache(cache);
      }
      // 日志缓存,在查询是会打印命中的缓存概率,可以在config.xml中配置日志,开启日志打印
      cache = new LoggingCache(cache);
      // 同步缓存
      cache = new SynchronizedCache(cache);
      if (blocking) {
        // 阻塞缓存,默认是没有开启,需要手动开启,当缓存没有命中,那么就会导致其他线程一直等待,直到缓存被填充进去
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

可以看到默认情况下readWrite这个属性是true,这里会添加一个序列化缓存进行装饰,然后日志缓存也会进行装饰,同步缓存也会进行装饰,所以在打印日志时看到如果对象没有进行序列化那么就会报错,并且金可以看到缓存命中率的日志会打印,并且获取缓存都是加了synchronized同步关键字的,不会存在线程安全问题。

这是缓存的准备阶段,接下来看缓存在执行过程中是怎么处理的?

以查询为例,还是以selectList为例,在DefaultSqlSession中:

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      // 获取Mapper中解析的配置,这个类中存放了sql语句,返回类型,参数类型等
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 调用查询,这里默认是CachingExecutor
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

这里的执行器默认是CachingExecutor,因为二级缓存默认是开启的,所以在解析构建Configuration时创建的执行器就是CachingExecutor,点进去:

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

这里创建缓存key会委派到BaseExecutor类中去创建:

@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 缓存key
    CacheKey cacheKey = new CacheKey();
    // crud的标签的id,statementId
    cacheKey.update(ms.getId());
    // 分页参数
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    // sql语句
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    // 参数值
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      // 环境信息
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

这就是创建的缓存key,可以看到由多个值组成,所以在redis看到key就是这样的:

677706599:5423390838:mybatis.mapper.UserMapper.selectById:0:2147483647:select *
        from user
        where id = ?:1:development

完全是可以对应上的,进去query方法:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      // 如果有必要就清除(刷新)缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // 从二级缓存中获取数据,这里使用的事务缓存
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

这里通过tcm去获取缓存,这个tcm就是事务缓存管理器TranslationalCacheManager,里面正是使用的TranslationalCache实现的,通过getObject方法调用就会经过装饰中的所有缓存组件,最终会调用到永久缓存或者是自定义缓存比如RedisCache,如果没有获取到,那么就会委派给BaseExecutor#query去查询,并且将数据缓存起来:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 从一级缓存中获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 查询数据从数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

这里会先从localCache也就是一级缓存中获取,一级缓存实际上就是永久缓存,如果没有查询到就会去调用queryFromDatabase去执行数据库查询:

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 执行真正的查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // 将查询的数据放入到缓存中
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

查询完之后将数据缓存到缓存中,但是这里有个问题就是二级缓存虽然是进行了putObject方法调用,但是数据还在TranslationalCache中,数据还没有真正的存储起来,进去TranslationalCache中可以看到只有调用了commit方法才会进行缓存的刷新并存储:

public void commit() {
    if (clearOnCommit) {
        // 清除缓存
        delegate.clear();
    }
    // 缓存数据,委派调用
    flushPendingEntries();
    // 清空本地缓存数据
    reset();
}

private void flushPendingEntries() {
    // 委派给上一级进行缓存存储
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

那么commit操作是什么时候调用的呢?点击源码可以轻松的找到有两个地方进行了调用,一个是CachingExecutor#close,一个是CachingExecutor#commit,这两个方法都会去调用TranslationalCacheManager#commit,最终调用到TranslationalCache#commit,也就是说我们要想二级缓存生效还得必须调用SqlSession#close或者SqlSession#commit方法才会生效。

比如我现在把二级缓存的测试把close方法给注释掉,并且把redis的缓存给清掉,然后执行查询:

@Test
    public void secondCache() {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();

        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);

        // 测试二级缓存开始
        User user1 = mapper1.selectById(1);
        //sqlSession1.close();
        System.out.println(user1);

        // 调用更新操作,破环二级缓存
//        mapper3.updateUser(new User().setId(1).setName("灵犀").setAge(18));
//        sqlSession3.commit();

        // 第二次查询
        User user2 = mapper2.selectById(1);
        System.out.println(user2);

        System.out.println(user1 == user2);


    }

输出如下:

==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
User(id=1, age=18, name=灵犀)
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 31765586.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1e4b452]
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
User(id=1, age=18, name=灵犀)
false

Process finished with exit code 0

如果打开close的注释,输出如下:

==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]
Returned connection 10711284 to pool.
User(id=1, age=18, name=灵犀)
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.5
User(id=1, age=18, name=灵犀)
false

Process finished with exit code 0

Mybatis缓存的清除比较简单,在执行更新操作的时候提交事务就会执行到clearCache方法,然后将缓存给清除掉:

这就是Mybatis缓存的原理和使用,相信看完一定会有一定的收获

posted @ 2022-08-20 16:54  玲丶蹊  阅读(237)  评论(0编辑  收藏  举报