资讯动态

从零搭建灌装监控系统(十二):配置系统,原子写入与容错

发布时间:2026/10/5 2:25:57 来源:尧图企业网站定制
配置系统原子写入与容错这是「从零搭建灌装监控系统」系列第12篇。配置文件看起来只是几行 JSON真正运行在现场以后它却承担了设备地址、串口参数和操作偏好。这篇不讨论怎么反序列化对象而是解决保存过程中最容易被忽略的三个问题原子写入、并发保存和损坏恢复。断电以后设备参数变成半个 JSON现场调试时操作员改了串口号点击保存程序马上退出。第二次启动时系统提示配置格式错误设备也连不上了。打开文件一看内容停在一个对象的一半少了结尾的大括号最后一个字段也没有写完。原因很现实程序写文件不是瞬间完成的写到一半突然断电、进程崩溃或 U 盘被拔出都可能留下半成品。如果直接使用File.WriteAllText(path, json)等于让正式文件承担写入过程中的风险。更稳妥的办法是先写临时文件确认写入完成后再用替换动作把临时文件变成正式文件。原子写入的核心动作原子写入不是说磁盘真的只用了一个硬件动作而是让程序的观察者只看到两种状态旧文件完整或者新文件完整不看到中间过程。privatestaticasyncTaskWriteAtomicallyAsync(stringpath,stringcontent,CancellationTokentoken){vardirectoryPath.GetDirectoryName(path)??thrownewArgumentException(配置路径无效,nameof(path));Directory.CreateDirectory(directory);vartempPathpath.tmp;awaitFile.WriteAllTextAsync(path:tempPath,contents:content,encoding:newUTF8Encoding(true),cancellationToken:token);File.Move(tempPath,path,overwrite:true);}这段代码已经比直接覆盖正式文件安全很多但还不够完整如果替换前进程崩溃.tmp文件会留下如果目标文件被其他程序占用替换会失败如果保存请求同时到达还可能发生互相覆盖。所以原子写入是一个流程不只是File.Move这一行。配置保存必须串行化设置页面可能有“保存”按钮后台服务也可能在连接模式切换时保存配置。如果两个任务同时序列化并写文件最后写入的内容不一定是用户最后看到的内容。用SemaphoreSlim给保存操作加一把异步锁publicstaticclassConfigService{privatestaticreadonlySemaphoreSlimSaveGatenew(1,1);publicstaticasyncTaskSaveAsync(DeviceSettingssettings,stringpath,CancellationTokentokendefault){awaitSaveGate.WaitAsync(token);try{varjsonJsonSerializer.Serialize(settings,newJsonSerializerOptions{WriteIndentedtrue});awaitSaveAtomicallyAsync(path,json,token);}finally{SaveGate.Release();}}}lock不能包住await因为它是同步锁SemaphoreSlim.WaitAsync适合这种异步文件操作。锁只保护保存流程不要把设备通信、弹窗等待和长时间业务操作都放进去。如果希望最后一次修改覆盖前面的保存请求可以进一步做防抖用户停止编辑 300ms 后再保存。但防抖不是并发锁的替代品两者解决的是不同问题。保存前先验证对象原子写入只能保证“写进去的内容完整”不能保证内容正确。一个完整的 JSON 也可能把端口写成负数把 IP 写成空字符串。privatestaticvoidValidate(DeviceSettingssettings){if(string.IsNullOrWhiteSpace(settings.PortName))thrownewInvalidOperationException(串口号不能为空);if(settings.BaudRate0)thrownewInvalidOperationException(波特率必须大于零);if(settings.TcpPortis1or65535)thrownewInvalidOperationException(TCP端口超出范围);}校验应该发生在序列化和写文件之前。保存失败时原正式文件应该保持不变UI 显示校验错误日志记录失败原因。不要先把错误对象写入文件再期待启动时帮你修复。启动读取正式文件、临时文件和备份文件读取策略要明确优先级正式文件存在且能反序列化使用正式文件正式文件损坏尝试.bak备份正式文件不存在尝试默认配置所有来源都失败创建内存中的安全默认值并记录错误。publicstaticasyncTaskDeviceSettingsLoadAsync(stringpath){try{if(File.Exists(path))returnawaitReadAndValidateAsync(path);}catch(JsonExceptionex){LogService.Error(配置文件格式错误,ex);}catch(IOExceptionex){LogService.Error(读取配置文件失败,ex);}varbackupPathpath.bak;if(File.Exists(backupPath)){try{varbackupawaitReadAndValidateAsync(backupPath);LogService.Warn(已从配置备份恢复);returnbackup;}catch(Exceptionex){LogService.Error(配置备份也无法读取,ex);}}LogService.Warn(未找到可用配置使用默认值);returnDeviceSettings.CreateDefault();}默认值必须是安全值。比如自动连接可以默认关闭通信地址使用示例地址不能让软件第一次启动就向未知设备发命令。是否是否否是读取配置正式文件可解析?使用正式文件.bak 备份可解析?从备份恢复并记录警告使用安全默认值业务校验校验通过?中止保存保留原文件写 tmp 后原子替换备份文件什么时候生成最简单的策略是在替换前把旧文件复制为.bakif(File.Exists(path))File.Copy(path,path.bak,overwrite:true);File.Move(tempPath,path,overwrite:true);更严格的流程还要考虑复制失败。如果备份失败是否继续替换答案要由产品场景决定。对于设备关键参数我宁愿中止保存也不愿意在没有备份的情况下覆盖唯一副本。实际项目可以保留一个带时间的备份目录但不要无限增长。配置备份不是日志保留最近 3 到 5 份通常已经足够清理动作也要避免删除当前正式文件。临时文件的清理程序上次异常退出后目录里可能留下settings.json.tmp。下一次启动不能盲目把它当正式配置因为它可能只写了一半。可以做两件事启动时尝试解析临时文件解析成功也只把它作为恢复候选不直接覆盖正式文件保存成功后删除临时文件失败时记录路径便于诊断。privatestaticvoidTryDeleteTemp(stringtempPath){try{if(File.Exists(tempPath))File.Delete(tempPath);}catch(IOExceptionex){LogService.Warn($临时配置文件清理失败{ex.Message});}}清理失败不应该让主程序启动失败但必须留下日志。磁盘权限和杀毒软件锁文件的问题往往只有日志能告诉你发生过什么。配置版本和向后兼容配置文件会跟着软件版本变化。新增字段通常可以使用默认值但字段重命名和结构调整就需要版本号{configVersion:2,connection:{mode:Serial,portName:COM1}}读取后先判断版本再做迁移if(model.ConfigVersionCurrentVersion){modelConfigMigrator.Upgrade(model);awaitSaveAsync(model,path);}不要在反序列化异常时直接把用户配置覆盖成默认值。先备份原文件记录迁移或恢复原因再让用户知道系统采取了什么措施。配置容错的目标是恢复运行不是悄悄丢掉现场设置。踩坑记录临时文件和正式文件放在不同磁盘跨磁盘移动不再是简单替换可能退化成复制加删除中间风险更大。临时文件必须和正式文件位于同一目录。用lock包住异步保存编译可能通过但设计会让异步流程难以维护。文件保存使用SemaphoreSlim并确保所有退出路径都释放。JSON 能解析就认为配置可用格式正确不代表值合法。端口、地址、范围和必填字段都必须做业务校验。恢复默认值却不提示程序能启动不代表问题解决了。如果配置损坏后默默使用默认值设备可能连到错误目标。至少要写日志并在 UI 给出明确提示。本篇小结风险对策断电留下半文件临时文件写完后替换多个保存同时发生SemaphoreSlim串行化写入错误值保存前业务校验正式配置损坏.bak备份恢复新旧版本不兼容配置版本和迁移临时文件残留启动诊断成功后清理数据、配置都有了稳定的落点接下来要面对的是现场更敏感的部分报警。报警不是一条红色文字它有触发、持续、恢复和确认四个不同阶段。下期预告第13篇报警系统设计生命周期与通知下一篇把报警从一个布尔值拆成完整生命周期并实现数据库持久化、重复抑制和 UI 事件通知。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑