SpringBoot下mybatis的缓存
背景:
  说起 mybatis,作为 Java 程序员应该是⽆⼈不知,它是常⽤的数据库访问框架。与 Spring 和 Struts 组成了 Java Web 开发的三剑客---SSM。当然随着 Spring Boot 的发展,现在越来越多的企业采⽤的是 SpringBoot + mybatis 的模式开发,我们公司也不例外。⽽ mybatis 对于我也仅仅停留在会⽤⽽已,没想过怎么去了解它,更不知道它的缓存机制了,直到那个⽣死难忘的 BUG。故事的背景⽐较长,但并不是啰嗦,只是让读者知道这个 BUG 触发的场景,加深记忆。在遇到类似问题时,可以迅速定位。
  先说下故事的前提,为了防⽌⽤户在动态中输⼊特殊字符,⽤户的动态都是编码后发到后台,⽽后台在存⼊到 DB 表之前会解码以⽅便在 DB 中查看以及上报到搜索引擎。⽽在查询⽤户动态的时候先从 DB 表中读取并在后台做⼀次编码再传到前端,前端再解码既可以正常展⽰了。流程如下图:
  有⼀天后端预发环境发布完毕后,⽤户的动态页⾯有的动态显⽰正常,⽽有的动态却是被编码过的。看到现象后的第⼀个反应就是部分被编码了两次,但是编码操作只会在 service 层的 findById 中有。理论不会在上层犯这种低级错误,于是开始排查新增加的代码。发现只要进⼊了新增加代码中的某个 if 分⽀则被编码了两次。分⽀中除了再次调⽤ findById(必要性不讨论),也⽆其他特殊代码了。百思不得其解后请教了旁边的⽼司机,⽼司机说可能是 mybatis 缓存。于是看了下我代码,将编码的操作从 findByI
d 中移出来后再次发布到预发,正常了,⼼想⽼司机不愧是⽼司机。本次 BUG 触发的有两个条件需要注意:
整个操作过程都在⼀个函数中,⽽函数上⾯加了 @Transactional 的注解(对 mybatis 来说是在同⼀个 SESSION 中)
⼀般只会调⽤ findByIdy ⼀次,如果进⼊分⽀则会调⽤两次(第⼀次调⽤后做了编码后被缓存,第⼆次从缓存读后继续被编码)
  于是,便开始⾕歌 mybatis 的缓存机制,搜到了⼀篇⾮常不错的⽂章《聊聊 mybatis 的缓存机制》,推荐⼤家看⼀下,特别是⾥⾯的流程图。同时关注下美团技术官⽅,上⾯有很多⼲货(这不是⼴告)。但是这篇⽂章讲到了源码,涉及的⽐较深。⽽且并没讲SpringBoot 下 mybatis 下的⼀些缓存知识点,遂作此篇,以作补充。
缓存的配置
  SpringBoot + mybatis 环境搭建很简单⽽且⽹上⼀堆教程,这⾥不班门弄斧了,记得在项⽬中将 mytatis 的源码下载下来即可。mybaits ⼀共有两级缓存:⼀级缓存的配置 key 是 localCacheScope,⽽⼆级缓存的配置 key 是 cacheEnabled,从名字上可以得出以下信息:
⼀级缓存是本地或者说局部缓存,它不能被关闭,只能配置缓存范围。SESSION 或者 STATEMENT。
⼆级缓存才是 mybatis 的正统,功能应该会更强⼤些。
  先来看下在 SpringBoot中如何配置 mybatis 缓存的相关信息。默认情况下 SpringBoot 下的 mybatis ⼀级缓存为 SESSION 级别,⼆级缓存也是打开的,可以在 mybatis 源码中的 org.apache.ibatis.session.Configuration.class ⽂件中看到(idea中打开),如下图:
  也可以通过以下测试程序查看缓存开启情况
@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnApplicationTests {
private SqlSessionFactory factory;
@Before
public void setUp() throws Exception {
InputStream inputStream = ResourceAsStream("l");
factory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void showDefaultCacheConfiguration() {
System.out.println("⼀级缓存范围: " + Configuration().getLocalCacheScope());
System.out.println("⼆级缓存是否被启⽤: " + Configuration().isCacheEnabled());
springboot推荐算法}
}
  如果要设置⼀级缓存的缓存级别和开关⼆级缓存,在 l (当然也可以在 l/yml 中配置)加⼊如下配置即可:
<settings>
<setting name="cacheEnabled" value="true/false"/>
<setting name="localCacheScope" value="SESSION/STATEMENT"/>
</settings>
  但需要注意的是⼆级缓存 cacheEnabled 只是个总开关,如果要让⼆级缓存真正⽣效还需要在 mapper xml ⽂件中加⼊ <cache /> 。⼀级缓存只在同⼀ SESSION 或者 STATEMENT 之间共享,⼆级缓存可以跨 SESSION,开启后它们默认具有如下特性:
映射⽂件中所有的select语句将被缓存
映射⽂件中所有的insert、update和delete语句将刷新缓存
  ⼀⼆级缓存同时开启的情况下,数据的查询顺序是⼆级缓存 -> ⼀级缓存 -> 数据库。⼀级缓存⽐较简单,⽽⼆级缓存可以设置更多的属性,只需要在 mapper 的 xml ⽂件中的 <cache /> 配置即可,具体如下:
<cache
type = "batis.caches.ehcache.LoggingEhcache"  //指定使⽤的缓存类,mybatis默认使⽤HashMap进⾏缓存,可以指定第三⽅缓存
eviction = "LRU"  //默认是 LRU 淘汰缓存的算法,有如下⼏种:
//1.LRU – 最近最少使⽤的:移除最长时间不被使⽤的对象。
//2.FIFO – 先进先出:按对象进⼊缓存的顺序来移除它们。
//3.SOFT – 软引⽤:移除基于垃圾回收器状态和软引⽤规则的对象。
//4.WEAK – 弱引⽤:更积极地移除基于垃圾收集器状态和弱引⽤规则的对象
flushInterval = "1000"  //清空缓存的时间间隔,单位毫秒,可以被设置为任意的正整数。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调⽤语句时刷新。
size = "100"      //缓存对象的个数,任意正整数,默认值是1024。
readOnly  = "true"  //缓存是否只读,提⾼读取效率
blocking = "true"  //是否使⽤阻塞缓存,默认为false,当指定为true时将采⽤BlockingCache进⾏封装,
blocking,
//阻塞的意思,使⽤BlockingCache会在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,
//否则会在查询数据库以后再释放锁这样可以阻⽌并发情况下多个线程同时查询数据,详情可参考BlockingCache的源码。
/>
触发 mybatis 缓存
  (1)配置⼀级缓存为 SESSION 级别
  Controller 中做两次调⽤,代码如下:
@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
//第⼀次调⽤
UserEntity One(id);
//第⼆次调⽤
UserEntity One(id);
return user1;
}
  调⽤:,打印结果如下:
  从图中的 1/2/3/4 可以看出每次 mapper 层的⼀次接⼝调⽤如 getOne 就会创建⼀个 session,并且在执⾏完毕后关闭 session。所以两次调⽤并不在⼀个 session 中,⼀级缓存并没有发⽣作⽤。开启事务,Controller 层代码如下:
@RequestMapping("/getUser")
@Transactional(rollbackFor = Throwable.class)
public UserEntity getUser(Long id) {
//第⼀次调⽤
UserEntity One(id);
//第⼆次调⽤
UserEntity One(id);
return user1;
}
  打印结果如下:
  由于在同⼀个事务中,虽然调⽤了 select 操作两次但是只执⾏了⼀次 sql ,缓存发挥了作⽤。这就跟⼀开始我遇到的那个 BUG 场景⼀样:同⼀ session 且 select 调⽤ > 1 次。如果在两次调⽤中间插⼊ update 操作,缓存会⽴即失效。只要 session 中有 insert、update 和delete 语句,该 session 中的缓存会⽴即被刷新。但是注意这只是在同⼀ session 之间。不同 session 之间如 session1 和
session2,session1 ⾥的 insert/update/delete 并不会影响 session 2 下的缓存,这在⾼并发或者分布式的情况下会产⽣脏数据。所以建议将⼀级缓存级别调成 statement。
  (2)配置⼀级缓存为 STATEMENT 级别
  再次将(1)中的⽆事务和有事务的代码分别执⾏⼀遍,打印结果始终如下:
  配置成 SATEMENT 后,⼀级缓存相当于被关闭了。STATEMENT 级别暂时不好模拟,但是我猜测 STATEMENT 级别即在同⼀执⾏sql 的接⼝中(如上⾯的 getOne 中)缓存,出了 getOne 缓存即失效。
  (3)⼆级缓存,同时为了避免⼀级缓存的⼲扰,将⼀级缓存设置为 STATEMENT
  Controller 中去掉 @Transactional 注解代码如下:
@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
UserEntity One(id);
UserEntity One(id);
return user1;
}
  ⼆级缓存开关保证打开,在 mapper xml ⽂件中加⼊ <cache />,整个⽂件代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-////DTD Mapper 3.0//EN" "/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.binggle.learn.dao.mapper.UserMapper" >
<resultMap id="BaseResultMap" type="com.binggle.ity.UserEntity" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="sex" property="sex"/>
</resultMap>
<sql id="Base_Column_List" >
id, name, sex
</sql>
<select id="getOne" parameterType="java.lang.Long" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM users
WHERE id = #{id};
</select>
<cache />
</mapper>
  从图中红框可以看出第⼆次查询命中缓存,0.5 是命中率,
  这次⼀次 sql 也没执⾏了,所以说⼆级缓存全局缓存。但它的缓存范围也是有限的,⼀级缓存在同⼀个 session 中。⼆级缓存可以跨session 但也只能在同⼀ namespace 中,所谓 namespace 即 mapper xml ⽂件中。具体实验请看《聊聊 mybatis 的缓存机制》中的关于⼆级缓存的实验 4 和 5。再
看下⼆级缓存配置对⼆级缓存的影响,为了明显的看出效果,只改如下配置:
<cache
size="1"            //⼀次只能缓存⼀个对象
flushInterval="5000" //刷新时间为 5s
/>
  controller 代码:
@RequestMapping("/getUser")
public UserEntity getUser(Long id, Long id2) {
//第⼀个对象 1
System.out.println("================缓存对象 1=================");
UserEntity user1 = One(id);
//另⼀个对象 2
System.out.println("========缓存对象 2,剔除缓存中的对象 1=======");
UserEntity One(id2);
user2 = One(id2);
//再次读取第⼀个对象
System.out.println("==========缓存被剔除,执⾏查询 sql===========");
user1 = One(id);
//暂停 5s
try {
sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("============5s 后再次查询对象 2=============");
user2 = One(id2);
return user1;
}
  太长了,拼接下:
  可以看出⼆级缓存只能缓存⼀个对象且 5s 后就失效了,缓存失效。
总结:
  我推荐的⽂章中总结的已经⾮常好了,直接引⽤下:
1、MyBatis⼀级缓存的⽣命周期和SqlSession⼀致。
2、MyBatis⼀级缓存内部设计简单,只是⼀个没有容量限定的HashMap,在缓存的功能性上有所⽋缺。
3、MyBatis的⼀级缓存最⼤范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建
议设定缓存级别为Statement。
4、MyBatis的⼆级缓存相对于⼀级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace
级别,通过Cache接⼝实现类不同的组合,对Cache的可控性也更强。
5、MyBatis在多表查询时,极⼤可能会出现脏数据,有设计上的缺陷,安全使⽤⼆级缓存的条件⽐较苛刻。
6、在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使⽤集中
式缓存将MyBatis的Cache接⼝实现,有⼀定的开发成本,直接使⽤Redis、Memcached等分布式缓存可能成本更低,安全性也更⾼。
7. 个⼈建议MyBatis缓存特性在⽣产环境中进⾏关闭,单纯作为⼀个ORM框架使⽤可能更为合适。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。