← 全部文章

本地文件缓存的数据一致性解决

记录一次本地缓存的踩坑史,从GET探测到版本号

更新于 2026.08.07

本文目录 8 节
本地文件缓存的数据一致性解决封面

背景

最近在开发OJ系统。OJ系统是一整个微服务的设计,承担判题的判题核心被拿出来独立作为一个服务。用户提交题目后把判题任务丢到MQ,判题服务从MQ拉取判题任务并更新数据库,这是我的解耦方案。

在这个过程中,代码沙箱又被我单独拿了出来,作为一个完全外部的服务,仅提供代码执行服务的API调用,而不涉及任何OJ的业务。

在过去一段时间里,我围绕输入数据缓存这件事,走了不少弯路。代码沙箱,会经常接收外部的输入数据作为stdin给执行的代码作为标准输入。在OJ场景中,一道题目的输入数据可能大至几MB,作为text存储在DB中显然不太合适。于是我决定使用文件存储题目的评测数据。

涉及到文件服务,就不免得使用到阿里云OSS\MinIO这一类对象存储服务。

两个问题是显然的。

  • 不能每次都从OSS下载评测数据,因此要建立本地缓存机制
  • 题目评测数据有可能更新,要维护OSS和本地缓存的数据一致性

最开始的方案,代码沙箱暴露一个更新缓存接口,我自认为是合理的。但是一部署上线,问题就暴露出来了。沙箱可能多服务器多实例部署,通过挨个请求沙箱接口清除缓存的方式实在难以维护。

踩坑史

这样,我似乎合理地认为探测评测数据的职责应该交给使用者,也就是代码沙箱来做。通过搜索,我发现对象存储服务会提供一个ETag,作为一个文件的唯一标识(这个提法可能欠妥)。可以通过比对本地和远端两个ETag来更新数据。

代码沙箱接收可GET的URL

最初的想法很直观。沙箱拿到一个预签名 URL,先发 HEAD,看 ETag 是否变化,变了再 GET 下载 ZIP。看起来没问题,但实际上,在预签名URL时要指定请求的HTTP方法。如果我们签名GET方法,是没有办法进行HEAD探测的。

由于着急跑通整个流程,我先降级方案,直接通过GET方法探测ETag,然后比对数据。这么做以后系统很快跑起来了,开始做其他功能的完善

这样其实有一个很大的问题。通过GET方法去请求OSS,实际上也是下载了一遍,只是使用文件的服务把后面的内容给丢弃了,这会造成大量的资源浪费。

修改接口,代码沙箱接收两个URL

痛定思痛,决定对代码沙箱开刀。把相关接口改成接收两个URL,同时接收HEAD和GET方法的URL。于是一顿改代码,阿里云报了以下错误:

java.lang.IllegalArgumentException: Only GET or PUT is supported! at com.aliyun.oss.model.GeneratePresignedUrlRequest.setMethod(GeneratePresignedUrlRequest.java:121) ~[aliyun-sdk-oss-3.17.4.jar:na] at ...

是的,Aliyun不完全兼容Amazon S3 API,不支持HEAD方法预签名。尽管MinIO支持,但受制于没有足够的服务器资源,遂决定完全毙掉该方案。

最终方案

绕了一圈之后,我终于意识到一个问题。

我一直在思考“怎么探测 OSS 上的数据有没有变”,却忽略了一个更关键的问题:

为什么要让沙箱去探测?

沙箱只是一个纯执行服务。它不知道题目,也不知道评测数据的业务语义。它只是接收一段代码、一组输入,然后执行。

真正知道“这份数据什么时候变了”的,是判题服务。

当我把这个问题想清楚之后,整个设计一下子变简单了。

把版本交还给判题服务

最终的思路其实非常朴素:

  • 判题服务在下发任务时,携带一个 inputDataVersion
  • 沙箱只做字符串精确比较
  • 相同就用本地缓存
  • 不同就重新下载

不再做 HEAD、不再解析 ETag、不再关心 OSS 的实现差异。

沙箱从“主动探测远端数据是否变化”,变成“被动接收一个版本号”。

职责一下子清晰了。

新的执行流程

现在的流程是这样的。

  1. 判题服务构造批量执行请求:
    • inputDataUrl
    • inputDataVersion
  2. 沙箱收到请求后:
    • 根据 URL 提取 objectKey
    • 定位本地缓存目录
    • 读取 _meta.properties 中的 version
    • 做字符串比较

情况一:版本一致

直接从本地读取数据,不发任何 HTTP 请求,没有 RTT,没有带宽消耗,纯本地 IO。

情况二:版本不一致或没有缓存

发起一次 GET、下载 ZIP、解压、写入 _meta.properties、记录 version,流程结束。

整个过程没有 HEAD,也没有双 URL 的复杂路径。

Over

起初,我执着于让代码沙箱变得“聪明”,试图让它自主感知数据的变化。但实际上,沙箱作为一个纯粹的执行单元,它最核心的价值是“快”和“稳”,而不是“懂业务”。当我们把定义的权力交还给判题服务,把复杂的网络探测简化为一次字符串比对,整个系统不仅性能提升了,逻辑也变得异常稳固。

评论