在SaaS系统发展到一定规模以后,按照租户进行分库可以降低单个数据库的压力,也能够在数据隔离、故障控制和容量扩展方面获得更大的灵活性。但数据库一旦从单库变成多个租户数据库,一个原本简单的SQL查询就可能变成多个数据库之间的数据访问问题。例如运营部门需要统计所有租户的订单数量,安全团队需要查询全局审计日志,企业管理员需要比较多个租户的数据,平台还可能需要生成跨租户的收入、活跃用户和系统使用情况报表。这些需求都属于跨租户查询,但它们并不意味着必须建立一个能够直接访问所有租户数据库的“大SQL”。
真正需要解决的问题,是如何在保持租户数据隔离的同时,为平台提供必要的全局数据能力。实践中,最重要的原则是先区分“跨租户读取”和“跨租户写入”。前者主要解决数据汇总、搜索和分析问题,后者则可能涉及事务、一致性和权限边界,两者的技术方案完全不同。
一、分库以后为什么跨租户查询会变复杂
在单库SaaS架构中,所有租户的数据可能都位于同一张orders表中,只需要增加tenant_id条件即可完成查询。例如统计某个时间段内所有租户的订单数量,可以直接进行聚合。但是按照租户分库以后,tenant_id不仅仅是数据库中的一个字段,它还承担了数据库路由的作用。应用首先需要知道某个租户的数据位于哪个数据库实例,然后才能向对应数据库发送查询。
因此,跨租户查询通常不应该直接让业务代码连接所有数据库并随意执行SQL。更合理的方式是由统一的租户元数据或路由服务维护tenant_id与数据库实例之间的映射关系。查询服务首先确定目标租户集合,再按照数据库位置把请求分组,然后分别执行查询,最后在应用层或者专门的数据查询层进行汇总。
例如平台需要统计1000个租户过去一天的订单数量,完全没有必要把1000个数据库拼接成一个巨大的UNION ALL语句。可以按照数据库实例进行分组,把多个租户的查询合并到对应数据库执行,然后只返回COUNT、SUM等必要的聚合结果。这样既可以减少网络传输,也能够避免把大量业务明细数据全部拉回应用服务器。
这里还需要注意,租户元数据表本身通常应该属于平台级数据,而不是某个租户数据库的一部分。它至少需要能够回答租户当前所在的数据库、数据库版本、状态以及必要的路由信息。实际系统中还可能包括租户状态、区域、数据迁移状态等字段。这个元数据层一旦发生错误,就可能导致查询路由错误,因此需要具备严格的权限控制、缓存策略和故障恢复机制。
二、跨租户查询最常见的办法是建立专门的数据汇总层
如果跨租户查询只是偶尔发生,例如管理员临时查询几个租户的数据,应用层Fan-out查询是一种比较直接的方法。系统根据租户列表找到对应数据库,然后并行或者分批执行相同的查询,最后将结果汇总。这种方式实现成本低,而且不会改变原有租户数据库的存储结构。
但如果每天都需要统计全平台的数据,就不应该每次都访问所有租户数据库。大量跨库扫描会产生明显的网络、CPU、磁盘I/O和连接池压力,而且租户数量增加以后,查询时间也可能随之增长。
对于报表、运营分析、审计和数据仓库等场景,更适合建立独立的数据汇总层。租户数据库继续承担在线交易,也就是OLTP工作;订单、支付、用户活动等需要跨租户分析的数据,则通过CDC、消息队列或者定期ETL同步到分析数据库、数据仓库或者搜索系统。这样平台的全局查询就不再需要逐个访问生产数据库。
这种架构的一个重要特点是接受适度的数据延迟。运营报表如果允许延迟几分钟甚至更长时间,就没有必要为了一个统计数字让1000个生产数据库同时承担查询压力。对于实时性要求不同的数据,可以设计不同的同步链路,而不是让所有查询都直接穿透到租户数据库。
三、不要把跨租户查询和分布式事务混为一谈
原文将TCC和XA作为跨租户查询的重要解决方案,这实际上混淆了两个不同的问题。普通的跨租户SELECT查询通常不需要分布式事务。统计订单数量、生成报表或者搜索审计数据,核心问题是如何读取和汇总数据,而不是如何让多个数据库同时提交一个事务。
真正需要讨论分布式事务的,是跨租户写入。例如一个平台级业务操作同时修改多个数据库,并且业务要求这些修改具有严格的原子性,此时才需要进一步考虑两阶段提交、TCC、Saga等分布式事务模式。
而且即使业务需要跨数据库写入,也不能简单地认为XA就是最佳方案。分布式事务会增加协调、锁持有、失败恢复和运维复杂度,对数据库性能以及系统可用性都会产生影响。很多SaaS系统更倾向于通过事件驱动、幂等操作、状态机和补偿机制解决最终一致性问题,而不是让所有业务操作都进入一个长时间存在的分布式事务。
尤其需要注意,跨租户数据操作本身还涉及业务权限问题。一个平台管理员能够查看所有租户的数据,并不意味着普通租户用户也拥有同样的数据库访问能力。租户隔离必须在身份认证、授权、查询路由和数据访问层同时建立,而不能只依赖SQL中的一个WHERE条件。
四、性能问题首先来自访问范围,而不是简单的SQL写法
跨租户查询最容易出现的问题,是一次请求访问了过多数据库。例如一个管理员页面只是需要显示“全部租户今日订单数”,如果后台每次打开页面都实时访问几百甚至几千个数据库,那么即使每个数据库只执行一个非常简单的COUNT,也可能形成巨大的连接和网络开销。
因此,首先应该限制查询范围。能够按照时间、租户、区域或者业务状态缩小范围,就不要扫描整个租户集合。对于大量租户,应采用批处理、并行度限制和任务队列控制访问速度,而不是无限制地同时建立数据库连接。
索引仍然重要,但应该根据实际查询条件设计。例如订单表经常按照created_at统计,那么相关索引可能比单纯建立tenant_id索引更有价值。对于单个租户数据库,tenant_id甚至可能根本不需要作为主要索引字段,因为数据库本身已经承担了租户隔离。如果每个数据库只保存一个租户的数据,再在每张表上重复建立tenant_id索引,可能只是增加索引维护成本。
覆盖索引也不是所有查询都应该强制采用。只有当查询模式稳定、返回字段有限,并且覆盖索引能够明显减少回表成本时,它才具有实际价值。数据库优化应该依据EXPLAIN或EXPLAIN ANALYZE、实际I/O、执行时间和数据分布进行,而不是套用“所有查询都要覆盖索引”的规则。
五、Redis适合缓存结果,但不能解决架构上的跨库压力
对于高频且允许短时间数据延迟的统计结果,可以使用Redis等缓存系统。例如“全部租户今日订单量”可能每分钟更新一次,没有必要每一个用户请求都重新扫描所有数据库。
但缓存TTL并不存在一个适用于所有SaaS系统的固定值。7天对于某些统计结果可能过长,对于另一些实时业务又可能完全不合适。缓存时间应该根据数据变化频率、业务容忍的数据延迟以及缓存失效策略决定。
更重要的是,Redis不能代替数据汇总架构。如果后台第一次访问缓存时仍然需要扫描几千个租户数据库,那么缓存只是降低重复查询的频率,并没有解决第一次查询的成本。对于长期存在的全局统计需求,应该优先考虑CDC、消息队列、数据仓库或者专门的聚合表。
六、跨租户写入比跨租户读取更加危险
读取多个租户数据库已经需要严格控制访问范围,而跨租户写入的风险更高。平台如果允许一个操作同时修改多个租户数据库,必须明确操作权限、失败处理、幂等机制以及重试策略。
不能简单地通过SELECT ... FOR UPDATE解决问题。这个语句只能在支持相应锁语义的事务中锁定当前数据库中的符合条件的记录,它并不会自动协调多个数据库之间的事务,更不能消除死锁。跨数据库操作如果需要锁定多个资源,还必须设计一致的锁顺序、合理的事务范围和超时处理。
对于大多数SaaS业务,更值得优先考虑的是把跨租户写操作拆成可追踪的业务任务。例如平台发起批量操作以后,为每个租户生成独立任务,任务执行必须具备幂等性,并记录成功、失败和重试状态。这样即使某一个租户数据库暂时不可用,也不会因为一个数据库故障让整个批量任务进入不可恢复状态。
七、安全边界必须建立在数据库之外
跨租户查询最大的风险并不只是SQL执行速度,而是数据越权。一旦查询服务能够访问所有租户数据库,它本身就成为一个高权限组件。因此,不能让业务人员直接输入SQL,然后由后台把SQL广播到所有数据库执行。
更合理的方式是采用受控查询模型。业务系统提供明确的查询接口,由服务端决定允许查询哪些字段、哪些租户以及哪些时间范围。对于审计和管理类功能,还应该记录操作者身份、查询范围、执行时间、访问租户和结果用途等信息。
“禁止OR查询”也不能作为通用安全规则。OR本身并不是数据泄露的原因,真正的问题是权限条件是否可靠、查询构造是否正确以及是否存在绕过租户过滤的逻辑漏洞。与其简单禁止某个SQL语法,不如在数据访问层建立不可绕过的授权机制,并通过自动化测试验证不同租户之间的数据隔离。
八、不同业务应该采用不同的跨租户架构
如果只是偶尔查询几个租户,可以采用应用层路由加并行查询;如果每天需要生成全平台报表,可以把数据同步到专门的分析数据库;如果需要全文搜索和复杂的数据检索,可以考虑搜索系统;如果需要长期经营分析,则更适合建立数据仓库或者湖仓体系。
MySQL本身也可以继续承担相当一部分工作。对于规模有限、查询模式比较简单的系统,没有必要因为“已经分库”就立即引入复杂的分布式数据库。PostgreSQL配合Citus等分布式扩展可以解决部分横向扩展问题,但这属于数据库架构选择,并不是跨租户查询的唯一答案。MongoDB分片则解决的是MongoDB自身的数据分布和扩展问题,同样不能简单等同于SaaS跨租户分析架构。
对于真正的大规模SaaS平台,一个常见的思路是把在线交易系统与全局分析系统分开。租户数据库负责保证业务交易性能和租户隔离,数据通过CDC或事件流进入集中式分析平台。这样既可以保留分库带来的隔离和扩展能力,也可以让全局统计不再直接压迫在线数据库。
SaaS数据库分库以后,跨租户查询真正需要解决的并不是“怎样写出一个能够同时查询所有数据库的SQL”,而是如何重新设计数据访问边界。少量、低频查询可以由应用层进行路由和汇总;高频统计应该通过聚合数据减少对生产库的访问;复杂分析应该进入专门的数据平台;跨租户写入则必须单独处理事务、一致性、幂等和失败恢复。随着租户数量增加,数据库架构最终考验的并不是某一条SQL能够跑多快,而是系统能否在数据隔离、查询效率、权限控制和故障恢复之间保持稳定的边界。
SaaS 数据库分库以后,跨租户查询应该怎样处理
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP