AI 写缓存时,为什么一定要先确认租户边界?

摘要| 同一个业务键为什么不能直接跨企业复用?本文从 Microi 吾码当前源码、专项 Skill 与测试断言出发,把租户命名空间、用户权限和数据新鲜度拆成三次独立确认。
✦ 01 / 缓存命中了,不等于读对了
让 AI 帮忙写接口时,最顺手的模板通常是:先读缓存,未命中再查库,查到后回填。问题是,这个模板只回答了“快不快”,没有回答“属于谁”。两个企业可以同时使用同一套应用,也可以同时使用同一个逻辑键;如果键的归属没有由可信运行时补上,缓存就可能把隔离问题伪装成性能优化。
还要再问一层:即使租户相同,当前用户是否都能看到这份结果?租户命名空间解决企业之间的边界,用户身份与业务条件解决企业内部的访问范围。两者都不能由前端传来的 OsClient 或任意字符串代替。
先写所有者| 每一段缓存代码都应先写清:这是租户共享值、用户专属值,还是按权限过滤后的短期结果。所有者不清楚,TTL 写得再漂亮也只是把错误保存得更久。
✦ 02 / 先从平台能力入口追到规范化函数
本次核对范围很窄:后端 V8 缓存调用如何把逻辑键收口到当前租户。依据包括当前工作区的 TenantConfigurationSecurity 实现、后端 AI/缓存专项 Skill,以及对应的 TenantConfigurationSecurityTests 断言。这里讨论的是源码与测试事实,不把它扩写成一次线上租户集成测试。

业务逻辑键先进入平台能力边界,再由当前租户上下文完成规范化。
源码把 NormalizeCacheKey 放在可信平台层,而不是要求每个可编辑脚本自行拼接前缀。这种位置选择的价值是复用:Get、Set、Remove 和 Hash 操作都可以共享同一条租户边界;它的限制也同样明确——它只负责命名空间,不会自动替业务判断权限或数据是否过期。
✦ 03 / 三类输入,三种不同结论
当前实现先修剪输入、拒绝控制字符和空键,再处理三种情况。普通逻辑键会补上当前租户前缀;已经带当前租户前缀的完整键保持兼容;以 Microi: 开头但属于其他租户的键直接拒绝。历史 SysConfig:{当前租户} 还有兼容分支,不能把它误认为普通业务键模板。
- 普通键:orders:today → Microi:tenant_a:orders:today。
- 当前租户完整键:Microi:tenant_a:orders:today → 原样保持规范化结果。
- 其他租户完整键:Microi:tenant_b:orders:today → 抛出跨租户访问异常。

同一个字符串是否安全,取决于它与当前可信租户上下文的关系。
这里的当前不是参数| 当前租户来自可信执行上下文。不要为了让示例看起来灵活,就把请求体里的 OsClient、租户名或完整缓存键当成切换租户的入口。
✦ 04 / 示例一:缓存租户共享的展示值
下面的接口引擎片段只演示一项租户共享配置。键由服务端代码固定,值不含个人信息;缓存失效时回到一个安全默认值。真正的业务还需要补充异常处理、变更后的失效策略与监控。
var key = 'ArticleDemo:Theme';
var theme = V8.Cache.Get(key);
if (!theme) {
theme = 'calm-blue';
V8.Cache.Set(key, theme, 300);
}
return { Code: 1, Data: theme };
这段代码的关键不是字符串叫 Theme,而是平台会把逻辑键解释为当前租户命名空间中的键。如果实际值改成订单、人员资料或审批结果,就必须重新判断是否需要用户维度、角色过滤或回源校验。
✦ 05 / 示例二:用户维度必须自己表达
“我的页面偏好”不是租户共享值。它需要当前用户身份进入业务键;而且用户 ID 仍然不等于授权,角色、部门、状态变化都可能让旧结果失去意义。
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 与一致性不是同一个问题
TTL 限制的是缓存保留时间,不保证数据库事务与缓存更新同时成功。一次写入提交后,如果清理失败,旧值仍可能在有效期内返回;如果先删缓存再写库,读请求又可能在窗口中看到旧数据或空结果。业务要先选择能接受的陈旧边界,再决定回源、拒绝还是补偿。
V8.Cache.HashSet('ArticleDemo:Hints', 'Theme', 'calm-blue');
V8.Cache.Expire('ArticleDemo:Hints', 300);
// 过期时间作用于整个 Hash Key,不是单独的 Theme 字段。
- 先写数据归属:租户共享、用户专属,还是权限过滤后的临时结果。
- 再写失效条件:到期、业务变更、权限变化分别怎样处理。
- 最后写故障行为:缓存异常时回源、拒绝或进入补偿队列。
✦ 07 / 给 AI 一张比“加 Redis”更完整的约束卡
更可靠的需求输入可以只有几行:运行端是后端接口引擎;通过 V8.Cache 使用当前租户缓存;逻辑键由服务端固定命名;值不含个人信息;有效期五分钟;缓存异常不能伪装成数据库查询成功;涉及用户数据时,补充用户维度、权限判断和失效条件。
var requestKey = 'ArticleDemo:Orders:' + V8.CurrentUser.Id;
var cached = V8.Cache.Get(requestKey);
if (cached) {
// 命中只代表曾经保存,不替代当前权限检查。
return { Code: 1, Data: FilterByCurrentPermission(cached) };
}
// 未命中时回源,并按同一权限条件保存可复用结果。

这张证据卡区分源码事实、测试断言与未执行的线上运行事实。
Microi吾码AI 的价值不在于让模型背下一个前缀,而在于把租户命名空间放进可复用的平台能力入口。模型负责业务编排,运行时持续执行基础边界;业务所有者仍要写清用户权限、数据新鲜度和故障行为。
✦ 08 / 最后一次确认:证据边界也要分层
本次结论可以落在三层:源码显示 NormalizeCacheKey 对普通键补当前租户前缀、对当前租户完整键保持兼容、对其他租户前缀拒绝;测试文件用三组输入断言了前两类结果和跨租户异常;本地发布包会把这些引用与 SHA-256 绑定到技术证据文件。
这不等于已经测量线上吞吐量,也不等于任何业务表都天然拥有正确的数据范围。看见“缓存命中”时,仍然按顺序问三句:这个值属于谁?什么时候会变旧?返回前还要确认什么?三句都有答案,缓存才从模板变成可维护的业务设计。
本文证据边界| 文章依据当前工作区源码、官方文档、专项 Skill 与测试断言创作;技术图示为确定性程序排版,不代表实际产品界面;没有把源码检查冒充线上租户集成测试。
本文由AI辅助创作,技术卡片底图由AI生成并由确定性程序叠加准确中文;源码分析与测试证据来自当前工作区。
更多推荐




所有评论(0)