首页/新闻资讯/正文详情

用了 Caffeine,消息为什么还是被处理了两次?

发布时间:2026/9/23 7:08:30 来源:云帆数科 栏目:资讯中心
用了 Caffeine,消息为什么还是被处理了两次?
最近的工作都是在公司的旧项目框架上去开发新的直播小游戏过程虽然坎坷别人的旧项目你们都懂但总算是顺利上线。然而某一天运营反馈某玩家发了一条参与指令结果扣了两次钱效果也叠加了。这里先普及下直播小游戏的玩法可能很多人未玩过就是玩家在真播间像聊天一样发指定格式的消息指令直播的游戏就会有不同的效果。例如本次玩家发的指令是参与C300表示使用300金币参与C玩法。当平播这边在收到这些弹幕指令后会转到游戏服务器经过游戏服处理后再发给客户端体现。这里也说明一下很多直播平台或支付中心在消息推送时都有个特点同一条消息可能重发。原因很多失败重发、推送地址检查等都会导致同一个消息的IDmessageId被推送多次。针对这种情况一般后端都会对消息做去重处理。从运营反馈的情况看大概率问题也是出在这个去重上。排查开始先简单看一眼代码去重逻辑确实写了用的是 Caffeine。当时第一反应是应该没问题吧Caffeine 挺成熟的。再检查相关日志。同一个 messageId两次进入推送流程16:49:44 INFO pushChat - {comment:参与C300,msgId:7673064607611507746,createTime:1786524582560} 16:49:46 INFO pushChat - {comment:参与C300,msgId:7673064607611507746,createTime:1786524583645}两条日志间隔 1.6 秒。如果是正常的去重命中应该会打一条 WARN[参与C300] msgchat-7673064607611507746 has process, ignored但翻遍日志这条 WARN 根本没出现。也就是说第二条消息进来的时候去重检查返回的是没处理过。奇怪的是同一时间点其他玩家的消息去重是正常的16:49:44 WARN [参与A1200] msgchat-7673064610798359587 has process, ignored 16:49:46 WARN [参与A1200] msgchat-7673064610798359587 has process, ignored同样是重复消息为什么有的能过滤、有的过滤不了再重新检查代码来事了Caffeine 是用了但用法有问题相关的去重处理是这样的privatefinalCacheString,StringMSG_CACHECaffeine.newBuilder().maximumSize(2048).expireAfterAccess(30,TimeUnit.MINUTES).build();privatebooleanisDuplicate(Stringlabel,StringmessageId){try{StringexistedMSG_CACHE.getIfPresent(messageId);if(existed!null){log.warn([{}] msg{} has process, ignored,label,messageId);returntrue;}}catch(Exceptione){}returnfalse;}调用处StringcacheKeychat-chatMsg.getMessageId();if(isDuplicate(chatMsg.getComment(),cacheKey)){continue;}MSG_CACHE.put(cacheKey,1);// 写入标记// ... 后续处理发现问题没有查和写是分开两步getIfPresent是查询put是写入而且isDuplicate后就已经马上put了中间应该不会有问题吧。但调用处实际是一个处理平台推送消息的Controller内这意味着是并发环境下的调用。所以真实情况下的执行时序会是时刻 T1请求A 进入 isDuplicate → 查缓存 MISS → 返回 false 时刻 T2请求B 进入 isDuplicate → 查缓存 MISS → 返回 false ← 缓存里还没写入 时刻 T3请求A 执行 MSG_CACHE.put → 写入缓存 时刻 T4请求B 执行 MSG_CACHE.put → 写入缓存重复写入但已经晚了请求 A 和请求 B 都认为自己没处理过于是都走完了整个流程。参与扣了两次钱。解决方法其实挺简单的对于我们的这个情况Caffeine 已经提供了对应的处理方案使用asMap().putIfAbsent。通过asMap()视图返回的是一个 ConcurrentMap。putIfAbsent是原子操作——返回 null 表示之前没有首次写入成功返回非 null 表示已存在重复跳过。具体实现privatebooleanisDuplicate(Stringlabel,StringmessageId){try{StringprevMSG_CACHE.asMap().putIfAbsent(messageId,1);if(prev!null){log.warn([{}] msg{} has process, ignored,label,messageId);returntrue;}}catch(Exceptione){log.error(isDuplicate error msg{},messageId,e);}returnfalse;}调用处去掉多余的 putStringcacheKeychat-chatMsg.getMessageId();if(isDuplicate(chatMsg.getComment(),cacheKey)){continue;}// 不需要再 putisDuplicate 里已经写入了使用putIfAbsent代替原来getIfPresent put的原因原子操作一步完成查和写没有窗口返回值即判定一步到位性能更好asMap()不是数据拷贝就是内部 Cache 的 ConcurrentMap 视图putIfAbsent内部基于 key hash 分区加锁不同 key 完全无竞争同 key 串行但只覆盖 CAS 级别的临界区比两次操作查写还快。问题是解决了但我更想讲的是对于成熟组件如本次的 Caffeine很多人觉得我用了 Caffeine它内部是线程安全那肯定没问题的。没错Caffeine 单个操作是线程安全的。getIfPresent是线程安全的put也是线程安全的。但你的业务逻辑是两个操作组合组合起来就不安全了。注意我说的这句组件的线程安全不等于你业务逻辑的线程安全。组件自身是线程安全的但是保存这个组件的空间本身并不一定线程安全。这种代码 review 的时候很容易放过逻辑看起来是对的先查、有就跳过、没有就处理然后写入但如果把眼光拉出来放在上一层去看就能发现问题。最后想强调的点第一公用组件不等于自动正确。以这次的 Caffeine 为例“用了 Caffeine” 和 “用对了 Caffeine” 是两回事。这次问题最隐蔽的地方在于代码看起来是对的逻辑是完整的甚至 review 的时候都觉得没问题但直到日志打脸才意识到两步操作之间有窗口执行存在并发。第二多实例部署下本地缓存的天花板。putIfAbsent只能解决了单进程下的并发。但如果部署了多个实例本地缓存不共享同一个 messageId 打到不同实例上还是会重复处理。这种场景要么上 Redis SETNX要么在接入层做一致性 hash 让同 messageId 落到同一实例。最后说一句公用组件不是万能往往只能解决一小部分问题。当真正的问题产生时很多时候真正的原因就藏在你觉得应该没问题的地方。本文首发于【掘金】作者【码路漫漫】

相关推荐

电机抖动四维诊断法:电磁-控制-硬件-机械根因分析
电机抖动四维诊断法:电磁-控制-硬件-机械根因分析

1. 为什么电机一转就“哆嗦”?这不是小毛病,是系统在报警“电机驱动系统为什么会抖?”——这问题我每天至少被问三遍,不是来自刚毕业的实习生,而是坐在总工办公室里、盯着产线良率报表发愁的工程师。上周去一家做精密激… · 2026/9/23 7:08:30

ehviewer下载避坑指南:从GitHub Release获取官方APK的正确姿势
ehviewer下载避坑指南:从GitHub Release获取官方APK的正确姿势

1. 先说清楚:ehviewer 不是网站,所以“官网下载”这个说法一开始就不成立很多人搜“ehviewer官方下载”的时候,会本能地把它当成一个可以输入域名访问的网站,觉得只要找到官网,就能像下载普通软件一样获取安装包。但这… · 2026/9/23 7:08:30

标准IO与系统IO:从缓冲机制到性能优化的全面解析
标准IO与系统IO:从缓冲机制到性能优化的全面解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/23 7:08:30

配电网韧性提升:移动储能预布局与动态调度建模及Matlab实现
配电网韧性提升:移动储能预布局与动态调度建模及Matlab实现

1. 一文看懂“预布局动态调度”到底在解决什么问题如果你这两年一直在关注配电网方向的研究,大概率会发现一个高频词:配电网韧性。这个词跟传统的“可靠性”不完全是一回事。可靠性强调的是平均意义上的停电频率和时长,而韧性针对的是小概率、… · 2026/9/23 7:55:29

Codex Team Runtime 07 | 我如何使用一个 AI 开发团队:六篇文章之后的实践与反思
Codex Team Runtime 07 | 我如何使用一个 AI 开发团队:六篇文章之后的实践与反思

我如何使用一个 AI 开发团队:六篇文章之后的实践与反思 过去六篇文章,我分别写了 AI 团队的角色、模型分配、MCP 召回、并行协作、交付验收和指标面板。单独看,每篇都在解释一种机制;放在一起,却容易漏掉最重要的问题&… · 2026/9/23 7:55:29

Allegro快捷键高效配置:ENV文件与Skill脚本实战指南
Allegro快捷键高效配置:ENV文件与Skill脚本实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/23 7:55:29

LabVIEW实现高效TCP多客户端通信的技术解析
LabVIEW实现高效TCP多客户端通信的技术解析

1. 项目背景与核心价值在工业自动化、测试测量和物联网领域,设备间的实时数据交互一直是刚需。传统方案往往采用串口通信或专用总线协议,但随着网络基础设施的普及和分布式系统的发展,TCP/IP协议栈因其通用性和可靠性成为首选。LabVIEW作为图… · 2026/9/23 7:55:22

影视后期制作工程师怎么考证?从报名学习到考试拿证,报考全攻略
影视后期制作工程师怎么考证?从报名学习到考试拿证,报考全攻略

影视后期制作工程师是计算机软件领域与影视传媒交叉的重要技术岗位。随着短视频、网络电影、广告、纪录片等内容产业持续发展,影视后期制作人才需求保持稳定增长。如果你正在考虑考取影视后期制作工程师证书,本文将从报名学习到考试拿证,做一… · 2026/9/23 7:55:22

零基础90天Python工程化学习路线图:从文件操作到可部署项目
零基础90天Python工程化学习路线图:从文件操作到可部署项目

1. 这不是又一本“从入门到放弃”的Python书——它是一份可执行的工程化学习路线图你点开这个标题,大概率正站在两个路口之间:一边是铺天盖ed的“零基础Python教程”,点进去全是print("Hello World")、变量类型、if-else三板斧&… · 2026/9/23 7:55:22

3招搞定手机怎么下载微信面试难题实战项目解析
3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03

你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型

你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29

Win7无线热点配置工具源码解析:解决API失效的3个实战技巧
Win7无线热点配置工具源码解析:解决API失效的3个实战技巧

Win7无线热点配置工具源码解析:解决API失效的3个实战技巧 Win7无线热点配置工具在Win10/11上跑不动?不是你的问题,是版本升级后 API 全变了。很多老项目里的 netsh wlan… · 2026/9/23 0:00:36

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码