隔离
由数据库守住的边界,而不是代码
每个CRM都声称自己是多租户的。这种声明很廉价;真正重要的是由哪一层来守住这条线。如果答案是“我们的应用总是加上正确的过滤条件”,那么这个保证的可靠性就取决于写查询的那个最缺觉的开发者。
iKonekt从第一次数据库迁移起就是多租户的,而不是事后补装的。这听起来像自夸,实际上是一种坦白:事后补装多租户几乎不可能做得令人信服,而每个尝试过的人都有故事可讲。
隔离存在于数据库中。二十六张表中的每一张都带有它所属的租户,检查这一点的策略在每次读取和每次写入时都会执行,完全在应用层之下。一条忘记边界的查询不会返回别人的客户。它什么都不返回。
行级安全,强制执行
不是约定,不是代码评审规则。是数据库拒绝。七十二条策略,一条一条手写出来而不是生成的——因为生成的策略是没人读过的策略。
复合键
子记录同时引用它的租户和它的父记录,作为一个整体。来自错误公司的杂散标识符无法解析——它根本就不是一个有效的引用。
永远没有默认租户
一个没有作用域的请求不会悄悄变成对零号租户的请求。它在到达数据库之前就会抛出异常。不存在可以忘记关闭的“无作用域”模式。
那个无法自我暴露的缺陷
做租户隔离工作的时候,大多数错误都是响亮的。给两家公司同一个客户域,数据库对象立刻报错,带着错误码和堆栈跟踪;午饭前就能修好。
有一个却没有吭声。一张可编辑提示词覆盖表只以提示词的种类作为键——而不是以种类和租户。第二家公司编辑自己的副本时会悄悄覆盖第一家的,用的是完全合法的 SQL,没有错误、没有警告、也没有失败的测试。它是在改了一个键之后,怀疑响亮的错误还有同伙,于是把剩下的每一个唯一索引逐个读了一遍才被发现的。
我们提这件事,因为它是这项工作诚实的样子。隔离不是一个可以打开的功能;它是一种习惯——去读 schema 里那些无聊的部分,并假定里面有什么东西不同意你的看法。
顺便记录一下测试:共 942 个,每次改动都在一个从零重建的数据库上运行。它们的存在不是为了证明设计有多聪明。它们存在是因为这种特定设计的失败方式是无声的,而无声的失败需要有人替它们喊出来。
隔离能给普通人带来什么
- 一个集团可以在同一套安装上运营多家公司,其中一家的销售永远不会看到另一家的客户——不是靠政策,而是靠构造。
- 合作伙伴计划、一组提示词、对客户的认领:全部按公司划分范围,所以一个租户的配置不会变成所有人的。
- 删除请求在一个边界之内得到执行,而不是跨越一堆共享数据,这正是能够诚实回答的原因。
- 而且谁都不用记住这些。这就是重点——一个依赖被记住的保证不是保证。
还有一句与数据库无关的话
家族里的每个同级服务都持有自己的命名凭证。没有任何东西用共享凭证通信,所以没有服务能意外地以另一个的身份行动——这条小小的纪律直接来自曾经用过共享令牌并且很不愉快的经历。
这和数据库规则是同一种直觉。让错误的事情变得不可能而不是不被鼓励,你就能把注意力花在客户身上而不是警惕上。
- 126 张表每一张都带有租户字段,检查被强制开启。
- 272 条策略逐条写出,所以每一条都被一个真人读过。
- 3942 个测试在每次改动都从零重建的数据库上运行。
- 40 个默认租户没有作用域的请求永远到不了数据。
在正式构建上实测

“我们的应用总是按公司过滤。”
“总是”这个词在那句话里承担了太多。
把边界下移到数据库里,一个被遗忘的 WHERE 子句返回的是空结果,而不是别人的客户。空白的屏幕比一次尴尬的介绍要好。
面向运营多个品牌的集团 →