发布于2026-07-18 阅读(0)
扫一扫,手机访问
先说几个核心判断:多租户架构里,数据隔离是真正的命门。很多事故,比如A租户的数据莫名其妙跑到了B的报表里,十有八九都是这一块没处理好。下面这些总结,希望能帮你少踩几个坑。

结论很明确:对于中小型系统,database-per-tenant(每个租户独立数据库)是最优解。这并非因为它听起来“高大上”,而是它从底层架构上,就帮你堵死了很多新手容易犯的致命错误。比如,SQL注入导致误查了其他租户的数据、WHERE条件里忘了写tenant_id、或者权限配置对不上号。反观共享数据库+共享表(也就是只靠一个tenant_id字段来区分),虽然看起来省事,但根据行业经验,上线后70%的数据越界问题,根源都在这里。
核心思路就一条:不要全局复用PDO实例。每个请求进来,都得根据租户的标识(比如子域名、HTTP请求头、或者JWT里的声明)去生成一个专属的连接。千万别再用mysql_select_db()那套老古董了——不安全,而且跨库查询基本没法做。
$_SERVER['HTTP_HOST']得到acme.example.com,那租户名就是acme。mysql:host=localhost;dbname=tenant_acme;charset=utf8mb4。注意,库名必须做白名单校验(用正则/^[a-z0-9_]+$/),这是为了防止路径遍历或SQL注入式的库名攻击。DatabaseManager的makeConnection()方法,传入动态的database配置项。InvalidArgumentException异常,而不是静默地回退到默认库。否则,租户访问你网站404了,却可能看到别人家的数据,这问题就大了。靠人写代码,永远会漏。必须把tenant_id绑定到查询生命周期里,而不是指望开发人员自觉。
GlobalScope,让所有模型查询都自动注入where('tenant_id', $current_tenant_id)。当然,这个scope要支持排除,比如管理员后台查全量数据时,可以用withoutGlobalScopes()临时关闭。db_query($sql, $params, $tenant_id)。这个函数内部会自动在SQL末尾加上AND tenant_id = ?,并把$tenant_id塞进$params里。tenant_id。租户专属数据的初始化,必须通过租户上下文触发,比如Tenant::find('acme')->seedDefaultPages()。ROW POLICY作为最后一道防线(需要开启check_constraint和row level security)。但注意,PHP层不能依赖它——因为策略失效时你不会收到PHP异常,数据可能已经泄露了。必须单独建一个不归属任何租户的“系统库”,里面只放租户的注册信息。其他所有业务库里,都不要存tenants表。否则,后期的迁移、备份、审计都会乱成一锅粥。
id(主键)、slug(用于URL/数据库名)、db_host、db_name、status(active/archived)。别存任何业务字段。CREATE DATABASE tenant_{$slug},然后导入基础schema。这个schema最好用mysqldump --no-data提前导出为模板,不要依赖ORM的migrate自动建表,因为不同版本的ORM可能导致结构不一致。DROP DATABASE那么简单。正确的流程是:先停写流量(改DNS或API网关路由),然后异步归档数据(mysqldump > /archive/tenant_xxx_202405.sql.gz),最后才能删库。跳过归档这一步骤,在GDPR这类合规要求下,就是严重的违规行为。其实,真正的麻烦从来不是“怎么切库”,而是那些看不见的坑:比如租户间抢资源(共用Redis连接池导致key冲突)、日志混在一起分不清谁是谁、监控指标没有按租户维度聚合。这些问题,在第一个租户上线前就得想清楚。不然等第二十个租户进来时,你很可能就得重写整个中间件层了。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8