Mybatis 缓存实现原理——案例实践

Mybatis 缓存介绍

关于缓存,想必大家早已不陌生。第一次使用查询数据时,Mybatis 将其结果缓存起来,当下次执行相同的查询的时候直接返回(没有声明需要刷新缓存,且缓存没有超时)。

默认情况下,Mybatis 只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。
一级缓存(本地的会话缓存):只与 SqlSession 有关,不同的 SqlSession 缓存不同。
二级缓存:SqlSession 可以共享缓存。

缓存(总开关)是默认开启的,如果需要关闭缓存只需在 MyBatis 的配置文件中添加一个属性设置,
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。

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

开启二级缓存的方式

要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

<cache/> 

还可自定义缓存参数

  <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>

创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有(默认的清除策略是 LRU):

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

flushInterval(刷新间隔):属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目):属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读):属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

另一种开启方式,在 Mapper 接口上添加 @CacheNamespace 注解。

@CacheNamespace
public interface AutoConstructorMapper {
}

注解配置和 xml 配置只能选择一个,否者报错如下:

Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Caches collection already contains value for org.apache.ibatis

另一种开启方式,还可和其他命名空间共享相同的缓存配置和实例,在 Mapper 接口上添加 @CacheNamespaceRef注解。

@CacheNamespaceRef(name = "org.apache.ibatis.submitted.cache.PersonMapper") // by name
// 或者 @CacheNamespaceRef(PersonMapper.class) // by type
public interface AutoConstructorMapper {
}

一级缓存的效果(同一个 SqlSession )

默认一级缓存,具体是什么效果呢?接下来看看下面的例子。

  @Test
  void localCacheTest() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject1 = mapper.getSubject(1);
      PrimitiveSubject subject2 = mapper.getSubject(1);
      PrimitiveSubject subject3 = mapper.getSubject(1);
      assertTrue(subject1 == subject2);
      assertTrue(subject1 == subject3);
    }
  }

单元测试成功,(subject1 == subject2,且 subject1 == subject3)说明查询返回是的是相同的对象。
日志也只打印了一个查询语句,也说明 SQL 只执行了一次。

2020-05-10 18:07:35 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 18:07:38 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:38 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:38 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 18:07:38 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:38 DEBUG [main] - <==      Total: 1
2020-05-10 18:07:38 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:38 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]

一级缓存的效果(不同的 SqlSession )

  @Test
  void localCacheDifferentSqlSession() {
    PrimitiveSubject subject1;
    PrimitiveSubject subject2;
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject1 = mapper.getSubject(1);
    }
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject2 = mapper.getSubject(1);
    }
    assertTrue(subject1 != subject2);
  }

单元测试成功,(subject1 != subject2)说明查询返回是的是不同的对象。
日志打印了两次查询语句,说明 SQL 执行了两次,一级缓存对不同的 SqlSession 无效。

2020-05-10 18:07:54 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 18:07:56 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:56 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 18:07:56 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:56 DEBUG [main] - <==      Total: 1
2020-05-10 18:07:56 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:56 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
2020-05-10 18:07:56 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 18:07:56 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:56 DEBUG [main] - <==      Total: 1
2020-05-10 18:07:56 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
2020-05-10 18:07:56 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]

二级缓存的效果(自定义缓存 )

接下来,我们测试一下开启二级缓存,配置如下:

<mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
  <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>
  <select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
    SELECT * FROM subject WHERE id = #{id}
  </select>
</mapper>

执行会出现什么结果呢

  @Test
  void twoLevelCacheDifferentSqlSession() {
    PrimitiveSubject subject1;
    PrimitiveSubject subject2;
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject1 = mapper.getSubject(1);
    }
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject2 = mapper.getSubject(1);
    }
    log.debug(subject1.toString());
    log.debug(subject2.toString());
    subject1.setAge(27);
    log.debug(subject1.toString());
    log.debug(subject2.toString());
    assertTrue(subject1 == subject2);
  }
2020-05-10 20:48:36 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 20:48:38 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.0
2020-05-10 20:48:38 DEBUG [main] - Opening JDBC Connection
2020-05-10 20:48:38 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 20:48:38 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 20:48:38 DEBUG [main] - <==      Total: 1
2020-05-10 20:48:38 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=27, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=27, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}

SQL 只执行了一次,且多了一行输出 Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5 ,说明二次缓存命中。

这里需要注意的是 Cache 的配置 ***readOnly=“true”***,如果修改缓存,是会影响其他调用者的。
可以看 subject1.setAge(27) 前后的输出结果。

直接使用默认配置会如何呢?

获取缓存报 NotSerializableException 错?

若使用默认配置 ***<cache/>***,可能会报如下错误:

org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: org.apache.ibatis.autoconstructor.PrimitiveSubject

此时需要给 PrimitiveSubject 类实现 Serializable 接口,因为默认 ***readOnly=“false”***,是通过 SerializedCache 类来实现的,序列化和反序列化需要实现 Serializable 接口,序列化可保证业务更改获取到的值不影响实际的缓存。具体实现代码在:

##org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators

      if (readWrite) {
        cache = new SerializedCache(cache);
      }

public class SerializedCache implements Cache {

  public void putObject(Object key, Object object) {
    if (object == null || object instanceof Serializable) {
      delegate.putObject(key, serialize((Serializable) object));
    } else {
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    }
  }
  
  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    return object == null ? null : deserialize((byte[]) object);
  }
}

二级缓存的效果(默认缓存配置 )

<mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
<!--  <cache-->
<!--    eviction="FIFO"-->
<!--    flushInterval="60000"-->
<!--    size="512"-->
<!--    readOnly="true"/>-->
  <cache/>
  <select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
    SELECT * FROM subject WHERE id = #{id}
  </select>
  @Test
  void twoLevelCacheDifferentSqlSessionWithDefaultCache() {
    ...
    log.debug(subject1.toString());
    log.debug(subject2.toString());
    assertTrue(subject1 != subject2);
  }

单元测试成功,且 subject1 != subject2(不相等,是因为反序列化后返回的结果),但是两者的内容是一样的,SQL 只执行一次,缓存命中。

2020-05-10 20:49:38 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 20:49:40 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.0
2020-05-10 20:49:40 DEBUG [main] - Opening JDBC Connection
2020-05-10 20:49:40 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 20:49:40 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 20:49:40 DEBUG [main] - <==      Total: 1
2020-05-10 20:49:40 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5
2020-05-10 20:49:40 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:49:40 CST 2020}
2020-05-10 20:49:40 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:49:40 CST 2020}

二级缓存支持事务

开启一个不自动提交的事务1,事务1先查询数据总数为3条,删除一条记录之后执行查询总数变为2条,回滚之后再次查询总数为3条。
开启一个自动提交的事务2,事务2执行查询,总数还是为3条,删除一条记录。
开启一个自动提交的事务3,事务3执行查询,总数变为2条。

  @Test
  void transactionCache() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession(false)) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      assertEquals(3, mapper.getSubjects().size());
      mapper.deleteSubject(1);
      assertEquals(2, mapper.getSubjects().size());
      sqlSession.rollback();
      assertEquals(3, mapper.getSubjects().size());
    }
    try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      assertEquals(3, mapper.getSubjects().size());
      mapper.deleteSubject(1);
    }
    try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      assertEquals(2, mapper.getSubjects().size());
    }
  }

执行无报错,缓存事务测试通过。

使用第三方缓存作为二级缓存

Mybatis 二级缓存默认使用 PerpetualCache 进行本地存储,不能满足分布式系统的要求。
可以通过实现你自己的缓存,来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口。

若需对缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值。

总结

Mybatis 默认只启用了本地的会话缓存,如果要开启二级缓存则另外需要增加配置,也可使用自定义的二级缓存实现。

源码分析请看下篇 Mybatis 缓存实现原理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值