【多线程】两台机器同时抢一个字典:一次并发 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% 消失,但系统从"必崩"变成了"稳得住"。

相关推荐
m0_715674431 小时前
政务泛监测体系能力排行:AI 重塑监测边界,治理的颗粒度革命
大数据·网络·人工智能·安全·网络安全·政务
w_zero_one1 小时前
递归算法(一)
java·开发语言·算法
SL_staff2 小时前
私有化低代码不是减配版开发:JVS双模式建模如何重建IT交付信任
java·低代码·spring cloud
AI 编程助手GPT2 小时前
curl 返回成功就能收文件?用本地 HTTP 验证错误页与超时
网络·网络协议·http
墨着染霜华2 小时前
Redocly 生成 OpenAPI 接口文档踩坑实录|Node 版本兼容 + 接口地址错误问题解决
java
文丑颜不良啊2 小时前
LeetCode1881~1990题目解析
java·leetcode
十碗饭吃不饱3 小时前
后端运维:网络联通、进程查询、Java服务启动脚本
java·运维·网络
T_Wizard3 小时前
后端转 AI 应用开发,我先把这 12 个名词理清了:一张图看懂架构、RAG、Tool Calling 与 MCP
java·ai编程
杨航 AI3 小时前
JEV的用法
java·前端·数据库
灿飞3 小时前
Kotlin与Java虚拟机七:空安全:把十亿美元的错误消灭在类型系统里
java·kotlin