抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

MyBatis缓存

缓存是优化中最常见的手段,像计算机中的多级缓存策略,在互联网应用中,数据库往往是整个链路中的性能瓶颈,使用缓存减轻数据库的压力,提高系统的性能。

一级缓存

一级缓存是 SqlSession 级别的缓存。SqlSession 是操作数据库的对象。不同的 SqlSession 之间的缓存相互不影响。

工作原理

用户查询时,SqlSession 先从缓存中查找是否有这条数据,如果有就读取,如果没有,就从数据库中查询,并且将查询数据放入一级缓存,给下一个查找使用。

如果 SqlSession 执行了 commit 操作,也就是增删改的时候,就会清空缓存,目的是为了防止读到脏数据。

二级缓存

需要二级缓存的原因

  1. 上面说到,一级缓存不同 SqlSession 之间是隔离的,二级缓存是 mapper 级别的缓存,解决了跨 SqlSession 的问题,作用范围更大。
  2. MyBatis 与 Spring 整合。Spring 是将事务放在 Service 中管理的,对于每一个 Service 中的 SqlSession 是不同的,通过 mybatis-spring 中的 org.mybatis.spring.mapper.MapperScannerConfigurer 创建 SqlSession 自动注入 service 中。在不带事务的方法中,每次查询都要关闭 SqlSession,关闭之后数据就清空了;带事务的方法中,每次请求数据库,只会创建一个 SqlSession,就可以使用到一级缓存。所以 spring 整合后,如果没有事务,一级缓存 是没有用的。

工作原理

可以看到二级缓存是 mapper 级别的缓存,多个 SqlSession 可以公用缓存,是跨 SqlSession 的。

每个 mapper 中都有一个二级缓存区域(按照 namespace 划分)。如果两个 mapper 的 namespace 相同,那么这两个 mapper 执行的 sql 查询到的数据会缓存到同一个二级缓存区域中。

使用二级缓存

打开总开关,修改 mybatis 全局配置文件mybatis-config.xml

<settings>
    <!-- 开启二级缓存 -->
    <setting name="cacheEnabled" value="true"/>
</settings>

在需要开始二级缓存的 mapper.xml 中加入 cache 标签

<cache/>

使用二级缓存的 POJO 类实现 Serializable 接口

public class User implements Serializable {
}

测试

@Test
public void testCache2() throws Exception {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    User user1 = userMapper1.findUserById(1);
    System.out.println(user1);
    sqlSession1.close();
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    User user2 = userMapper2.findUserById(1);
    System.out.println(user2);
    sqlSession2.close();
}

输出log

DEBUG [main] - Cache Hit Ratio [com.iot.mybatis.mapper.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 103887628.
DEBUG [main] - Setting autocommit to false on JDBC Connection
[com.mysql.jdbc.JDBC4Connection@631330c]
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=张三, sex=1, birthday=null, address=null]

DEBUG [main] - Resetting autocommit to true on JDBC Connection
[com.mysql.jdbc.JDBC4Connection@631330c]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@631330c]
DEBUG [main] - Returned connection 103887628 to pool.
DEBUG [main] - Cache Hit Ratio [com.iot.mybatis.mapper.UserMapper]: 0.5
User [id=1, username=张三, sex=1, birthday=null, address=null]

从 log 中可以看到,两个不同的 SqlSession 去查询同一条数据,只发起了一次 select 查询语句,第二次是从 Cache 中读取的。

一级缓存,在和 Spring 整合之后,如果没有事务,每次查询都是会关闭 SqlSession 的,关闭就意味数据清空,而二级缓存,即使关闭了 SqlSession,也会把 SqlSession 一级缓存中的数据添加到 namespace 的二级缓存中。这样即使关闭了,缓存依旧存在。

cache 标签

<!-- 
 eviction:缓存置换策略,先进先出
 flushInterval:清空缓存间隔
 size:最多缓存引用512个对象
 readOnly:只读
 -->
<cache
    eviction="FIFO" 
    flushInterval="60000" 
    size="512"
    readOnly="true"/>

cache 有以下属性,每种属性都对低层 Cache 的一种装饰,采用 装饰器模式 实现的。

  • blocking:默认是 false,指定为 true 时,将采用 BlockingCache 包装。在查询缓存时会锁定对应的 key,如果命中了则释放,否则在查询数据库以后再释放锁,这样可以阻止并发下多个线程查询同时命中数据。
  • eviction:驱逐之意,默认是 LRU。如果想要自己实现置换算法,实现 Mybatis 的 Cache 接口即可。
    • LRU:默认保存1024个key,超出就按照最近最少使用算法淘汰。
    • FIFO:先进先出。
    • SOFT:采用软引用存储 Value。
    • WEAK:采用弱引用存储 Value。
  • flushInterval:清空缓存时间间隔,单位毫秒,默认不清空。使用 ScheduleCache 包装。会在对缓存进行操作时判断最近一次清空缓存的时间是否超过了 flushInterval 的时间,如果超过了就清空。
  • readOnly:是否只读,默认 false。false 时使用 SerializableCache 包装。每次写的时候,对缓存对象进行序列化,然后再读缓存的时候进行发序列化,这样每次读到的都将是一个新的对象,即使修改了也不会影响原来的对象。也就是非只读,拿到的缓存结果可以进行修改,而不会影响原来的缓存结果。当为 true 时,每次获取到的都是同一个引用,修改会影响后续的缓存读取,这种情况不建议对获取到的缓存进行更改,也就是只读。虽然每次序列化和反序列化会影响性能,但是这样操作缓存结果更加安全,使用还是要看具体业务场景。
  • size:缓存中最多可保存的key数量。
  • type:指定当前低层缓存实现类。默认是 PerceptualCache。如果要指定自定义的Cache,通过该属性来指定自定义Cache类的全路径即可。

cache-ref 标签

<cache-ref namespace="cn.lgq.dao.UserMapper"/>

该标签用来指定 mapper.xml 中定义的 cache,有时候需要多个 mapper 共享一个缓存。

然后只需要保证它们的key是一致的即可命中,二级缓存的key是通过 Executor 接口的 createCacheKey() 方法生成的,其基本实现是 BaseExecutor。

缓存使用总结

对于查询多,commit少而且用户对查询结果实时性要求不高的业务场景,可以使用二级缓存来降低数据库量的访问,提高访问的速度。

不能滥用二级缓存,弊端很多,所以MyBatis默认都是关闭二级缓存的。二级缓存是建立在同一个 namespace 下的,如果对表的操作查询可能有多个 namespace,那么得到的数据就是错误的。

例如:订单 和 订单详情 分别是 orderMapper、orderDetailMapper。在查询订单详情(orderDetailMapper)时,需要把订单信息(orderMapper)也查询出来,那么这个订单详情(orderDetailMapper)的信息被二级缓存在 orderDetailMapper 的 namespace 中,这时候有人要修改订单的基本信息(orderMapper),那就是在 orderMapper 的 namespace 下修改,但它是不会影响到 orderDetailMapper 的缓存的,那么再次查找订单详情时,拿到的是缓存的数据其实已经是过时的。

使用原则

  • 只能在一个命名空间下使用二级缓存。由于二级缓存的不同 namespace 中的数据互不干扰,所以在多个 namespace 对同一个表进行操作时,就会出现不一致的情况。
  • 在单表上使用二级缓存。如果一个表与其他表有管理,那么非常可能存在多个 namespace 对同一个数据进行操作了。这样就会很容易出现 namespace 中数据不一致的问题。
  • 查询多余修改时使用二级缓存。查询操作远多于增删改的情况下使用。增删改操作都会刷新二级缓存,频繁刷新会影响性能。

评论