一个事务里先查库存再扣减,第二次查询拿到旧值:MyBatis 一级缓存把超卖藏在了本地 Map
title: 一个事务里先查库存再扣减第二次查询拿到旧值MyBatis 一级缓存把超卖藏在了本地 Maptags: [MyBatis, 一级缓存, Executor, 源码分析, 事务]category: 后端大促零点库存变成了负数那次大促我们给秒杀接口挂了限流本以为万无一失。结果开抢 3 分钟库存表出现了-37这个离谱的数字超卖 37 单。第一反应是并发没加锁但加了SELECT ... FOR UPDATE还是复现。最后定位到一个谁都没注意的地方同一个 Spring 事务里查库存、扣库存、再查库存第二次查根本没走数据库拿的是 MyBatis 一级缓存里的旧对象。事故现场看起来毫无问题的三段逻辑扣减接口长这样单线程跑永远正确问题出在缓存。Transactional public void deduct(Long skuId, int count) { Stock stock stockMapper.selectBySku(skuId); // 1. 查库存比如 100 if (stock.getCount() count) { throw new BizException(库存不足); } stock.setCount(stock.getCount() - count); // 2. 内存里减 stockMapper.updateById(stock); // 3. 写回数据库 Stock again stockMapper.selectBySku(skuId); // 4. 再查期望 100-count log.info(扣减后库存{}, again.getCount()); // 5. 日志里还是 100 }第 4 步的again.getCount()打印出来还是 100而不是 100-count。我们一度怀疑updateById没生效直到手动在数据库客户端查值明明已经更新了。差别在于Java 这边第二次selectBySku压根没发 SQL。源码逐行为什么第二次查询「消失」了MyBatis 的 Mapper 接口是动态代理调用会先经过MapperProxy最终落到SqlSession的selectOne再交给Executor。// MapperProxy.invoke —— 每个 Mapper 方法调用的入口 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 1. 把方法包装成 MapperMethod统一走 execute final MapperMethod mapperMethod cachedMapperMethod(method); // 2. 这里转而调用 sqlSession 的 select/insert进入 Executor 链 return mapperMethod.execute(sqlSession, args); }关键在于sqlSession是谁给的。Spring 环境下MapperFactoryBean注入的是SqlSessionTemplate它会从事务同步管理器里拿SqlSession// SqlSessionTemplate.SqlSessionInterceptor.invoke public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 从事务上下文取「当前线程绑定的 SqlSession」 SqlSession sqlSession getSqlSession(sqlSessionFactory, executorType, exceptionTranslator); try { // 2. 复用同一个会话整个 Transactional 方法共用一个 SqlSession return method.invoke(sqlSession, args); } finally { // ... } }因为整个deduct方法在一个事务里getSqlSession每次都返回同一个SqlSession。而这个SqlSession背后的CachingExecutor→BaseExecutor维护了一个localCachePerpetualCache本质是个HashMap。// BaseExecutor.query —— 一级缓存命中的地方 public E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) { // 1. 用 statementId 参数 rowBounds 算一个 CacheKey // 2. 先查本地缓存 list resultHandler null ? (ListE) localCache.getObject(key) : null; if (list ! null) { // 3. 命中就直接返回缓存对象不发 SQL handleLocallyCachedOutputParameters(...); } else { list queryFromDatabase(...); // 4. 没命中才查库并写入 localCache } return list; }第 3 步就是元凶同一个事务、同一个SqlSession、同一个查询参数第二次selectBySku的CacheKey完全一致直接从localCache返回第一次查到的那个Stock对象。我们第 2 步改的是内存里那个对象的字段但数据库已经更新成新值——Java 这边看到的却始终是「第一次查出来的快照」。更糟的是localCache存的是同一个对象引用如果别处也改了它还会互相串。排查过程先怀疑事务隔离级别再怀疑缓存我们一开始以为是事务隔离级别问题把READ_COMMITTED调来调去没用。真正突破口是开了 MyBatis 的 SQL 日志发现第二次selectBySku根本没打印任何 SQL。顺着BaseExecutor.query打了断点看到localCache.getObject(key)直接返回了对象。确认后做了个最小复现在事务方法里连续selectBySku两次中间不 update第二次一定命中缓存。项目环境是 MyBatis 3.5.13、MyBatis-Spring 2.1.1、Spring Boot 2.7.18、MySQL 8.0.32、HikariCP 5.0.1。四种处理手段效果不同手段做法优点代价关一级缓存localCacheScopeSTATEMENT每次查询都查库彻底消除失去缓存收益QPS 高时压力上移flushCachetrue在 update 的 Mapper 上标Options(flushCache...)更新后清缓存要记得每个写操作都标拆事务查询和扣减分两个事务/方法各自独立 SqlSession事务边界要重新设计查完即弃第二次查询改用不同参数或重新select绕开相同 CacheKey治标不治本我们线上先用Options(flushCache Options.FlushCachePolicy.TRUE)标在写操作上止血长期方案是把「读-算-写」拆成两个事务边界清晰的方法读用独立SqlSession。Update(update stock set count #{count} where sku_id #{skuId}) Options(flushCache Options.FlushCachePolicy.TRUE) // 写完立刻清一级缓存 int updateById(Param(skuId) Long skuId, Param(count) int count);复盘数字这次超卖持续约 4 分钟涉及 37 个 sku超卖合计 412 单事后对账补单花了 2 天。加上flushCache后单 sku 扣减链路 P99 从 12ms 升到 18ms多了一次查库但库存数据恢复强一致这个代价我们接受。我的取舍判断我不建议无脑把localCacheScope全局设成STATEMENT。一级缓存在「一个方法里多次查同一只读数据」的场景是有意义的能挡掉不少重复 SQL。真正要防的是「先查、再改、再查」这种写读混合逻辑落在同一个SqlSession里——它把缓存变成了脏数据来源。我的习惯是凡是涉及「查出来再算再写」的库存、余额、状态机类操作要么显式flushCache要么把写操作和读操作放在不同的事务边界让localCache自然失效。比起追求那点缓存收益数据正确性优先级高得多。二级缓存会把坑再放大一圈如果stockMapper上开了CacheNamespace二级缓存情况更糟二级缓存跨SqlSession生效意味着就算你拆掉事务让每次查询用新SqlSession只要别的线程改了库存并提交二级缓存可能还返回旧值。我们的硬性规则是库存、余额、订单状态这类写多读少、要求强一致的表一律不开二级缓存Mapper 上标Options(flushCache TRUE, useCache false)。最终的「读-算-写」我们改成直接用 SQL 在数据库侧原子扣减根本不依赖先查后算Update(UPDATE stock SET count count - #{count} WHERE sku_id #{skuId} AND count #{count}) int deductCount(Param(skuId) Long skuId, Param(count) int count); // 1. 数据库侧原子扣减 Transactional(readOnly true, propagation Propagation.REQUIRES_NEW) public int currentCount(Long skuId) { return stockMapper.selectCount(skuId); // 2. 只读查询放独立事务自带新 SqlSession }第 1 行用UPDATE ... SET count count - #{count}在数据库行上加锁扣减返回受影响行数判断够不够扣彻底绕开了本地缓存的旧值问题。第 2 行把只读查询放到REQUIRES_NEW的独立事务里每次都是新SqlSession不会吃到上一个事务遗留的一级缓存。思考题你项目里有没有「同一个事务里 update 完立刻再 select 校验」的写法把 MyBatis SQL 日志打开跑一遍看看第二次查询有没有真的打到数据库。

相关新闻

Demo 一时爽,上线火葬场:Agent 评测到底怎么搞

Demo 一时爽,上线火葬场:Agent 评测到底怎么搞

你肯定见过这种场面:Demo 会上,Agent 行云流水,老板眼睛放光,当场拍板「下周上线」。然后呢?上线两周,效果还行;三周之后,幻觉率上来了,人工接管率上来了,业务…

2026/8/22 4:23:45 阅读更多 →
AI像素画编辑器部署指南:从环境配置到批量生成实战

AI像素画编辑器部署指南:从环境配置到批量生成实战

这次我们来看一个用 AI 做像素画编辑器的项目,它主打的是“童年回忆杀”,通过 AI 能力快速生成或转换出复古风格的像素画。对于想快速创作像素艺术、制作游戏素材或重温经典游戏美术风格的朋友来说,这是一个非常有趣且实用的工具。 这个项目…

2026/8/22 4:23:45 阅读更多 →
Universal-Updater 3DS 自制软件管理:快速装好商店、搜索与自动更新

Universal-Updater 3DS 自制软件管理:快速装好商店、搜索与自动更新

Universal-Updater 3DS 自制软件管理:快速装好商店、搜索与自动更新 【免费下载链接】Universal-Updater An easy to use app for installing and updating 3DS homebrew 项目地址: https://gitcode.com/gh_mirrors/un/Universal-Updater Universal-Updater …

2026/8/22 4:23:45 阅读更多 →

最新新闻

Go语言面试核心要点与实战技巧解析

Go语言面试核心要点与实战技巧解析

1. Go语言面试核心要点解析作为一门由Google开发的开源编程语言,Go凭借其简洁的语法、高效的并发模型和强大的标准库,近年来在云计算、微服务和分布式系统领域获得了广泛应用。对于准备Go语言面试的开发者而言,掌握以下核心知识点至关重要。1…

2026/8/22 5:53:19 阅读更多 →
SAP集成认证实战:X.509客户端证书从原理到工程化落地

SAP集成认证实战:X.509客户端证书从原理到工程化落地

1. 项目概述:从“能用”到“好用”的认证跨越在SAP这类企业核心系统的集成与自动化场景里,登录认证是个老生常谈却又常谈常新的问题。我们早已习惯了用户名密码,但在机器对机器(M2M)、系统间深度集成的场景下&#xff…

2026/8/22 5:53:19 阅读更多 →
嵌入式AI部署实战:从TensorFlow模型到边缘设备的完整优化指南

嵌入式AI部署实战:从TensorFlow模型到边缘设备的完整优化指南

上周帮一个做智能硬件的朋友排查问题,他花了两个月把YOLOv5模型训到了90%的mAP,兴冲冲地想部署到自己的嵌入式板子上做实时检测。结果第一步就卡住了——在PC上运行流畅的.pt模型,怎么都转换不到目标平台能用的格式。内存爆了、算子不支持、推…

2026/8/22 5:53:18 阅读更多 →
算法与数据结构系统学习指南:从基础到实践,构建算法思维

算法与数据结构系统学习指南:从基础到实践,构建算法思维

1. 先搞清楚这套课程到底解决什么实际问题算法学不会,数据结构搞不清,这几乎是每个程序员在某个阶段都会遇到的坎。很多人一上来就刷LeetCode,或者硬啃《算法导论》,结果就是越看越懵,题目稍微一变就无从下手。这套被广…

2026/8/22 5:53:18 阅读更多 →
无人机协同搜救建模实战:MILP路径优化与动态约束处理

无人机协同搜救建模实战:MILP路径优化与动态约束处理

1. 这不是“抄答案”,而是一次完整的建模实战复盘2023亚太杯数学建模竞赛C题——“无人机协同搜救路径优化与资源调度问题”,在当年开赛48小时内就登上高校数学建模社群热搜榜TOP3。它表面看是道典型的运筹优化题,但实际嵌套了多层现实约束&a…

2026/8/22 5:53:18 阅读更多 →
Linux内核开发实战:在Ubuntu 18.04上添加自定义系统调用

Linux内核开发实战:在Ubuntu 18.04上添加自定义系统调用

1. 项目概述与核心价值如果你是一名Linux开发者或系统爱好者,当你在用户空间写程序时,可能会好奇那些open、read、write之类的函数,最终是如何让硬件动起来的。这个“最终”的桥梁,就是系统调用。它像是用户程序和内核之间的一道严…

2026/8/22 5:52:18 阅读更多 →

日新闻

沉金PCB工艺实战指南:从设计到SMT焊接的可靠性保障

沉金PCB工艺实战指南:从设计到SMT焊接的可靠性保障

在电子硬件开发领域,PCB(印制电路板)的沉金工艺是提升产品可靠性和焊接质量的关键环节。对于需要高密度互连、长期稳定运行或高频信号传输的板卡,如“黍姐仿通行证”这类可能涉及身份识别、数据交互的硬件项目,选择正确…

2026/8/22 0:00:11 阅读更多 →
电气考研电路八月强化四步法:从知识体系到真题实战的闭环攻略

电气考研电路八月强化四步法:从知识体系到真题实战的闭环攻略

这次我们来看一个针对电气考研电路科目的学习规划项目。它不是软件工具,而是一套聚焦于8月份关键节点的备考策略。对于电气工程考研的同学来说,电路分析是专业课的重中之重,也是拉开分差的关键。进入8月,复习进入强化阶段&#xf…

2026/8/22 0:00:11 阅读更多 →
消除AI代码的“AI味”:Claude Code设计优化技能配置与实战指南

消除AI代码的“AI味”:Claude Code设计优化技能配置与实战指南

大家好,我是专注于前端开发与AI工具实践的技术博主。在日常使用 Claude Code 等AI编程助手时,你是否也遇到过这样的困扰:生成的代码功能上没问题,但代码风格、组件设计、交互逻辑总透着一股“AI味”——布局单调、样式简陋、交互生…

2026/8/22 0:00:11 阅读更多 →

周新闻

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

如果你是一名开发者,最近可能已经感受到了AI大模型正在从“玩具”变成“生产力工具”的强烈信号。从代码补全到智能Agent,从本地部署到云端API,我们正处在一个技术栈快速重构的节点。然而,面对层出不穷的模型、框架和工具&#xf…

2026/8/21 3:21:33 阅读更多 →
工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/21 0:02:09 阅读更多 →
【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和…

2026/8/21 6:07:56 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/21 16:42:28 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/20 21:46:49 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/22 3:22:48 阅读更多 →