缓存和更新,这两个模块让我多写了两周代码

📅 2026/7/13 20:09:10 👁️ 阅读次数 📝 编程学习
缓存和更新,这两个模块让我多写了两周代码

搞 SchoolTools 的时候,我低估了"缓存"和"更新"这两个模块的复杂度。原本以为就是套个 hash map、定时检查下服务器版本,结果实际开发中踩了一堆坑,最后多写了整整两周代码才稳定下来。今天记录下 SQCache 和 SQUIUpdate 这两个模块的设计思路,如果你也在做类似的东西,希望能少走点弯路。

缓存:不止是个 Hash Map

我一开始真的以为缓存就是std::unordered_map<std::string, std::string>,存个键值对就完事了。但 SchoolTools 的缓存面临几个具体问题:

多线程读写:SchoolTools 的 API 层是异步回调驱动的,多个线程同时读写缓存的情况很常见。简单加锁?可以,但性能会炸。而且我们用了多线程读写锁,写锁和读锁的粒度必须精确控制,否则死锁问题排查起来头大。

持久化需求:缓存不能只在内存里。用户关掉软件再打开,缓存不应该全部失效。我们选择了 SQLite 做持久化存储,但 SQLite 和内存缓存的同步策略又是个问题——全量同步太慢,增量同步又复杂。

过期策略:哪些缓存需要 TTL?哪些需要主动失效?比如用户配置可以永久缓存,但某些 API 返回的数据必须 5 分钟内过期。我们给 SQCache 加了ttl参数,支持每个 key 独立设置过期时间。

更新:自动更新比想象中难搞

SQUIUpdate 是自动更新模块,负责检测新版本、下载、校验、安装。听起来简单,实际实现时各种 edge case:

Windows 上文件锁定:Windows 可执行文件在运行时被系统锁定,无法直接替换。我们需要先启动一个独立的 updater 进程,主进程退出后,updater 替换文件,再重启主进程。这个流程在 macOS 和 Linux 上完全不需要,但 Windows 上必须这么搞。

断点续传:用户下载到一半断网了,下次不应该从头下载。我们支持了 HTTP Range 请求,把下载进度和临时文件存在缓存目录里。但断点续传和文件校验的组合又引入了新问题:如果服务器端文件被替换了(比如热修复),断点续传的文件可能损坏,需要校验失败后重新下载。

版本回退:如果更新后用户发现软件无法启动,怎么办?我们在更新前备份了旧版本文件,启动时如果检测到新版本连续崩溃 3 次,自动回退到备份版本。这个逻辑很保守,但真的救过命。

跨平台一致性

SchoolTools 支持 Windows、macOS 和 Linux,三个平台的缓存路径和更新机制完全不同:

  • Windows%APPDATA%\SchoolTools,更新需要 updater 进程
  • macOS~/Library/Application Support/SchoolTools,更新直接替换.app
  • Linux~/.local/share/SchoolTools,更新替换可执行文件

SQCache 的存储路径统一用QStandardPaths管理,SQUIUpdate 的平台差异用#ifdef封装。但测试覆盖是个大问题,每次修改更新逻辑,三个平台都得手动测一遍,很痛苦。

写在最后

说实话,缓存和更新这两个模块的代码量,加起来差不多占了整个项目业务逻辑代码的 30%。但它们不是"核心功能",很难在产品介绍里展示。用户只会觉得"软件启动很快"“更新很顺滑”,背后就是这些基础设施的功劳。

如果你也在做类似的东西,我的建议是:早点设计,多写测试。缓存和更新的问题往往是延迟暴露的,等用户抱怨"怎么又需要重新登录"“更新后打不开了”,再修复就晚了。

有问题欢迎评论区交流。