背景:

  说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架。与 Spring 和 Struts 组成了 Java Web 开发的三剑客--- SSM。当然随着 Spring Boot 的发展,现在越来越多的企业采用的是 SpringBoot + mybatis 的模式开发,我们公司也不例外。而 mybatis 对于我也仅仅停留在会用而已,没想过怎么去了解它,更不知道它的缓存机制了,直到那个生死难忘的 BUG。故事的背景比较长,但并不是啰嗦,只是让读者知道这个 BUG 触发的场景,加深记忆。在遇到类似问题时,可以迅速定位。

  先说下故事的前提,为了防止用户在动态中输入特殊字符,用户的动态都是编码后发到后台,而后台在存入到 DB 表之前会解码以方便在 DB 中查看以及上报到搜索引擎。而在查询用户动态的时候先从 DB 表中读取并在后台做一次编码再传到前端,前端再解码既可以正常展示了。流程如下图:

  有一天后端预发环境发布完毕后,用户的动态页面有的动态显示正常,而有的动态却是被编码过的。看到现象后的第一个反应就是部分被编码了两次,但是编码操作只会在 service 层的 findById 中有。理论不会在上层犯这种低级错误,于是开始排查新增加的代码。发现只要进入了新增加代码中的某个 if 分支则被编码了两次。分支中除了再次调用 findById(必要性不讨论),也无其他特殊代码了。百思不得其解后请教了旁边的老司机,老司机说可能是 mybatis 缓存。于是看了下我代码,将编码的操作从 findById 中移出来后再次发布到预发,正常了,心想老司机不愧是老司机。本次 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 = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
factory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void showDefaultCacheConfiguration() {
System.out.println("一级缓存范围: " + factory.getConfiguration().getLocalCacheScope());
System.out.println("二级缓存是否被启用: " + factory.getConfiguration().isCacheEnabled());
}
}

  

  如果要设置一级缓存的缓存级别和开关二级缓存,在 mybatis-config.xml (当然也可以在 application.xml/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 = "org.mybatis.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 user1=userMapper.getOne(id);
//第二次调用
UserEntity user2=userMapper.getOne(id);
return user1;
}

  调用:http://localhost:8080/getUser?id=1,打印结果如下:

  从图中的 1/2/3/4 可以看出每次 mapper 层的一次接口调用如 getOne 就会创建一个 session,并且在执行完毕后关闭 session。所以两次调用并不在一个 session 中,一级缓存并没有发生作用。开启事务,Controller 层代码如下:

@RequestMapping("/getUser")
@Transactional(rollbackFor = Throwable.class)
public UserEntity getUser(Long id) {
//第一次调用
UserEntity user1=userMapper.getOne(id);
//第二次调用
UserEntity user2=userMapper.getOne(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 user1=userMapper.getOne(id);
UserEntity user2=userMapper.getOne(id);
return user1;
}

  二级缓存开关保证打开,在 mapper xml 文件中加入 <cache />,整个文件代码如下:

<?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="com.binggle.learn.dao.mapper.UserMapper" >
<resultMap id="BaseResultMap" type="com.binggle.learn.dao.entity.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>

  执行 http://localhost:8080/getUser?id=1,打印结果如下:

  从图中红框可以看出第二次查询命中缓存,0.5 是命中率,

  再次执行 http://localhost:8080/getUser?id=1 打印结果如下:

  这次一次 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 = userMapper.getOne(id); //另一个对象 2
System.out.println("========缓存对象 2,剔除缓存中的对象 1=======");
UserEntity user2=userMapper.getOne(id2);
user2 = userMapper.getOne(id2); //再次读取第一个对象
System.out.println("==========缓存被剔除,执行查询 sql===========");
user1 = userMapper.getOne(id); //暂停 5s
try {
sleep(5000);
}catch (Exception e){
e.printStackTrace();
} System.out.println("============5s 后再次查询对象 2=============");
user2 = userMapper.getOne(id2); return user1;
}

  http://localhost:8080/getUser?id=1&id2=2 最后打印的结果如下:

  太长了,拼接下:

  可以看出二级缓存只能缓存一个对象且 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框架使用可能更为合适。

SpringBoot 下 mybatis 的缓存的更多相关文章

  1. spring-boot下mybatis的配置

    问题描述:spring boot项目想添加mybatis的配置,在src/main/resources目录下新建了mybatis-config.xml文件,在application.propertie ...

  2. Mybatis 一级缓存和二级缓存原理区别 (图文详解)

    Java面试经常问到Mybatis一级缓存和二级缓存,今天就给大家重点详解Mybatis一级缓存和二级缓存原理与区别@mikechen Mybatis缓存 缓存就是内存中的数据,常常来自对数据库查询结 ...

  3. SpringBoot之Mybatis操作中使用Redis做缓存

    上一博客学习了SpringBoot集成Redis,今天这篇博客学习下Mybatis操作中使用Redis做缓存.这里其实主要学习几个注解:@CachePut.@Cacheable.@CacheEvict ...

  4. spring-boot集成mybatis,用redis做缓存

    网上有很多例子了,执行源码起码有3个,都是各种各样的小问题. 现在做了个小demo,实现spring-boot 用redis做缓存的实例,简单记录下思路,分享下源码. 缓存的实现,分担了数据库的压力, ...

  5. (04) springboot 下的springMVC和jsp和mybatis

    1. springboot 和springmvc下的注解完全一样(新增了一些有用的) 常用的注解如下: @Controller @RestController= @Controller + @Resp ...

  6. (4)一起来看下mybatis框架的缓存原理吧

    本文是作者原创,版权归作者所有.若要转载,请注明出处.本文只贴我觉得比较重要的源码,其他不重要非关键的就不贴了 我们知道.使用缓存可以更快的获取数据,避免频繁直接查询数据库,节省资源. MyBatis ...

  7. Springboot整合Ehcache 解决Mybatis二级缓存数据脏读 -详细

    前面有写了一篇关于这个,但是这几天又改进了一点,就单独一篇在详细说明一下 配置 application.properties ,启用Ehcache # Ehcache缓存 spring.cache.t ...

  8. spring-boot整合mybatis(1)

    sprig-boot是一个微服务架构,加快了spring工程快速开发,以及简便了配置.接下来开始spring-boot与mybatis的整合. 1.创建一个maven工程命名为spring-boot- ...

  9. SpringBoot配置mybatis

    一直都说SpringBoot是零配置,当然,真正实现零配置是不可能的,但是在配置mybatis这里真的是太简单了,哈哈,下面我们一起看一下. 1.先导入基于SpringBoot的mybatis依赖包 ...

随机推荐

  1. 对比剖析Swarm Kubernetes Marathon编排引擎

    Docker Native Orchestration 基本结构 Docker Engine 1.12 集成了原生的编排引擎,用以替换了之前独立的Docker Swarm项目.Docker原生集群(S ...

  2. 练手——用Python写的时间戳转换为北京时间的小工具

    #北京时间需加上8小时bj = 8*3600 def time_stamp(times):    #一天总秒数    nonDaySeconds = 24*3600    leapmonths = [ ...

  3. -bash: belts.awk: command not found

    执行awk命令时,没有问题.可是执行awk脚本时,出现这个问题:-bash: belts.awk: command not found. 既然之前直接执行awk命令没有问题,说明awk已经装了,本身是 ...

  4. PHP7.1 mcrypt_module_open() is deprecated

    PHP7.1 mcrypt_module_open() is deprecated 一:函数前添加抑制符 @ mcrypt_module_open....;->@mcrypt_module_op ...

  5. HAproxy指南之haproxy实现动静分离(案例篇)

    HAproxy指南之haproxy实现动静分离(案例篇) 转自   https://blog.51cto.com/blief/1751806   实际应用环境中,往往需要根据业务请求将相关不同请求跳转 ...

  6. Linux网络属性管理

    Linux网络属性管理 局域网:以太网,令牌环网 Ethernet: CSMA/CD 冲突域 广播域 MAC:Media Access Control 48bits: 24bits: 24bits: ...

  7. 删除zabbix数据库日志

    #!/bin/bashuser="root"passwd="361way"timedate=`date -d $(date -d "-90 day&q ...

  8. opencv检错:程序运行过程正常,当跳出函数时出现断言错误(Debug Assertion Failed)

    转载http://blog.csdn.net/u012327581/article/details/51351780 1.问题描述 在VS2015下配置好Opencv后,程序在函数运行过程中正常,调试 ...

  9. FJNU Fang G and his Friends(状压DP)题解

    Description     众所周知,fang G 有很多小伙伴,有一天,Fang G 打算带他们去玩有趣的游戏OOXX,这个游戏需要分成两组,有趣的是,每个人互相之间都有一个满意度,大家都想和自 ...

  10. Redis Index

    Indexes 集群 主从模型 哨兵机制与RAFT算法 实践 单机多实例 开启Sentinel 存储 持久化 RDB 与 AOF 数据结构 内存管理 事务 并发问题 分布式锁 整体图 中间件 Jedi ...