mybatis缓存,从一个“灵异”事件说起

2023-12-20 00:30

本文主要是介绍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);
}

  1. xml定义
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO">SELECT ...
</select>

  1. service定义
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsTrackService extends BaseService<GoodsTrack, GoodsTrackDTO> {@Autowiredprivate 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> {@Autowiredprivate GoodsTrackService goodsTrackService;@Overridepublic 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>
  1. 然后在mapper的xml文件的 下设置cache相关配置:
<cache eviction="LRU"  flushInterval="60000" size="512" readOnly="true"/> 

支持的属性:

  • type:cache使用的类型,默认是PerpetualCache
  • eviction: 回收的策略,常见的有LRU,FIFO
  • flushInterval: 配置一定时间自动刷新缓存,单位毫秒
  • size: 最多缓存的对象个数
  • readOnly: 是否只读,若配置为可读写,则需要对应的实体类实现Serializable接口
  • blocking: 如果缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存

也可以使用 来与另一个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工具包中提供)对需要更改的数据做副本拷贝。

  1. 禁用缓存的方案

在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>

  1. 避开缓存的方案

再定义一个实现相同查询的mapper方法,id不一样来避开使用相同的缓存,这种做法就不怎么优雅了。

<select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true"> SELECT ...
</select>

避开缓存的另一种做法是不使用事务,使两个查询不在一个SqlSession中,但有时候事务是必须的,所以得分场景来。

另外由于mybatis的缓存都是基于本地的,在分布式环境下可能导致读取的数据与数据库不一致,比如一个服务实例两次读取中间,另一个服务实例对数据进行了更新,则后一次读取由于缓存还是读取的旧数据,而不是更新后的数据,可能导致问题。这时可以通过将缓存设置为STATEMENT级别来禁用mybatis缓存,通过Redis,MemCached等来提供分布式的全局缓存。

认真生活,快乐分享欢迎关注微信公众号:空山新雨的技术空间公众号二维码获取更多关于Spring Boot,Spring Cloud, Docker等企业实战技术

这篇关于mybatis缓存,从一个“灵异”事件说起的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/514245

相关文章

将Mybatis升级为Mybatis-Plus的详细过程

《将Mybatis升级为Mybatis-Plus的详细过程》本文详细介绍了在若依管理系统(v3.8.8)中将MyBatis升级为MyBatis-Plus的过程,旨在提升开发效率,通过本文,开发者可实现... 目录说明流程增加依赖修改配置文件注释掉MyBATisConfig里面的Bean代码生成使用IDEA生

C#如何动态创建Label,及动态label事件

《C#如何动态创建Label,及动态label事件》:本文主要介绍C#如何动态创建Label,及动态label事件,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#如何动态创建Label,及动态label事件第一点:switch中的生成我们的label事件接着,

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Spring Boot结成MyBatis-Plus最全配置指南

《SpringBoot结成MyBatis-Plus最全配置指南》本文主要介绍了SpringBoot结成MyBatis-Plus最全配置指南,包括依赖引入、配置数据源、Mapper扫描、基本CRUD操... 目录前言详细操作一.创建项目并引入相关依赖二.配置数据源信息三.编写相关代码查zsRArly询数据库数

Spring Boot 整合 MyBatis 连接数据库及常见问题

《SpringBoot整合MyBatis连接数据库及常见问题》MyBatis是一个优秀的持久层框架,支持定制化SQL、存储过程以及高级映射,下面详细介绍如何在SpringBoot项目中整合My... 目录一、基本配置1. 添加依赖2. 配置数据库连接二、项目结构三、核心组件实现(示例)1. 实体类2. Ma

Linux修改pip和conda缓存路径的几种方法

《Linux修改pip和conda缓存路径的几种方法》在Python生态中,pip和conda是两种常见的软件包管理工具,它们在安装、更新和卸载软件包时都会使用缓存来提高效率,适当地修改它们的缓存路径... 目录一、pip 和 conda 的缓存机制1. pip 的缓存机制默认缓存路径2. conda 的缓

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给

Mybatis从3.4.0版本到3.5.7版本的迭代方法实现

《Mybatis从3.4.0版本到3.5.7版本的迭代方法实现》本文主要介绍了Mybatis从3.4.0版本到3.5.7版本的迭代方法实现,包括主要的功能增强、不兼容的更改和修复的错误,具有一定的参考... 目录一、3.4.01、主要的功能增强2、selectCursor example3、不兼容的更改二、

mybatis-plus分页无效问题解决

《mybatis-plus分页无效问题解决》本文主要介绍了mybatis-plus分页无效问题解决,原因是配置分页插件的版本问题,旧版本和新版本的MyBatis-Plus需要不同的分页配置,感兴趣的可... 昨天在做一www.chinasem.cn个新项目使用myBATis-plus分页一直失败,后来经过多方