1. 如果删掉 Redis,这个页面还能打开吗
介绍标注平台的技术栈时,Redis 很容易变成一句话:“缓存高频数据,提高性能。”
但我更想回答一个具体问题:项目看板上的待审核数量,如果不从 Redis 读取,还能从哪里得到?
假设每条任务只对应一张图片,MySQL 已经保存了任务和状态,那么可以直接查询:
SELECT COUNT(*)
FROM annotation_task
WHERE project_id = #{projectId}
AND status = 'REVIEWING';也就是说,这个数字可以重新算出来。Redis 保存的是查询结果的副本,任务本身仍然在 MySQL。
这使缓存的用途变得清楚:当许多人反复查看同一个数字时,减少重复计算。是否值得加缓存,要看查询代价和读取频率,不能只因为项目技术栈里还缺一个组件。
2. 看板数字和审核状态,可以有不同要求
假设页面显示还有 12 条待审核任务。审核员刚处理完一条,看板短时间仍显示 12,用户可以接受刷新后再更新。
但他再次点击这条任务的审核按钮时,后端必须知道它已经被处理。这里不能因为缓存里还是待审核,就放行第二次操作。
| 数据 | 使用方式 |
|---|---|
| 项目任务数量、待审核数量 | 可重建的看板缓存 |
| 某条任务当前能否审核 | 以数据库中的业务条件判断 |
| 标注结果、审核意见、模型版本关系 | 持久保存业务记录 |
| 训练状态摘要 | 可以缓存展示,最终结果另有持久记录 |
所以,我会把“页面展示允许稍晚”和“业务操作必须满足条件”分开。一个数字允许短暂滞后,不等于整个业务都允许使用旧状态。
这里讨论的是看板缓存。如果 Redis 同时承担登录会话或限流,停掉它对整个系统的影响还要分别分析,不能从看板可回源推导出所有功能都不受影响。
3. 先找缓存,没有再算
AWS 的缓存模式说明介绍了按需加载缓存的方式。放到这个页面上,就是一条很短的读取路径:
检查项目访问权限
↓
读取 Redis
├─ 命中 → 返回看板
└─ 未命中或缓存不可用
↓
查询 MySQL
↓
尝试写回缓存 → 返回看板下面用伪代码说明先后关系,省略序列化和客户端配置:
public Dashboard loadDashboard(long projectId) {
permissions.requireProjectDashboard(currentUser.id(), projectId);
String key = "dashboard:v1:project:" + projectId;
Optional<Dashboard> cached = cache.getBestEffort(key);
if (cached.isPresent()) {
return cached.get();
}
Dashboard result = dashboardMapper.aggregate(projectId);
cache.putBestEffort(key, result, Duration.ofSeconds(30));
return result;
}这里的 BestEffort 不是现成框架 API,而是表示一个约定:缓存连接失败或超时,就记录问题并按未命中处理;写缓存失败,不抹掉已经从数据库得到的结果。
它不应该把权限异常、数据库失败或者程序错误统统吞掉。数据库查询失败时,不能用一个全是零的看板冒充正常结果。
30 秒只是说明过期配置的例子,需要按业务能接受的刷新延迟确定。这个值不能说明页面已经获得了多少性能提升。
4. 缓存键也有权限范围
如果缓存键只有 dashboard,项目 A 的结果可能被项目 B 读到。至少应当把项目编号放进去。
不过,加入项目编号仍然有前提:这个接口对有权查看项目看板的人,返回的是同一份项目级统计。
如果标注员只能看自己的任务,管理员能看全部任务,两者不能共享同一份结果。可以分成“项目看板”和“我的任务”两个接口,让个人统计的键包含用户编号及相关筛选条件。
项目级:dashboard:v1:project:42
个人级:my-tasks:v1:project:42:user:8无论是否命中,仍然先做权限检查。缓存里有数据,不代表发起请求的人有权看它。
当角色变化会改变返回内容时,还要让相关缓存失效,或让键反映新的可见范围。否则,一个拼接正确的键,也可能留着旧权限下生成的内容。
5. 审核通过以后,缓存什么时候删
任务通过审核后,待审核数量发生变化。容易想到的办法是更新数据库,再删除看板缓存,让下一次读取重新计算。
关键是“更新数据库”应当指事务已经提交。如果先删缓存,而数据库事务还没有提交,另一个请求可能查到旧数据,再把旧数字写回 Redis。
审核事务提交
↓
删除项目看板缓存
↓
后续读取重新计算但即使顺序正确,也不能承诺缓存立即完全一致。一个更早开始的读取请求,可能已经查到了旧数字,等删除完成以后才把它写回去;删除请求本身也可能失败。
Redis 对缓存一致性的讨论也指出,cache-aside 不能自动保证数据始终一致。这里允许短暂滞后,因此可以用提交后失效配合过期时间,同时记录失效失败。若业务需要更强的更新保证,再考虑持久失效任务或版本化缓存。
过期时间限制的是缓存项写入后存活多久,不是“从数据库更新开始,最多只会旧这么久”的严格证明。旧查询晚写回、查询耗时和故障,都可能影响实际滞后窗口。
6. 过期时间要和写入放在一起
如果先 SET,再单独 EXPIRE,两条命令之间发生异常,就可能留下没有过期时间的缓存。
Redis 的 SET支持在同一条命令中设置过期时间,例如:
SET dashboard:v1:project:42 '{"pendingReview":12}' EX 30这里的数字仍然只是示意数据。选择 Redis 客户端时,应确认“写值并设置 TTL”的调用最终具备这个语义。
看板重新计算完成后,最好附带计算时间,让页面知道这份统计对应哪个时刻。计算时间也不等于所有数据永久冻结在那一刻;如果看板包含多条查询,跨查询一致性需要另外考虑。
7. 回到 MySQL,也要考虑它接不接得住
缓存故障时回源,听起来很稳妥。但如果所有请求突然回到数据库,原来被缓存挡住的读取压力也会一起回来。
因此,可回源只是功能路径存在。还需要给缓存调用设置较短且合适的超时,限制同时进行的聚合查询,并在数据库无法承受时明确返回暂不可用,而不是无限排队。
热门键到期时,多个请求还可能同时计算同一份统计。确实出现这种负担以后,可以考虑合并同一键的并发加载,或者提前刷新。先把查询条件和索引弄清楚,通常比一口气加入所有缓存机制更容易解释。
判断缓存是否有帮助,我会同时看接口延迟、缓存命中情况、数据库查询次数和失败情况。只看命中率,可能忽略已经变旧的数据;只看单次 Redis 读取耗时,也不能代表完整接口表现。
我也会先保留从数据库重建统计的路径,不急着把审核操作改成“直接给 Redis 里的数量减一”。重复请求、事务回滚和批量操作都可能让增量计数偏离事实;如果后面确实需要这样优化,去重、核对和修复就要一起补上。
重新看缓存这件事,我更关心的是它省掉了什么工作,以及失效以后能否恢复。先回答这两个问题,才容易判断下一步该优化查询、调整缓存,还是暂时维持现状。