AI 写缓存前,先读懂租户边界

摘要| 同一个缓存键,为什么可以属于不同企业?沿着 Microi吾码的 V8 缓存调用链,分清租户命名空间、用户权限和数据一致性。

01 / AI 写对了业务,缓存也可能写错边界

让 AI 帮忙写一段接口引擎,最容易获得的答案往往是:先读缓存,没有就查数据库,再把结果放回去。这段逻辑看起来完整,却少了一个先决问题——这个结果到底属于谁?属于整个平台、一个租户,还是当前用户?

假设两个企业都使用同一套 Microi吾码应用,而且都把展示配置放在 Theme 键里。逻辑键同名并不意味着业务相同。再假设同一企业的采购员与财务人员请求“我的待办”,即便租户一致,两个人也不应该读到同一份列表。两个问题很像,解决它们的边界却不同。

先确定所有者| 租户命名空间解决企业之间的键归属;用户身份和业务权限决定同一企业内部谁能读到什么。AI 生成的缓存代码,需要同时回答这两个问题。

02 / 从文档追到真正执行的入口

这次分析以当前工作区的官方后端 V8 文档、缓存专项 Skill,以及 V8Engine、V8TenantCache、TenantConfigurationSecurity 三处实现为依据。文档声明逻辑键会自动加上当前租户前缀,源码则解释了这件事发生在哪里。

执行引擎把租户缓存包装成 V8TenantCache,再放进脚本上下文。这个代理保存规范化后的当前租户标识;脚本调用 Get、Set、Remove 或 Hash 相关方法时,键先经过同一个 NormalizeCacheKey,再转交底层缓存。换句话说,租户约束在能力入口里执行,不依靠每段 AI 生成脚本都记住拼前缀。

从脚本调用到租户键规范化的调用链示意

从脚本调用到租户键规范化的调用链示意

这个实现位置很有价值:当能力被复用到多段脚本时,一处约束可以覆盖多种缓存操作。但它也提醒我们,分析不能停在“示例写了前缀”,还要沿着调用链确认实际传下去的键。

03 / 三种键输入,对应三种处理

当前规范化方法先整理输入,拒绝空键和控制字符。普通逻辑键会补成当前租户命名空间;已经带当前租户前缀的完整键保持兼容;以 Microi: 开头却指向其他租户的键会被拒绝。历史 SysConfig:{当前租户} 也有专门兼容分支,普通业务不应该把它当成自己的命名模板。

  • 业务逻辑键:ArticleDemo:Theme → 当前租户前缀 + ArticleDemo:Theme。
  • 当前租户完整键:兼容已有写法,不会重复拼接前缀。
  • 其他租户完整键:直接拒绝,不能靠传入字符串切换租户。

这里的“当前”很关键| 租户来自可信执行上下文。不要为了让示例灵活,就把前端提交的 OsClient 或任意 Key 当成跨租户访问入口。

键命名空间、用户权限与数据一致性的边界对照

键命名空间、用户权限与数据一致性的边界对照

04 / 第一个例子:缓存租户共享的展示值

下面是一段可按业务改造的接口引擎示例。它使用服务端固定的逻辑键,保存一个不含个人信息的展示值。这里的 300 表示有效期秒数;示例仅说明缓存 API 与键归属,不承诺缓存故障时业务一定成功。

var key = 'ArticleDemo:Theme';
var value = V8.Cache.Get(key);
if (!value) {
    value = 'calm-blue';
    V8.Cache.Set(key, value, 300);
}
return { Code: 1, Data: value };

这个键可以在当前租户中共享,因为值本来就是租户共享的。如果实际内容改成报价、人员资料或审批数据,先重新判断数据范围,再决定缓存策略。把同一段模板复制到敏感业务,并不会自动继承正确的权限。

05 / 第二个例子:用户维度要自己表达

“我的页面偏好”属于当前用户。除了要求已登录,还要把可信的用户标识加入业务键。下面只保存演示偏好,不读取任何业务表;它说明用户维度如何进入键,而不是一份完整的业务鉴权方案。

if (!V8.CurrentUser || !V8.CurrentUser.Id) {
    return { Code: 0, Msg: '请先登录' };
}
var key = 'ArticleDemo:Preference:' + V8.CurrentUser.Id;
var preference = V8.Cache.Get(key);
return { Code: 1, Data: preference || 'default' };

如果缓存的是“可查看的订单”,用户 ID 仍然不够。角色变化、部门调整、订单状态变化都可能让旧结果过时。命中缓存之前或取出之后,需要按业务设计重新确认访问条件;也可以缓存较稳定的基础数据,再在返回时执行当前权限过滤。具体选择取决于成本和一致性要求。

不要把命中当成授权| 缓存里存在某个对象,只说明曾经保存过它。它不证明当前请求者仍有权访问,也不证明它仍然符合当前业务状态。

06 / TTL、Hash 与一致性,别混成一件事

缓存的有效期限制数据保留时间,却不保证数据库事务与缓存更新一起完成。一次业务写入成功之后,如果缓存清理失败,旧值仍可能继续存在。因此,需要明确哪些数据允许短暂陈旧,哪些操作必须回到权威数据源判断。

Hash 也有容易误读的地方。当前公开能力可以给整个 Hash 键设置过期时间;这不等于每个字段拥有独立的有效期。把每个用户的临时结果塞进同一个 Hash,再以为它们分别倒计时,模型很容易生成看似顺手、实际语义不同的代码。

V8.Cache.HashSet('ArticleDemo:Hints', 'Theme', 'calm-blue');
V8.Cache.Expire('ArticleDemo:Hints', 300);
// 300 秒作用于整个 Hash Key,不是单独的 Theme 字段。
  • 先写数据归属:租户共享、用户专属,还是权限过滤后的临时结果。
  • 再写失效条件:到期、业务变更、权限变化分别怎样处理。
  • 最后写故障行为:缓存读取或删除失败时,业务是回源、拒绝还是稍后补偿。

07 / 怎样给 AI 一份更可靠的任务说明

比起只说“加 Redis 缓存”,更有效的输入是一张简短的约束卡:运行端为后端接口引擎;通过 V8.Cache 使用当前租户缓存;逻辑键由服务端固定命名;值不含个人信息;有效期为五分钟;缓存异常不能伪装成数据库查询成功。若涉及用户数据,再明确用户维度和权限变化后的失效策略。

把验收条件写进需求| 要求 AI 解释键由谁构造、何时失效、命中后还要检查什么,并指出哪些结论来自当前源码。这样更容易发现遗漏,而不是只评价生成代码是否简短。

当前源码中已有对逻辑键、当前租户完整键、历史配置键以及其他租户键拒绝行为的测试用例。本文核对了这些源码与断言,但没有把它们描述成此次已经运行的租户集成测试,也没有测量吞吐量。源码行为、测试执行、实际部署版本,是三份不同的证据。

当前源码中租户键规范化分支的摘录与核对范围

当前源码中租户键规范化分支的摘录与核对范围

08 / 一个小入口,决定了复用是否可靠

Microi吾码把租户键规范化放进 V8 缓存代理,使接口引擎在复用缓存能力时拥有统一的命名空间约束。对 AI 辅助开发而言,这类平台原子能力尤其重要:模型可以专注业务编排,基础边界由运行时持续执行。

同时,业务所有者仍要把用户权限、数据新鲜度与故障处理写清楚。下次看到 AI 提交“先读缓存、未命中再查询”的代码,可以先问三个具体问题:这个值属于谁,什么时候会变旧,返回前还要确认什么。答案清楚了,缓存才真正成为可维护的业务设计。

本文由AI辅助创作,概念配图由AI生成。技术分析依据当前工作区源码与官方文档;调用链、边界图与源码摘录由程序排版。概念图不代表实际产品界面。

Logo

宁波官方开源宣传和活动阵地,欢迎各位和我们共建开源生态体系!

更多推荐