mybatis缓存,从一个“灵异”事件说起
刚准备下班走人,被一开发同事叫住,让帮看一个比较奇怪的问题:Mybatis同一个Mapper接口的查询方法,第一次返回与第二次返回结果不一样,百思不得其解!
问题
Talk is cheap. Show me the code. 该问题涉及的主要代码实现包括
1.mapper接口定义
public interface GoodsTrackMapper extends BaseMapper<GoodsTrack> {
List<GoodsTrackDTO> listGoodsTrack(@Param("criteria") GoodsTrackQueryCriteria criteria);
}
2.xml定义
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO">
SELECT ...
</select>
3.service定义
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsTrackService extends BaseService<GoodsTrack, GoodsTrackDTO> {
@Autowired
private GoodsTrackMapper goodsTrackMapper;
public List<GoodsTrackDTO> listGoodsTrack(GoodsTrackQueryCriteria criteria){
return goodsTrackMapper.listGoodsTrack(criteria);
}
public List<GoodsTrackDTO> goodsTrackList(GoodsTrackQueryCriteria criteria){
List<GoodsTrackDTO> listGoodsTrack = goodsTrackMapper.listGoodsTrack(criteria);
Map<String, GoodsTrackDTO> goodsTrackDTOMap = new HashMap<String, GoodsTrackDTO>();
for (GoodsTrackDTO goodsTrackDTO : listGoodsTrack){
String goodsId = String.valueOf(goodsTrackDTO.getGoodsId());
if (!goodsTrackDTOMap.containsKey(goodsId)){
goodsTrackDTOMap.put(goodsId, goodsTrackDTO);
}else {
GoodsTrackDTO goodsTrack = goodsTrackDTOMap.get(goodsId);
int num = goodsTrack.getGoodsNum() + goodsTrackDTO.getGoodsNum();
goodsTrack.setGoodsNum(num);
}
}
List<GoodsTrackDTO> list = new ArrayList(goodsTrackDTOMap.values());
return list;
}
}
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsOrderService extends BaseService<GoodsOrder, GoodsOrderDTO> {
@Autowired
private GoodsTrackService goodsTrackService;
@Override
public GoodsOrderDTO create(GoodsOrderDTO goodsOrderDTO) {
//...
List<GoodsTrackDTO> rs1 = goodsTrackList(criteria);
//...
List<GoodsTrackDTO> rs2 = listGoodsTrack(criteria);
//...
}
}
大致逻辑就是在 GoodsTrackService
定义了两个查询方法,一个是直接从数据库中获取数据,第二个是从数据库中获取数据后进行了一些加工(通过某个字段进行合并累加,类似sum group by),然后在GoodsOrderService
的同一个方法(该方法是一个事务方法 )中调用这两个查询,发现rs2中的数据存在问题, 期望是都应该与数据库表的数据一致,但其中部分数据却与查出后进行了修改的rs1中的一致。
定位
初步看,listGoodsTrack
方法直接调用的mapper方法 goodsTrackMapper.listGoodsTrack(criteria)
没做任何应用层的处理,第一反应是缓存的原因。 我问前面的查询有没有改变查询返回的结果(一开始没细看具体实现),答曰没有。折腾一阵后,返过去细看 goodsTrackList
的实现,果然还是眼见为实、耳听为虚。在该方法中,通过goodsId对返回的列表进行分组,对goodsNum进行累加,最后返回累加后的几个对象。但是在累加的时候,是直接作用于返回结果对象的,明明就是改变了查询结果(居然说没有?!!)。 这就是问题所在了,mybatis在同一个事务中,对同一个查询(同样的sql,同样的参数)的返回结果进行了缓存(称为一级缓存),下一次做同样的查询时,如果中间没有任何更新操作,则直接返回缓存的数据,而在本例中因为对缓存数据做了人为的修改,所以最后导致查出的数据与数据库不一致。
mybatis缓存机制
简单介绍下mybatis的两级缓存机制
一级缓存:一级缓存包括SqlSession与STATEMENT两种级别,默认在 SqlSession 中实现。在一次会话中,如果两次查询sql相同,参数相同,且中间没有任何更新操作,则第二次查询会直接返回第一次查询缓存的结果,不再请求数据库。如果中间存在更新操作,则更新操作会清除掉缓存,后面的查询就会访问数据库了。STATEMENT级别则每次查询都会清掉一级缓存,每次查询都会进行数据库访问。
二级缓存:二级缓存则是在同一个namesapce的多个 SqlSession 间共享的缓存,默认未开启。当开启二级缓存后,数据查询的流程就是 二级缓存 ——> 一级缓存 ——> 数据库, 同一个namespace下的更新操作,会影响同一个Cache。
如何开启二级缓存
1.需要在mybatis-config.xml中设置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
2.然后在mapper的xml文件的<mapper>下设置cache相关配置:
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
支持的属性:
- type:cache使用的类型,默认是PerpetualCache
- eviction: 回收的策略,常见的有LRU,FIFO
- flushInterval: 配置一定时间自动刷新缓存,单位毫秒
- size: 最多缓存的对象个数
- readOnly: 是否只读,若配置为可读写,则需要对应的实体类实现Serializable接口
- blocking: 如果缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存
也可以使用 <cache-ref namespace="mapper.UserMapper"/>
来与另一个mapper共享二级缓存
解决
已经定位到是由于mybatis的一级缓存导致,那如何解决本文提到的问题呢? 基本上有三个解决方向。
1.使用缓存的方案
既然要使用缓存,那就不能更改缓存的数据,此时我们可以在需要更改数据的地方把数据做一次副本拷贝,使其不改变缓存数据本身, 如
for (GoodsTrackDTO goodsTrackDTO : listGoodsTrack){
String goodsId = String.valueOf(goodsTrackDTO.getGoodsId());
if (!goodsTrackDTOMap.containsKey(goodsId)){
goodsTrackDTOMap.put(goodsId, ObjectUtil.clone(goodsTrackDTO));
}else {
GoodsTrackDTO goodsTrack = goodsTrackDTOMap.get(goodsId);
int num = goodsTrack.getGoodsNum() + goodsTrackDTO.getGoodsNum();
goodsTrack.setGoodsNum(num);
}
}
使用ObjectUtil.clone()方法(hutool工具包中提供)对需要更改的数据做副本拷贝。
2.禁用缓存的方案
在xml的sql定义中添加 flushCache="true" 的配置,使该查询不使用缓存,如下
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ...
</select>
禁用缓存的另一种方案是将一级缓存直接设置为STATEMENT来进行全局禁用,在mybatis-config.xml中配置:
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
3.避开缓存的方案
再定义一个实现相同查询的mapper方法,id不一样来避开使用相同的缓存,这种做法就不怎么优雅了。
<select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ...
</select>
避开缓存的另一种做法是不使用事务,使两个查询不在一个SqlSession中,但有时候事务是必须的,所以得分场景来。
另外由于mybatis的缓存都是基于本地的,在分布式环境下可能导致读取的数据与数据库不一致,比如一个服务实例两次读取中间,另一个服务实例对数据进行了更新,则后一次读取由于缓存还是读取的旧数据,而不是更新后的数据,可能导致问题。这时可以通过将缓存设置为STATEMENT级别来禁用mybatis缓存,通过Redis,MemCached等来提供分布式的全局缓存。
认真生活,快乐分享
欢迎关注微信公众号:空山新雨的技术空间
获取更多关于Spring Boot,Spring Cloud, Docker等企业实战技术
mybatis缓存,从一个“灵异”事件说起的更多相关文章
- 用MsmqBinding投送message出现的一个灵异事件 【第二篇】
一直都在用Msmqbinding,也一直忽视了message里面的内容格式是什么样的,这也是微软给我们高层封装带给我们的开发效率,但同时一旦中间出了什么问题, 就不知道从何查起了.有个需求是这样的,服 ...
- SWUST OJ 东6宿舍灵异事件(0322)
东6宿舍灵异事件(0322) Time limit(ms): 1000 Memory limit(kb): 65535 Submission: 88 Accepted: 31 Descriptio ...
- [Swust OJ 322]--东6宿舍灵异事件(中缀表达式转化为后缀表达式的简单运用)
题目链接:http://acm.swust.edu.cn/problem/322/ Time limit(ms): 1000 Memory limit(kb): 65535 Descripti ...
- 记录一起k8s的service服务名解析灵异事件
故障现象: 基于alpine 3.7的镜像,构建的spring boot服务及eureka服务器. 在使用deployment和service文件部署到k8s集群之后, 在不同的pod内部,访问ser ...
- android中listview点击事件失效的灵异事件
首先说明一下我想实现的功能: 点击某个item之后,让其颜色发生变化.如果变化网上有很多例子,我就不班门弄斧了.Listview之所以点击没有反应是因为上图中绿色部分(自己定义的一个继承BaseAda ...
- redis系列-14点的灵异事件
概述 项目组每天14点都会遭遇惊魂时刻.一条条告警短信把工程师从午后小憩中拉回现实.之后问题又神秘消失.是PM喊你上工了?还是服务器给你开玩笑?下面请看工程师如何一步一步揪出真凶,解决问题. 如果不想 ...
- MySQL 灵异事件一则 -- desc报语法错误
今天有一开发同学找到我,说查询SQL中倒序报错,不明原因,于是奔赴工位现场研究情况. 果然,只要SQL中带有desc 就会报错,而ASC没问题. 哪怕desc放在句首用作explain也会报错. 报错 ...
- AngularJS进阶(十四)AngularJS灵异代码事件
AngularJS灵异代码事件 注:请点击此处进行充电! 事情原委 router_sys.js源代码如下: 自己在html路由跳转的代码如下: 但是在实际路由过程中,却路由到了下面的状态,相应的页面中 ...
- 高效开发之SASS篇 灵异留白事件——图片下方无故留白 你会用::before、::after吗 link 与 @import之对比 学习前端前必知的——HTTP协议详解 深入了解——CSS3新增属性 菜鸟进阶——grunt $(#form :input)与$(#form input)的区别
高效开发之SASS篇 作为通往前端大神之路的普通的一只学鸟,最近接触了一样稍微高逼格一点的神器,特与大家分享~ 他是谁? 作为前端开发人员,你肯定对css很熟悉,但是你知道css可以自定义吗?大家 ...
随机推荐
- 洛谷$P2570\ [ZJOI2010]$贪吃的老鼠 网络流+二分
正解:网络流+二分 解题报告: 传送门$QwQ$ 和上一题有点儿像,,,?$QwQ$但是比上一题要有趣很多$QwQ$ 首先把大致思路捋下?依然是.二分出每个奶酪的开始和结束时间,然后check下最大流 ...
- Google被墙怎么办?
Google被墙怎么办? 1 声明 请小伙伴们遵守法律法规,我们只是为了更好的查询学习资料. 想使用Google查询相关资料 想使用Google账号管理收藏夹 想使用Google商店安装软件 == 2 ...
- .NET Core 3.0 System.Text.Json 和 Newtonsoft.Json 行为不一致问题及解决办法
行为不一致 .NET Core 3.0 新出了个内置的 JSON 库, 全名叫做尼古拉斯 System.Text.Json - 性能更高占用内存更少这都不是事... 对我来说, 很多或大或小的项目能少 ...
- Activity启动过程源码分析(Android 8.0)
Activity启动过程源码分析 本文来Activity的启动流程,一般我们都是通过startActivity或startActivityForResult来启动目标activity,那么我们就由此出 ...
- asp.net core 实现支持多语言
asp.net core 实现支持多语言 Intro 最近有一个外国友人通过邮件联系我,想用我的活动室预约,但是还没支持多语言,基本上都是写死的中文,所以最近想支持一下更多语言,于是有了多语言方面的一 ...
- 从0开发3D引擎(五):函数式编程及其在引擎中的应用
目录 上一篇博文 函数式编程的优点与缺点 优点 缺点 为什么使用Reason语言 函数式编程学习资料 引擎中相关的函数式编程知识点 数据 不可变数据 可变数据 函数 纯函数 高阶函数 柯西化 参考资料 ...
- 关于i++的底层原理分析
首先看一道典型题 public class Test { static int x, y, z; static { int x = 5;//局部变量 x--; } static { x--; } pu ...
- a标签属性href值为#和javasrcript:void(0)的区别
当我们需要一个空链接时,通常有两种方法: <a href="#">这个一个空链接</a> <a href="javascript:void( ...
- Redis系列之----Redis的两种持久化机制(RDB和AOF)
Redis的两种持久化机制(RDB和AOF) 什么是持久化 Redis的数据是存储在内存中的,内存中的数据随着服务器的重启或者宕机便会不复存在,在生产环境,服务器宕机更是屡见不鲜,所以,我们希望 ...
- C#多线程与异步
1.什么是异步同步 如果一个方法被调用,调用者需要等待该方法被执行完毕之后才能继续执行,则是同步. 如果方法被调用后立刻返回,即使该方法是一个耗时操作,也能立刻返回到调用者,调用者不需要等待该方法,则 ...