系统星链引擎技术实践:多租户 SaaS 分库分表架构设计与落地

3 阅读34分钟

在团队自研跨平台内容全生命周期管理 SaaS 系统「星链引擎」的过程中,数据库架构是支撑业务规模化增长的核心底座。项目初期,为了快速落地 MVP,我们采用了单库单表 + tenant_id 字段逻辑隔离的极简架构,支撑了产品从 0 到 1 的落地。但随着业务快速发展,租户规模从几十家增长到数千家,核心业务表单表数据量突破 1 亿行,传统架构的短板彻底暴露:核心接口查询耗时从毫秒级飙升至秒级、大促高峰期数据库连接数打满、租户间业务互相干扰、表结构变更锁表长达数十秒、数据备份恢复耗时超 10 小时,甚至出现过因单租户大查询导致全平台业务卡顿的严重故障。

对于企业级多租户 SaaS 系统而言,分库分表从来不是简单的 “拆库拆表” 技术优化,而是需要兼顾租户数据安全隔离、业务读写性能最优、系统平滑扩容能力、运维复杂度可控、业务代码无侵入五大核心目标。基于此,我们从零到一重构了整套数据库架构,设计了 **「租户等级混合隔离 + 业务域垂直分库 + 场景化水平分表」的分库分表体系 **,基于 Apache ShardingSphere(Sharding-JDBC)实现了业务无侵入的分片路由,同时配套了完整的分布式事务、不停机扩容、全链路监控治理体系。

本文将完整拆解这套多租户分库分表架构的业务背景、设计思路、核心技术实现、线上踩坑复盘与落地效果,为同类企业级 SaaS 系统的数据库架构设计提供可落地的实践参考。

一、业务场景与核心技术挑战

星链引擎作为服务于企业内容运营团队的多租户 SaaS 系统,其数据模型与读写特征具备极强的业务属性,也决定了我们不能直接套用通用的分库分表方案,必须结合多租户场景做定制化设计。

1. 核心业务数据场景

我们的核心业务数据分为五大域,不同数据域的量级、读写特征、租户隔离要求完全不同,是分库分表设计的核心依据:

数据域核心表数据量级读写特征隔离要求
租户域租户信息、套餐配置、组织架构、角色权限万级低频读写,数据量小极高,企业租户要求物理隔离
账号域内容平台账号、授权凭证、账号配置百万级中等频率读写,单租户数据量差异大高,需严格租户隔离
内容域内容发布任务、发布记录、内容元数据亿级高频读写,流水型数据,持续增长中高,按租户隔离,冷热特征明显
素材域素材元数据、分片信息、标签特征千万级中等频率读,低频写,单租户数据量差异极大中,按租户隔离,大租户数据量超百万行
日志域操作审计日志、系统运行日志、行为埋点十亿级高频写,低频读,归档特征明显低,按租户 + 时间隔离,仅用于审计与回溯

从读写特征来看,系统 90% 的业务查询都带着 tenant_id 租户标识,99% 的事务操作都集中在单个租户内部,跨租户的查询仅存在于平台侧的运营统计场景,这为我们的分片键选型提供了核心依据。

2. 传统单库架构的核心痛点

随着租户规模与数据量的爆发式增长,传统单库单表架构彻底无法支撑业务发展,暴露了六大核心痛点:

  1. 性能瓶颈严重,查询效率暴跌:核心的内容发布记录表单表数据量突破 1 亿行,即使加了索引,普通租户的分页查询耗时也超过 2 秒,大租户的复杂查询耗时超 10 秒,完全无法满足业务实时性要求。
  2. 数据库连接数耗尽,高峰期业务卡顿:单库的最大连接数存在物理上限,大促高峰期并发请求量增长 5 倍,数据库连接数直接打满,新请求无法建立连接,导致全平台业务卡顿。
  3. 租户间性能互相干扰,无隔离性:单库架构下,所有租户的请求都落在同一个数据库实例上,单个大租户的批量查询、大事务操作,会直接占用所有数据库资源,导致其他中小租户的业务请求超时,出现 “一租户闹,全平台宕” 的严重问题。
  4. 表结构变更风险极高,锁表时间长:单表单亿行数据的场景下,即使是简单的加字段操作,也会导致表级锁,锁表时间长达数十秒,期间业务无法写入,只能在凌晨低峰期变更,迭代效率极低,且存在极高的线上风险。
  5. 数据备份与恢复困难,运维成本极高:单库数据量超 5TB,全量备份耗时超 10 小时,恢复耗时超 20 小时,一旦出现数据故障,业务中断时间完全不可控;同时无法针对单个租户做数据备份与恢复,无法满足企业租户的合规要求。
  6. 扩容能力完全受限,无法支撑规模化增长:单库架构只能通过提升服务器配置做垂直扩容,而硬件配置存在物理上限,成本呈指数级增长,完全无法支撑未来数万级租户的规模化增长目标。

3. 多租户场景下分库分表的核心挑战

和通用的互联网业务分库分表相比,多租户 SaaS 场景的分库分表设计,面临着更复杂的核心挑战,也是我们架构设计必须解决的核心问题:

  1. 租户数据量严重不均衡,数据倾斜问题突出:头部企业租户的单租户数据量超千万行,而大量免费租户的单租户数据量不足百行,数据量差异超 10 万倍,普通的哈希分片会导致严重的数据倾斜,部分分片压力过大,部分分片资源闲置。
  2. 租户隔离性与资源利用率的平衡难题:企业租户要求数据物理隔离,独享数据库资源,而中小租户更关注成本,愿意共享资源,如何在一套架构中同时满足不同等级租户的隔离要求,同时最大化资源利用率,是核心设计难点。
  3. 跨分片查询与事务一致性的矛盾:分库分表后,跨分片的关联查询、聚合统计性能极差,而跨分片的分布式事务又会带来性能损耗与数据一致性风险,如何在业务设计中规避跨分片操作,同时满足平台侧的统计需求,是必须解决的问题。
  4. 平滑扩容与不停机数据迁移的高要求:SaaS 系统要求 7*24 小时高可用,不允许长时间停服扩容,如何在业务无感知的情况下,完成数据分片扩容与数据迁移,同时保障数据一致性,是架构设计的硬性要求。
  5. 业务代码无侵入,降低研发与维护成本:分库分表改造不能让业务研发人员感知,不能要求业务代码手动处理分片路由、租户过滤,否则会出现大量重复代码,极易出现租户过滤遗漏导致的越权安全漏洞。

二、分库分表整体架构设计

针对上述核心痛点与挑战,我们摒弃了 “一刀切” 的分片方案,设计了 **「租户等级混合隔离 + 业务域垂直分库 + 场景化水平分表」的三层混合架构 **,既满足了不同等级租户的隔离要求,又解决了海量数据的性能与扩容问题,同时最大限度降低了对业务代码的侵入性。

核心架构设计原则

在架构设计之初,我们制定了六大核心原则,所有设计都严格遵循这些原则,避免为了技术炫技而过度设计:

  1. 租户隔离优先原则:架构设计的第一优先级是保障租户数据安全,企业租户必须实现物理隔离,中小租户必须实现逻辑强隔离,从架构层面杜绝跨租户数据越权风险。
  2. 业务语义优先原则:分片规则完全贴合业务访问模式,分片键的选择必须与 90% 的高频查询路径强相关,避免出现大量不带分片键的查询,导致全分片扫描的性能灾难。
  3. 业务无侵入原则:分片路由逻辑完全由中间件处理,业务代码只需使用逻辑表名,无需关注底层数据存储位置,研发人员无需感知分库分表的存在,降低研发与维护成本。
  4. 可平滑扩容原则:分片架构必须支持不停机平滑扩容,扩容时仅需迁移少量数据,无需全量重哈希,避免业务停服,支撑未来数万级租户的规模化增长。
  5. 能不分就不分原则:分库分表会引入额外的架构复杂度,对于数据量小、低频访问的表,优先不做拆分;能通过垂直分库解决的问题,不做水平分表;能通过单库分表解决的问题,不做多库分片。
  6. 高可用与可观测原则:分库分表架构必须配套完整的监控、告警、运维体系,全链路可观测,同时支持读写分离、主从切换,保障数据库层的高可用。

整体架构分层

整套分库分表架构从上到下分为 5 层,各层职责单一、完全解耦,通过 Sharding-JDBC 实现了对业务代码的完全透明,同时配套了完整的治理能力。

架构层级核心组件核心职责
接入层租户上下文拦截器、动态数据源路由、分片参数校验负责请求入口的租户上下文初始化,校验分片键完整性,为下层路由提供基础上下文,同时拦截非法的无分片键请求
核心执行层Sharding-JDBC、SQL 解析引擎、分片路由引擎、SQL 执行器、结果归并引擎架构的核心,负责拦截 JDBC 请求,解析 SQL 语句,根据分片规则计算路由目标库表,将 SQL 转发到对应库表执行,最后合并执行结果返回给业务代码,对业务完全透明
存储层MySQL 主从集群、垂直分库实例、水平分片库、冷热归档库负责数据的物理存储,按业务域垂直分库,按租户等级与场景做水平分片,同时配套读写分离架构,主库写、从库读,分摊数据库压力
事务保障层单分片本地事务、Seata AT 分布式事务、最终一致性消息队列负责事务一致性保障,优先将事务控制在单个分片内,确需跨分片的场景,通过 Seata AT 模式保障强一致性,非核心场景通过消息队列保障最终一致性
治理运维层分片配置中心、不停机数据迁移工具、监控告警平台、数据备份恢复体系负责分片规则的动态配置、数据扩容迁移、全链路指标监控、数据备份与恢复,降低架构的运维复杂度

分片策略选型与落地

结合多租户业务场景,我们采用了混合分片策略,针对不同租户等级、不同业务域的表,采用差异化的分片方案,而非统一的哈希分片。

1. 第一层:租户等级混合隔离架构

我们根据租户的套餐等级,将租户分为三大类,采用差异化的隔离方案,在保障隔离性的同时,最大化资源利用率:

  • 企业版租户:独立数据库实例:每个付费企业版租户独享一个独立的 MySQL 数据库实例,实现最高级别的物理隔离。租户的所有数据都存储在独立实例中,与其他租户完全隔离,不会受到其他租户业务的干扰,同时支持独立的备份恢复、资源配置、合规审计,满足中大型企业的安全合规要求。
  • 专业版租户:共享实例,独立 Schema:专业版租户共享 MySQL 实例,但每个租户拥有独立的数据库 Schema,实现逻辑隔离。这种方案隔离级别较高,运维成本适中,同时避免了租户间的表名冲突,适合中等规模的租户。
  • 基础版 / 免费租户:共享实例,共享 Schema,行级隔离:基础版与免费租户共享数据库实例与 Schema,通过 tenant_id 字段做行级隔离,配合数据库行级安全策略(RLS)兜底,最大限度降低存储与运维成本,适合小规模租户。

同时,我们搭建了统一的租户 - 数据源路由映射表,租户创建时,根据套餐等级自动创建对应的数据库资源,注册到路由中心;租户套餐升级时,可通过数据迁移工具,平滑完成数据从共享库到独立库的迁移,业务无感知。

2. 第二层:按业务域垂直分库

我们将整个系统的数据库,按业务域拆分为 6 个独立的垂直分库,不同业务域的数据库部署在独立的服务器上,彻底解决了单库的连接数、性能瓶颈,同时实现了故障隔离,单个业务域的数据库故障不会影响其他业务域。

  • 租户管理库:存储租户信息、套餐配置、组织架构、权限体系等核心数据,数据量小,读写频率低,对一致性要求极高。
  • 账号管理库:存储平台账号、授权凭证、账号配置等数据,中等读写频率,对安全性要求极高。
  • 内容发布库:核心业务库,存储内容发布任务、发布记录、内容元数据等流水型数据,高频读写,数据量持续增长,是分库分表的核心重点。
  • 素材管理库:存储素材元数据、标签特征、分片信息等数据,读多写少,单租户数据量差异大。
  • 审计日志库:存储全量操作审计日志、系统运行日志,高频写,低频读,数据量极大,冷热特征明显。
  • 统计分析库:存储平台侧的运营统计数据、租户数据报表,用于后台管理与数据分析,避免统计查询影响核心业务库性能。

3. 第三层:场景化水平分表

针对垂直分库后,数据量依然持续增长的核心大表,我们根据表的业务特征、读写模式,采用差异化的水平分表策略,避免一刀切的哈希分片。

表类型代表表分片键分片算法设计思路
租户主表租户信息表、账号表tenant_id一致性哈希分片(虚拟槽)99% 的查询都带着 tenant_id,按租户分片确保单租户的所有数据都在同一个分片内,避免跨分片查询
流水型大表内容发布记录表、操作审计表tenant_id + 月份复合分片:先按 tenant_id 哈希分库,再按月份分表流水型数据持续增长,按时间分表便于后续冷热数据分离与历史数据归档,同时 tenant_id 作为第一分片键,确保单租户的查询只落在少数分片内
关联子表发布任务详情表、素材标签表tenant_id + 主表 ID绑定表分片和主表使用相同的分片键与分片算法,确保关联数据落在同一个分片内,避免跨分片 JOIN 查询
静态配置表平台参数表、模板配置表广播表数据量小,极少更新,所有分片都存储全量数据,避免跨分片关联查询

其中,核心的分片算法我们采用了1024 个虚拟槽的一致性哈希算法,而非简单的哈希取模。预设 1024 个固定的虚拟槽,每个物理分片承载若干个虚拟槽,租户创建时,通过 tenant_id 的哈希值计算对应的虚拟槽,映射到对应的物理分片。这种方案的核心优势在于,扩容时仅需迁移部分虚拟槽的数据,无需全量数据重哈希,对业务影响极小,完美解决了传统哈希取模的扩容难题。

核心分片算法代码示例:

java

运行

/**
 * 基于虚拟槽的一致性哈希分片算法
 */
public class TenantVirtualSlotShardingAlgorithm implements StandardShardingAlgorithm<String> {

    // 预设固定虚拟槽数量,2的整数次幂,便于扩容
    private static final int VIRTUAL_SLOT_COUNT = 1024;
    // 虚拟槽与物理分片的映射关系,从配置中心动态加载,支持热更新
    private volatile Map<Integer, String> slotToDataSourceMap;

    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
        // 1. 计算租户ID对应的虚拟槽
        String tenantId = shardingValue.getValue();
        int slot = Math.abs(tenantId.hashCode() & Integer.MAX_VALUE) % VIRTUAL_SLOT_COUNT;
        // 2. 从映射关系中获取对应的物理数据源
        return slotToDataSourceMap.get(slot);
    }

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<String> shardingValue) {
        // 范围查询处理,仅允许同租户的范围查询,返回对应租户的分片
        String lowerTenantId = shardingValue.getValueRange().lowerEndpoint();
        String upperTenantId = shardingValue.getValueRange().upperEndpoint();
        if (lowerTenantId.equals(upperTenantId)) {
            return Collections.singletonList(doSharding(availableTargetNames, new PreciseShardingValue<>(
                    shardingValue.getLogicTableName(), shardingValue.getColumnName(), lowerTenantId
            )));
        }
        // 禁止跨租户的全分片范围查询,抛出异常拦截非法请求
        throw new UnsupportedOperationException("跨租户范围查询被禁止,请携带tenant_id分片键");
    }

    /**
     * 监听配置中心变更,动态更新槽位映射关系,支持不停机扩容
     */
    @EventListener
    public void onSlotMappingChange(SlotMappingChangeEvent event) {
        this.slotToDataSourceMap = event.getNewSlotMapping();
    }
}

三、核心技术模块的工程化实现

1. 无侵入式分片路由与租户上下文体系

为了实现对业务代码的零侵入,我们基于之前权限体系的租户上下文,结合 Sharding-JDBC,搭建了一套全链路租户上下文传递与自动分片路由体系,业务研发人员无需关注分片逻辑,只需像使用单库一样编写业务代码。

核心实现流程:

  1. 请求入口上下文初始化:通过 Spring MVC 拦截器,在请求进入业务逻辑前,解析 JWT 令牌中的租户 ID,写入 TenantContextHolder 线程本地变量,同时通过 Dubbo RPC 过滤器、自定义线程池,实现微服务之间、异步线程之间的上下文全链路传递,确保上下文不丢失。
  2. 分片键自动注入:通过 MyBatis 拦截器,拦截所有执行的 SQL,自动为 WHERE 子句注入当前租户的 tenant_id 分片键,即使开发人员遗漏了租户条件,也会自动补全,从根源上避免跨租户数据查询与全分片扫描。
  3. SQL 透明路由:Sharding-JDBC 拦截 JDBC 请求,解析 SQL 语句,根据我们配置的分片规则,自动计算目标库表,将 SQL 转发到对应的物理库表执行,最后合并执行结果,对业务代码完全透明。
  4. 非法请求拦截:在分片路由层,我们增加了校验逻辑,对于核心业务表的查询,必须携带 tenant_id 分片键,否则直接抛出异常,拦截全分片扫描的非法请求,避免数据库性能灾难。

2. 分布式事务与数据一致性保障

分库分表后,最大的难题之一就是数据一致性问题。我们遵循「单分片事务优先,跨分片事务尽量避免,不可避免时最小化」的原则,设计了三层事务保障体系。

  1. 单分片本地事务(优先) :我们通过分片规则设计,将同一个租户的所有关联数据都落在同一个分片内,99% 的业务事务都可以控制在单个分片内,直接使用 MySQL 本地事务保障 ACID 特性,性能最高,无一致性风险。
  2. 跨分片强一致性事务(Seata AT 模式) :对于平台侧的少数跨分片事务场景,比如批量统计、租户套餐变更等,我们采用 Seata AT 模式实现分布式事务。AT 模式基于两阶段提交,对业务代码无侵入,性能损耗低,同时保证了数据的最终一致性,满足我们的业务需求。
  3. 最终一致性事务(本地消息表) :对于非核心的跨分片异步场景,比如日志同步、数据归档等,我们采用「本地消息表 + RocketMQ」的方案,保障数据的最终一致性,避免分布式事务带来的性能损耗。

同时,我们制定了严格的开发规范:禁止在业务代码中编写跨租户的事务操作,禁止大跨度的跨分片事务,所有跨分片操作必须经过架构组评审,最大限度降低分布式事务带来的风险。

3. 不停机平滑扩容与数据迁移体系

为了支撑业务的规模化增长,我们设计了一套完整的不停机数据迁移与平滑扩容方案,可在业务无感知的情况下,完成分片扩容与数据迁移,全程无需停服,同时保障数据一致性。

核心扩容流程分为 6 个阶段,全程可监控、可回滚:

  1. 准备阶段:新增新的分片数据库实例,配置好主从同步、读写分离,更新虚拟槽与分片的映射关系,配置到配置中心,此时新的映射关系暂不生效。
  2. 双写阶段:开启数据双写模式,所有新增、修改、删除的数据,会同时写入旧分片与新分片,同时启动全量数据同步工具,将旧分片的历史数据批量同步到新分片,同步过程中通过主键幂等性去重,确保数据一致性。
  3. 数据校验阶段:全量同步完成后,通过数据校验工具,逐行比对旧分片与新分片的数据,校验数据的完整性与一致性,对于不一致的数据,自动修复补全,确保两边数据完全一致。
  4. 路由切换阶段:确认数据完全一致后,在配置中心动态更新虚拟槽映射关系,分片路由引擎监听到配置变更后,动态刷新路由规则,将请求切换到新的分片,切换过程毫秒级完成,业务无感知。
  5. 双写关闭阶段:路由切换完成后,持续观察业务运行情况,确认新分片的读写正常,无异常请求,持续 24 小时无问题后,关闭双写模式,所有读写都指向新分片。
  6. 旧数据清理阶段:观察一周后,确认业务完全正常,无数据问题,清理旧分片的迁移数据,完成整个扩容流程。

同时,我们开发了可视化的扩容运维平台,全程自动化执行,支持进度实时查看、异常自动暂停、一键回滚,大幅降低了扩容的运维复杂度,即使是零基础的运维人员也能完成操作。

4. 冷热数据分离与归档策略

针对流水型大表持续增长的问题,我们设计了冷热数据分离与自动归档体系,将热数据留在高性能的主库,冷数据归档到低成本的归档库,既保障了热数据的查询性能,又降低了存储成本。

核心实现逻辑:

  1. 冷热数据定义:我们将最近 3 个月的业务数据定义为热数据,用户高频访问;3 个月以上的数据定义为冷数据,仅用于审计回溯,访问频率极低。
  2. 自动归档流程:基于时间分表的设计,我们通过定时任务,每月自动将 3 个月前的历史分表数据,通过 Canal 同步到归档库,同步完成后,校验数据一致性,确认无误后,将主库的历史分表转为归档表,迁移到低成本的存储服务器。
  3. 查询自动路由:用户查询历史数据时,Sharding-JDBC 会根据查询的时间范围,自动路由到对应的热库或归档库,对业务代码完全透明,无需修改查询逻辑。
  4. 超期数据自动清理:对于超过 1 年的审计日志、埋点数据,自动备份到对象存储后,清理数据库中的数据,进一步降低存储成本。

5. 全链路性能优化

为了最大化发挥分库分表的性能优势,我们做了多维度的性能优化,将核心接口的平均响应时间稳定在 50ms 以内。

  1. 绑定表与广播表优化:将主表与子表设置为绑定表,使用相同的分片键与分片算法,确保关联数据落在同一个分片内,避免跨分片 JOIN;将数据量小、极少更新的配置表设置为广播表,所有分片都存储全量数据,消除跨分片关联查询。
  2. 读写分离与负载均衡:所有分片都采用一主多从的主从架构,Sharding-JDBC 自动实现读写分离,写请求路由到主库,读请求负载均衡到从库,将主库的读压力分摊到从库,主库 CPU 利用率降低了 60% 以上。
  3. 索引与 SQL 优化:针对分片后的查询场景,为所有高频查询建立合适的联合索引,分片键必须作为联合索引的第一个字段,避免全表扫描;同时禁止使用 SELECT *、禁止不带分片键的查询、禁止大跨度的跨分片查询,从开发规范层面规避慢 SQL。
  4. 多级缓存配合:分库分表必须配合多级缓存使用,我们将租户信息、账号配置、模板数据等静态数据,放入 Redis 分布式缓存与本地 Caffeine 缓存,将高频的查询结果放入 Redis 缓存,减少数据库的访问压力,进一步提升查询性能。
  5. 避免全分片聚合查询:对于平台侧的统计聚合需求,我们禁止直接在业务库做跨分片 COUNT、SUM 等聚合操作,而是通过 Flink 实时数仓,将分片数据同步到 Doris OLAP 引擎,在 OLAP 引擎中做统计分析,既提升了查询性能,又避免了统计查询影响业务库性能。

四、线上踩坑复盘与优化方案

在分库分表架构的落地与上线过程中,我们踩过了很多典型的坑,这里做完整的复盘与解决方案分享,帮助同类场景避坑。

坑 1:分片键选型错误,导致大量全分片扫描,数据库性能雪崩

问题现象:上线初期,部分核心接口没有携带 tenant_id 分片键,导致 Sharding-JDBC 将查询请求转发到所有分片执行,出现全分片扫描。高峰期这类请求过多,导致所有分片的数据库 CPU 利用率飙升至 100%,业务接口大面积超时。根因分析

  1. 部分内部 RPC 接口、定时任务,没有传递租户上下文,导致 SQL 中没有 tenant_id 分片键;
  2. 平台侧的运营后台,部分查询场景需要查询全租户的数据,没有走 OLAP 引擎,直接在业务库查询,导致全分片扫描;
  3. 分片规则没有做强制校验,允许不带分片键的查询执行,没有拦截非法请求。解决方案
  4. 重构分片路由逻辑,增加强制分片键校验,核心业务表的查询必须携带 tenant_id 分片键,否则直接抛出异常,拦截全分片扫描请求,从根源上杜绝这类问题。
  5. 全量梳理代码,修复所有没有传递租户上下文的接口与定时任务,通过代码扫描工具,在开发阶段就拦截不带分片键的 SQL。
  6. 平台侧的全租户统计查询,全部迁移到 Doris OLAP 引擎执行,业务库仅处理单租户的业务查询,彻底消除跨分片聚合查询对业务库的影响。优化效果:优化后,再也没有出现过全分片扫描的请求,数据库平均 CPU 利用率从 80% 降至 20%,核心接口响应时间稳定在 50ms 以内。

坑 2:大租户数据倾斜,单个分片压力过大,性能瓶颈突出

问题现象:上线 3 个月后,我们发现少数几个头部企业租户所在的分片,数据库 CPU 利用率持续超过 90%,而其他分片的利用率不足 30%,出现了严重的数据倾斜问题,大租户的业务查询卡顿严重。根因分析

  1. 初期我们采用了简单的哈希取模分片,部分大租户恰好哈希到同一个分片,导致该分片的数据量与请求量是其他分片的 5 倍以上;
  2. 头部企业租户的单租户数据量超千万行,QPS 是普通租户的 100 倍,即使单独在一个分片,也会占用大量数据库资源,影响同分片的其他租户。解决方案
  3. 重构分片算法,采用基于虚拟槽的一致性哈希分片,同时支持大租户单独分片,将头部企业租户单独分配到独立的虚拟槽与独立的数据库实例,独享资源,完全隔离,不会影响其他租户。
  4. 实现分片负载均衡调度,定期监控各分片的数据量与请求量,当出现数据倾斜时,自动给出扩容建议,通过迁移虚拟槽的方式,将高负载分片的部分租户迁移到低负载分片,平衡各分片的压力。
  5. 针对大租户的超大表,在 tenant_id 分片的基础上,再按时间做二次分表,进一步降低单表的数据量,提升查询性能。优化效果:优化后,各分片的负载均衡度达到 90% 以上,无数据倾斜问题,头部租户的查询性能提升了 10 倍,不再出现单分片压力过大的问题。

坑 3:跨分片 JOIN 查询滥用,导致接口性能极差,频繁超时

问题现象:上线初期,部分业务代码编写了跨分片的 JOIN 查询,比如关联查询不同分片的账号表与发布记录表,这类查询的响应时间超过 5 秒,高峰期频繁超时,甚至导致数据库连接耗尽。根因分析

  1. 开发人员没有理解分库分表的核心逻辑,沿用了单库的开发习惯,随意编写跨分片 JOIN 查询;
  2. 关联表没有设置为绑定表,分片键不一致,导致关联查询必须在所有分片执行后再内存归并,性能极差;
  3. 没有在开发规范中禁止跨分片 JOIN,也没有对应的拦截校验机制,导致这类 SQL 上线。解决方案
  4. 制定严格的开发规范,明确禁止业务代码编写跨分片 JOIN 查询,所有关联查询必须控制在单个分片内;对于确需关联的表,必须设置为绑定表,使用相同的分片键,确保关联数据在同一个分片内。
  5. 重构所有跨分片 JOIN 的代码,采用「单分片查询 + 应用层内存组装」的方式替代,先在主表分片查询出主数据,再根据关联键到对应分片查询关联数据,在应用层组装结果,性能提升 10 倍以上。
  6. 通过 SQL 拦截器,拦截跨分片 JOIN 的 SQL,开发环境直接抛出异常,生产环境记录告警日志,从流程上杜绝这类 SQL 上线。优化效果:优化后,彻底消除了跨分片 JOIN 查询,相关接口的响应时间从 5 秒以上降至 100ms 以内,数据库连接数压力大幅降低。

坑 4:分布式事务导致的锁等待与数据不一致问题

问题现象:上线初期,部分跨分片的分布式事务,出现了锁等待超时的问题,甚至出现了事务提交失败导致的数据不一致,严重影响了业务正常运行。根因分析

  1. 我们在业务代码中滥用了分布式事务,很多可以控制在单分片内的事务,也使用了跨分片分布式事务,导致事务链路过长,锁等待时间增加;
  2. 大事务跨多个分片执行,执行时间过长,导致行锁长时间不释放,出现大量锁等待超时;
  3. Seata 的配置不合理,全局事务超时时间过短,部分慢事务还未执行完成,就触发了回滚,导致数据不一致。解决方案
  4. 全面梳理业务代码,将所有可以控制在单分片内的事务,全部改为本地事务,仅保留极少数必须跨分片的核心场景使用分布式事务,分布式事务数量减少了 95%。
  5. 拆分大事务,将长事务拆分为多个短事务,避免长时间持有行锁;同时禁止在事务中执行远程调用、慢查询,确保事务执行时间控制在 100ms 以内。
  6. 优化 Seata 配置,调整全局事务超时时间,优化 TC 服务的集群部署,提升分布式事务的处理性能;同时增加了事务异常告警,出现事务回滚时立即通知相关人员排查。
  7. 增加了分布式事务的幂等性处理,避免重复提交、空回滚、事务悬挂等问题,确保数据一致性。优化效果:优化后,再也没有出现过分布式事务锁等待超时的问题,事务执行成功率达到 100%,无数据不一致问题。

五、性能测试与落地效果

这套分库分表架构目前已在星链引擎中全量上线,稳定运行超过 1 年,支撑了租户规模从数千家到数万家的规模化增长,经过多次大促峰值场景的验证,核心性能与业务效果均达到了设计预期。

核心性能指标

性能指标优化前(单库单表)优化后(分库分表)提升幅度
核心接口平均响应时间800ms45ms提升 17 倍
数据库 TPS 峰值200020000提升 10 倍
单表最大数据量1.2 亿行800 万行降低 93%
表结构变更耗时30s+<1s降低 99%
全量数据备份耗时12 小时30 分钟降低 96%
数据恢复 RTO20 小时30 分钟降低 97.5%
数据库平均 CPU 利用率75%20%降低 73%

业务落地收益

  1. 彻底解决了系统性能瓶颈,支撑了业务规模化增长:分库分表架构彻底解决了单库的性能上限问题,核心接口响应时间提升了 17 倍,数据库 TPS 峰值提升了 10 倍,完美支撑了租户规模从数千家到数万家的爆发式增长,无需担心数据库架构的扩容问题。
  2. 实现了租户间的完全隔离,消除了业务干扰:通过租户等级混合隔离架构,企业租户独享数据库资源,完全不会受到其他租户的影响,彻底解决了 “一租户闹,全平台宕” 的问题,产品的企业级能力得到了客户的高度认可。
  3. 大幅降低了研发与运维成本:无侵入式的分片路由设计,业务研发人员无需关注分库分表逻辑,开发效率提升了 70%;垂直分库实现了故障隔离,单个业务域的数据库故障不会影响其他业务,同时自动化的扩容、备份、归档工具,让 DBA 的运维工作量降低了 80%。
  4. 全面提升了系统的高可用与合规能力:分库分表架构配套的读写分离、主从切换、多副本架构,让系统的可用性从 99.9% 提升至 99.99%;同时针对单个租户的独立备份恢复能力,满足了企业客户的等保合规、数据安全要求,成为了产品的核心竞争力之一。

六、总结与未来规划

对于企业级多租户 SaaS 系统而言,分库分表从来不是一个简单的技术优化,而是从业务场景出发,结合数据特征、读写模式、租户需求做的系统性架构设计。我们没有盲目套用通用的分库分表方案,而是针对多租户场景的核心痛点,设计了混合隔离的分库分表架构,既解决了单库架构的性能瓶颈,又兼顾了租户隔离、业务无侵入、平滑扩容、运维可控等核心需求。

本文所分享的架构设计、技术实现、踩坑复盘,不仅适用于内容管理 SaaS 场景,也可以复用到企业服务、电商、教育、金融等各类多租户 SaaS 系统的数据库架构设计中,具备极强的通用性与可复用性。

未来,我们会持续迭代优化这套数据库架构,核心聚焦于四个方向:

  1. 国产化适配升级:完成国产化操作系统、国产化数据库(达梦、人大金仓、OceanBase)的全栈适配,支持国密加密算法,满足政企客户的国产化合规要求。
  2. 冷热数据湖仓一体架构:引入 Apache Iceberg 数据湖,将冷数据归档到对象存储,通过 Flink 与 Doris 实现湖仓一体查询,既降低了冷数据的存储成本,又实现了冷热数据的统一查询分析。
  3. Serverless 化架构改造:基于云原生 Serverless 数据库,重构分片架构,实现计算与存储资源的按需付费、自动扩缩容,彻底解决峰值与低峰的资源调度问题,进一步降低使用成本。
  4. 智能化运维与自治:基于 AI 大模型,实现慢 SQL 自动优化、分片负载自动均衡、数据倾斜自动治理、故障自动恢复的数据库自治能力,进一步降低运维成本,提升系统的稳定性。