设计 SaaS 数据库时,最容易犯的错误不是表设计得太少,而是一开始就把系统设计成大型互联网平台。一个只有几百个客户的 SaaS 产品,如果上来就采用分库分表、分布式事务、多活数据库、复杂权限引擎和数据联邦,数据库可能还没有遇到性能问题,架构本身已经成为维护成本。
比较合理的方式,是先围绕业务建立最小可用的数据模型,再根据用户数量、数据量、权限复杂度和业务需求逐步增加表和基础设施。
一个简单的多租户 SaaS,通常可以从用户、租户、成员关系、角色权限以及业务数据这几个部分开始。
用户表是身份系统的基础
最基本的用户表通常保存用户的身份信息和账户状态,例如用户ID、登录邮箱或用户名、密码哈希、显示名称、状态、创建时间和更新时间等。
这里需要区分“用户”和“租户”。
用户代表一个人或者一个登录账户,而租户通常代表一个企业、团队或者独立客户。两者不是同一个概念。
例如一家企业有20名员工,那么可以有一个租户记录,同时对应20个用户账户。
因此,简单系统可以设计成:
users
保存用户本身的信息。
tenants
保存企业或者团队的信息。
tenant_users
保存用户属于哪些租户以及在租户中的身份。
这种设计比直接在users表中加入一个tenant_id更加灵活。因为很多SaaS产品以后可能允许一个用户加入多个企业。例如一个顾问同时管理三家公司的账户,如果users表只能保存一个tenant_id,就需要重新设计。
如果产品明确规定一个用户永远只能属于一个租户,那么直接在users表保存tenant_id也完全可以。数据库设计应该服务于业务,而不是为了追求复杂而复杂。
主键也没有必要迷信UUID
用户ID可以使用自增整数,也可以使用UUID、UUIDv7等方案。
UUID的优势包括分布式环境下生成ID比较方便,以及对外暴露时不容易根据ID连续性推测数据规模。但这并不意味着自增ID不适合SaaS。
如果系统规模不大,而且数据库主要采用单库架构,自增BIGINT完全可以满足需求。
如果系统未来需要多数据库写入、离线生成ID或者跨系统合并数据,那么UUID等全局唯一ID的价值会更加明显。
因此,ID类型应该根据数据库、分布式需求以及对外接口设计决定,而不是把UUID当成SaaS的强制标准。
角色和权限可以从RBAC开始
用户登录以后,系统还需要知道他能做什么。
最常见的简单方案是RBAC,也就是基于角色的访问控制。
数据库可以设计:
roles
保存角色,例如管理员、经理、普通员工。
permissions
保存具体权限,例如users.read、users.write、invoice.read等。
role_permissions
建立角色与权限之间的关系。
tenant_users
则可以记录某个用户在某个租户中拥有哪个角色。
这样,一个用户进入系统后,程序可以先确定他属于哪个租户,再确定他在该租户中的角色,然后根据角色判断能否执行某项操作。
对于很多中小型SaaS,这套模型已经足够。
没有必要一开始就引入ABAC、复杂策略引擎或者角色继承。只有当业务出现“同一个角色因为部门、地区、资源归属、客户等级等属性不同而拥有不同权限”时,才有必要进一步引入属性条件或者策略系统。
业务表才是SaaS数据库的核心
用户系统只是基础设施,真正决定数据库结构的是业务。
假设这是一个发票管理SaaS,那么除了users、tenants和权限相关表,还需要invoices、invoice_items、customers、payments等业务表。
如果这是项目管理软件,则可能需要projects、tasks、project_members、comments等。
如果是订阅型产品,则需要subscriptions、plans、subscription_items、invoices等与计费有关的数据。
这些业务表通常都需要明确保存所属租户的信息。例如:
invoices
其中包含tenant_id。
projects
其中包含tenant_id。
customers
其中包含tenant_id。
这样,在查询业务数据时,程序就可以始终带上租户条件,避免把一个租户的数据返回给另一个租户。
这一步比讨论数据库分片更加重要。
多租户隔离不能只靠数据库外键
外键可以保证数据库中的引用关系,例如tenant_users.tenant_id必须对应tenants.id,但外键并不能自动防止用户访问其他租户的数据。
真正的租户隔离还需要应用层访问控制。
例如查询发票时,不能只写:
SELECT * FROM invoices WHERE id = ?
而应该根据当前登录用户所属租户进一步限制:
SELECT * FROM invoices WHERE id = ? AND tenant_id = ?
这样,即使用户知道另一个租户的发票ID,也不能仅凭ID获取数据。
对于重要业务,还可以在数据库层增加额外的隔离机制,但不能把“有外键”理解成“已经完成了多租户安全”。
JSON字段应该适量使用
SaaS系统经常会遇到一些不固定的用户属性,例如主题设置、通知偏好、自定义界面配置等。
这些数据可以考虑放在JSON字段中。
例如user_preferences可以保存:
语言设置、时区、界面布局、通知选项等。
这样可以避免为了增加一个不重要的配置项频繁修改数据库结构。
但JSON不应该成为“什么都往里面塞”的垃圾抽屉。
如果某个字段经常用于查询、排序、统计、唯一性约束或者关联其他表,那么最好设计成普通字段。
例如用户状态、tenant_id、created_at等核心数据,就没有必要全部放进JSON。
审计日志和安全日志也不一定要分成两个系统
SaaS通常需要记录重要操作,例如谁修改了客户资料、谁删除了发票、谁改变了权限。
可以建立audit_logs表,记录租户、用户、操作类型、对象类型、对象ID、时间以及必要的上下文信息。
例如:
tenant_id
user_id
action
resource_type
resource_id
created_at
metadata
metadata可以保存少量与具体事件有关的附加信息。
对于登录失败、密码修改、权限变化等安全事件,也可以记录在审计系统中。只有当安全事件分析需求明显增加,或者系统已经接入专业安全监控平台时,才需要进一步拆分专门的安全事件系统。
审计日志应该考虑生命周期
日志是典型的持续增长型数据。
一个刚上线的SaaS可能每天只有几百条日志,但用户数量增加以后,日志可能迅速超过业务表的数据量。
因此应该提前考虑保留期限、归档和删除策略。
对于大型系统,可以按照时间进行分区或者把历史日志迁移到更适合长期保存和分析的存储系统。但小型SaaS没有必要为了几万条日志马上建立复杂的数据仓库。
计费数据最好与普通用户资料分开考虑
如果SaaS采用订阅收费,建议逐渐建立独立的计费数据模型。
例如plans保存套餐,subscriptions保存租户当前订阅,invoices保存账单,payments保存支付记录。
不要把“订阅状态、付款时间、套餐名称、下一次扣款日期”等所有信息塞进tenants表。
租户表可以保存当前状态等必要的概要信息,但完整的计费历史应该由专门的业务表保存。
这样以后处理升级、降级、取消订阅、退款、账单和支付失败时,数据结构会更加清晰。
数据迁移也不应该一开始就做成独立复杂系统
原文把“数据迁移表”列为简单SaaS的核心表之一,这实际上取决于业务。
如果产品没有租户合并、跨数据库迁移或者大型数据导入需求,根本没有必要专门建立复杂的租户迁移系统。
当业务确实需要迁移时,可以建立migration_jobs等任务表,记录任务状态、开始时间、结束时间、进度和错误信息。
对于大型迁移任务,还应该使用后台任务队列,而不是让用户浏览器一直等待一个数据库事务完成。
数据库性能优化应该从查询开始
一个简单SaaS最常见的性能问题通常不是“没有分库分表”,而是查询没有正确使用索引。
例如业务查询经常使用:
tenant_id + status
tenant_id + created_at
tenant_id + user_id
那么可以根据实际查询建立相应的复合索引。
但索引也不是越多越好。每增加一个索引,写入数据时就可能增加维护成本,占用额外存储空间。
因此应该先观察真实查询,再通过数据库的执行计划分析慢查询,而不是按照所谓的“标准SaaS索引模板”一次创建几十个索引。
什么时候才需要分库分表
当数据库规模继续增长以后,才需要考虑更复杂的架构。
如果单数据库仍然可以稳定处理业务,就没有必要为了理论上的未来容量提前分片。
当数据量、并发写入或者租户隔离需求达到一定规模后,可以考虑按租户、业务或者数据类型进行拆分。
例如,一部分大型企业客户可能被放入独立数据库,而普通客户继续共享数据库。
这也是多租户架构的一种常见演进方式。
共享数据库共享表、共享数据库不同schema、不同数据库以及混合模式都可以成立,并不存在一个适用于所有SaaS的唯一答案。
分布式事务也不是数据库设计的起点
如果所有核心业务数据都在同一个关系型数据库中,很多业务操作直接使用普通事务即可。
只有当一个业务操作跨越多个独立服务、多个数据库或者外部支付系统时,才需要进一步考虑事务边界、幂等、消息队列以及最终一致性。
Raft和Paxos属于分布式一致性协议,并不是设计普通SaaS业务表时必须考虑的技术。
同样,多活数据库、数据联邦和复杂的数据仓库,也应该在实际业务规模和需求出现以后再引入。
一个简单SaaS可以从十张以内的核心表开始
最初的数据库完全可以非常简单。
例如:
tenants
users
tenant_users
roles
permissions
role_permissions
以及若干业务表,再加上必要的audit_logs。
随着业务发展,再增加subscriptions、invoices、payments、notifications、files等模块。
当数据规模继续扩大以后,再考虑缓存、读副本、消息队列、分库、分表、数据仓库和独立日志系统。
数据库设计的核心不是一次性预测未来十年的流量,而是让当前业务的数据关系清楚、租户边界明确、权限能够验证、事务能够保证,并且给未来扩展留下合理空间。
一个好的SaaS数据库应该能够随着业务增长逐步演进,而不是在产品只有几十个客户的时候,就背负一套只有大型互联网公司才需要的分布式架构。
一个简单的 SaaS 数据库应该有哪些表?从用户系统开始逐步设计
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
分享 Facebook | X | WhatsApp | LinkedIn
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP