【多线程】两台机器同时抢一个字典:一次并发 Bug 的排查

文章目录


一、事故现场

程序弹出异常:

服务器无法处理请求。 ---> 集合已修改;可能无法执行枚举操作。

调用栈直指:

复制代码
Dictionary.KeyCollection.Enumerator.MoveNext()
WebService.Service.setData(...)

翻译:遍历字典时,字典被人改了。

二、背景

  • 两台上位机,程序一模一样,1 秒一次调同一个 WebService
  • 服务端维护一个共享字典 dicData
  • 还有另一路程序也在往这个字典写数据
  • 两台机器都要读同一个 key,读完后原本还会"清空"它

三、为什么必然出错

C# 的 Dictionary 内部有个 _version,任何改结构 的操作(Add / Remove / Clear)都会让它 +1。

foreach 遍历时,枚举器每次 MoveNext() 都检查版本号是否一致,不一致就抛 InvalidOperationException: 集合已修改。

所以只要:

  • A 请求正在遍历 dicData
  • B 请求同时 Add / Remove
  • 时序重叠

就炸。两机 + 1 秒定时 + 另一路写,长期跑必然撞上。

四、最反直觉的一点

异常抛在"正在遍历"的那一方,但根源往往是"另一方在写"。

堆栈里看起来是你炸了,实际是别人改了版本号。所以光改客户端没用------你只是受害者。

五、服务端改不了,客户端怎么办

三条路:

1. 加 try-catch(必须)

原来 Tick 没捕获异常,服务端一抛就弹框崩溃。加上后:异常被兜住,下次 Tick 再试。

2. 降低并发概率

  • 只在读到有效数据时才清空,减少一半调用
  • 两机错开 Tick 相位,降低同时调用的概率

3. 干脆不清空(最终选择)

原逻辑是"读 → 立刻清空",目的是防两台重复执行。

但换个思路:本地记录上次状态,变了才处理,重复读到同一条也不会重复执行。

于是:

  • 不再写服务端 → 不当冲突加害者
  • 靠本地状态比对去重 → 没变化不动作
  • 两台各维护本地状态 → 互不干扰

六、代价

不能 100% 去重。两台机器同一时刻首次读到同一条新数据时,可能各处理一次。

服务端改不了的前提下,只能把崩溃和重复的概率降到可接受范围。

七、几个值得记住的点

  1. foreach 遍历时,集合不能改结构
  2. 改 value 不触发版本变化,Add / Remove / Clear 才触发
  3. 报错方往往不是凶手,并发写才是
  4. 共享字典 + 多客户端 = 必须有并发保护:加锁、ConcurrentDictionary、或避开遍历用 ContainsKey
  5. 改不了服务端时,客户端要会兜住 + 去重 + 降频
  6. 有时候"不做某件事"(不清空)比做更聪明

八、一句话

两台机器抢一个共享字典,一个在遍历,一个在改结构,枚举器直接翻脸。

服务端改不了,就在客户端兜住、降频、错相、本地去重,最后干脆不清空。

问题没 100% 消失,但系统从"必崩"变成了"稳得住"。

相关推荐
牛战士coding1 小时前
LLM 选错工具怎么办?提示词之外还需要确定性规则兜底
java·人工智能
Multipath7121 小时前
以多链路聚合为通信底座,让低空飞行器“连得上、传得快、飞得稳、管得住”
网络·5g·安全·智能路由器·实时音视频
今天的砖头有点烫手啊1 小时前
Java 序列化到底怎么选?从 Serializable 到 Protobuf 的演进与选型
java·json
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 登录发送验证码的测试验证与经验复盘
java·开发语言·交友
夕除1 小时前
redis--主从还是集群,内存
java·spring·mybatis
百数平台1 小时前
低代码安全技术实践:表单 / 流程 / 报表三级权限体系设计解析
java·大数据·数据库
白露与泡影1 小时前
Spring Boot 实现验证码:Hutool-captcha、AJ-Captcha、EasyCaptcha 到底怎么选?
java·spring boot·后端
马优晨1 小时前
Java中的dto是什么
java·开发语言·java中的dto是什么·java的dto是什么·dto在java中是什么
小蒜学长1 小时前
基于Springboot+Vue的高校教师教学业绩综合管理系统的设计与开发(代码+数据库+LW)
java·数据库·spring boot·后端·教学业绩管理·三级权限