做B2B SaaS最头疼的架构抉择是什么?不是技术选型,而是租户数据怎么放。给运输公司搭平台时,我卡在了一个经典难题上:每个租户单独数据库,还是共享数据库加逻辑隔离?

选了共享数据库。不是拍脑袋,是算过账的。

打开网易新闻 查看精彩图片

500个租户意味着什么?如果是独立数据库,加一个字段要跑500次迁移。新功能上线?先熬几个通宵迁移数据。备份恢复?500份备份文件管理到眼花。共享数据库的核心优势就三条:原子迁移一次搞定、备份恢复简单、基础设施成本低。代价也明显:隔离是逻辑层面的,存在"吵闹邻居"风险——某个租户的大查询可能拖慢其他人。

打开网易新闻 查看精彩图片

但权衡下来,共享数据库赢了。真到500租户那天,维护成本会替我证明这个选择。

架构设计比选择更重要。我的方案是纯Laravel实现,零外部依赖。请求进来先过中间件EnsureTenantAccess:验身份、提取租户ID、存进TenantContext。这是个单例,一个请求周期内全局唯一,彻底消灭上下文混乱。

数据隔离的魔法在BelongsToTenant这个Trait里。所有租户相关模型——客户、车辆、运输单——都挂载它。Trait自动做两件事:读的时候加全局作用域,自动拼接where tenant_id = X;写的时候自动填充tenant_id字段。工程师不用手动赋值,彻底告别"忘加租户条件"的Bug。

代码层面,TenantContext极简:set存ID、id取ID、isSet判空。就三个方法,但它是整个隔离体系的唯一信源。BelongsToTenant Trait在boot阶段注册全局作用域,查询构造器自动注入过滤条件。创建模型时,如果tenant_id为空且上下文已设置,自动赋值。

打开网易新闻 查看精彩图片

但代码写得再漂亮,没有测试就是赌运气。我写了专门的跨租户隔离测试:创建租户A和B,分别插入商品,用A租户用户请求列表,断言只返回A的数据、条数为1、名称匹配。没有这行测试,我只能祈祷GlobalScope正常工作——而祈祷不是工程策略。

实际运营中还有两个刚需场景。超级管理员需要 impersonate(模拟)租户视角查看问题,我的方案是:管理员无固定tenant_id,后台选择目标租户后写入session,中间件照常注入TenantContext,用户看到的界面和真实租户完全一致。只读模式也常见——欠费租户只能查不能改,中间件里判断is_read_only且请求非安全方法时直接403。

这套方案跑了两年,核心结论就一句:共享数据库的多租户,隔离可靠性不取决于数据库层,取决于代码层的纪律性。TenantContext单例+全局作用域自动注入+强制测试覆盖,这三道防线比物理隔离更可控——毕竟物理隔离也能被错误的连接字符串击穿。