WebFlux + R2DBC 场景下的分库分表预研:从架构选型到落地风险

1. 为什么要预研分库分表

在业务早期,单库单表通常是最简单、最稳定、成本最低的方案。

但当系统中出现一些高频写入的大表时,单库单表会逐渐暴露问题。例如:

  • 查询越来越慢。
  • 索引越来越大。
  • 写入和查询互相影响。
  • 大表 DDL 风险变高。
  • 历史数据归档困难。
  • 慢 SQL 优化空间越来越小。
  • 单库资源扩容成本越来越高。

典型的大表包括:

  • 消息流水表。
  • 设备事件表。
  • 操作日志表。
  • 交易明细表。
  • 监控采集表。
  • 时序统计表。
  • 审计记录表。

这类表的共同特点是:

  • 数据持续增长。
  • 写入频率高。
  • 查询经常带时间范围。
  • 数据天然有业务维度,例如设备、用户、租户、组织。
  • 最近数据访问频繁,历史数据访问较少。

所以,分库分表预研的核心问题不是"怎么拆表",而是:

text 复制代码
系统的核心查询和写入模型,是否适合被稳定地路由到少量分片。

如果不能精准路由,分库分表很可能只是把一个单表慢查询,变成多个分片上的广播查询。

2. 什么时候需要考虑分库分表

分库分表不是越早越好。过早引入会增加开发、测试和运维复杂度。

通常可以从以下维度判断是否需要进入预研阶段。

2.1 数据量维度

可以重点观察:

  • 单表行数是否已经达到千万级甚至更高。
  • 表数据文件和索引文件是否持续膨胀。
  • 历史数据是否明显多于热数据。
  • 大表备份、恢复、DDL 是否已经变慢。

数据量只是一个信号,不是唯一标准。

有的表几千万行但查询简单、索引合理,仍然可以稳定运行;有的表几百万行,但查询复杂、索引设计差,也可能很慢。

2.2 查询维度

重点关注:

  • 是否出现大量慢 SQL。
  • 是否有大 offset 分页。
  • 是否频繁按时间范围查询。
  • 是否经常只查最近一段时间数据。
  • 是否存在不带高选择性条件的模糊查询。
  • 是否存在多字段动态组合查询。

如果慢查询主要来自不合理 SQL、缺索引、隐式类型转换、函数导致索引失效,那么应该先做 SQL 和索引优化,不要直接上分库分表。

2.3 写入维度

重点关注:

  • 写入吞吐是否接近单库瓶颈。
  • 高频写入是否导致索引维护成本过高。
  • 写入是否影响查询。
  • 是否有批量写入、异步写入、削峰空间。

如果只是瞬时峰值,可以先考虑:

  • 批量写入。
  • 队列削峰。
  • 异步落库。
  • 读写分离。
  • 表分区或归档。

2.4 运维维度

分库分表会引入新的运维问题:

  • 分片元数据管理。
  • 分片扩容。
  • 数据迁移。
  • Proxy 高可用。
  • 分布式事务。
  • 跨分片聚合。
  • 监控和告警。

如果团队没有足够的运维和排障能力,建议先通过更保守的方式优化。

3. 分库分表前可以先做哪些优化

在正式拆表前,可以先做一轮低成本优化:

  1. 补齐必要索引。
  2. 清理无效索引。
  3. 消除隐式类型转换。
  4. 避免函数作用在索引列上。
  5. 大分页改成游标分页。
  6. 冷热数据归档。
  7. 读写分离。
  8. 报表查询走汇总表。
  9. 搜索类查询走搜索引擎。
  10. 高频统计走预聚合表。

如果这些手段已经不能满足需求,再考虑分库分表。

4. 目标表如何选择

不是所有表都适合分库分表。

优先处理:

  • 写入量最大的流水表。
  • 数据增长最快的历史表。
  • 查询压力最集中的核心表。
  • 历史数据占比高且可按时间归档的表。

不建议优先处理:

  • 配置表。
  • 字典表。
  • 状态变化不频繁的主数据表。
  • 数据量不大的关联表。
  • 查询不在核心链路上的辅助表。

一个常见策略是:

text 复制代码
核心大表:分库分表
配置字典表:保持单表
报表统计:汇总表或物化视图
搜索筛选:搜索引擎或索引表
历史数据:归档或冷热分层

5. 分片维度设计

分片维度通常要从查询模型倒推。

5.1 时间维度

高频流水表大多天然适合按时间拆分。

常见粒度:

text 复制代码
按天:yyyyMMdd
按周:yyyy_WW
按月:yyyyMM
按季度:yyyy_Q

按时间拆分的好处:

  • 历史归档方便。
  • 查询最近数据时扫描范围小。
  • 按时间删除旧数据成本低。
  • 单表大小可控。

缺点:

  • 时间范围过大时会跨多张表。
  • 表数量会随着时间增长。
  • 需要处理跨表聚合和分页。

一般建议从按月或按周开始,极高频写入场景再考虑按天。

5.2 业务维度

除了时间,还需要选择一个能稳定路由的业务维度。

常见选择:

text 复制代码
device_id
user_id
tenant_id
org_id
account_id
order_id

选择分片键时要看:

  • 核心查询是否一定带这个字段。
  • 字段分布是否均匀。
  • 是否容易产生热点。
  • 关联写入是否能落到同一分片。
  • 后续扩容和迁移成本是否可控。

不适合作为分片键的字段:

  • 状态。
  • 类型。
  • 布尔值。
  • 低基数字段。
  • 经常为空的字段。

5.3 时间 + 哈希组合

对于高频流水表,一个比较常见的设计是:

text 复制代码
t_message_yyyyMM_hashN

例如:

text 复制代码
t_message_202601_00
t_message_202601_01
t_message_202602_00
t_message_202602_01

路由逻辑:

text 复制代码
先根据时间确定月份表组
再根据业务 ID hash 到具体分片

这样可以同时控制:

  • 单表数据量。
  • 单分片写入压力。
  • 历史归档范围。

6. 分片算法

6.1 哈希取模

最常见的算法:

text 复制代码
hash(sharding_key) % N

优点:

  • 实现简单。
  • 数据分布相对均匀。
  • 同一个分片键能稳定落到同一分片。

缺点:

  • N 变化后,大量数据需要迁移。
  • 扩容成本较高。

适合初期分片数量相对稳定的场景。

6.2 一致性哈希

一致性哈希适合需要后续扩容的场景。

优点:

  • 扩容时迁移的数据量相对较少。
  • 可通过虚拟节点改善分布。

缺点:

  • 实现和运维复杂度更高。
  • 对中间件支持情况有要求。

6.3 范围分片

范围分片常用于时间字段。

例如:

text 复制代码
2026-01 -> t_message_202601
2026-02 -> t_message_202602

优点:

  • 便于历史数据清理。
  • 查询时间范围时容易裁剪分片。

缺点:

  • 当前时间分片可能成为写入热点。
  • 时间范围过大时会跨很多表。

7. 技术方案选型

7.1 ShardingSphere-JDBC

ShardingSphere-JDBC 是应用内嵌式方案。

优点:

  • 功能完整。
  • 生态成熟。
  • 支持分库分表、读写分离、分布式事务、数据加密等能力。
  • 文档和案例较多。

不足:

  • 主要面向 JDBC 生态。
  • 对纯 R2DBC 项目不如 Proxy 自然。
  • 应用侧引入成本更高。

7.2 ShardingSphere-Proxy

对于 WebFlux + R2DBC 场景,更推荐优先评估 ShardingSphere-Proxy。

架构形态:

text 复制代码
应用服务 -> R2DBC Driver -> ShardingSphere-Proxy -> 后端数据库

应用仍然以数据库方式连接,只是连接目标从真实数据库变成了 Proxy。

优点:

  • 对应用侵入较小。
  • 对 R2DBC 项目更友好。
  • 分片规则集中管理。
  • 可以统一处理读写分离和分片路由。
  • 后续数据库迁移时,应用侧改动相对可控。

风险:

  • Proxy 本身成为关键链路。
  • 需要额外部署和监控。
  • 跨分片聚合仍然有成本。
  • 复杂 SQL 兼容性需要压测验证。

7.3 自研路由

自研路由通常是应用内根据业务参数计算物理库表。

示例:

text 复制代码
业务参数 -> 路由规则 -> DataSource / ConnectionFactory -> 物理表名

优点:

  • 性能可控。
  • 逻辑贴合业务。
  • 可以针对特殊查询做深度优化。

缺点:

  • 研发和维护成本高。
  • 跨分片查询要自己处理。
  • 扩容和迁移要自己处理。
  • 事务边界更复杂。

除非团队有明确的中间件维护能力,否则不建议一开始就完全自研。

8. 推荐架构

更稳妥的落地路线是:

text 复制代码
ShardingSphere-Proxy + 汇总表 + 索引表 + 冷热归档

职责划分:

  • Proxy 负责普通分片路由。
  • 汇总表负责报表统计。
  • 索引表负责非分片键查询。
  • 搜索引擎负责复杂检索。
  • 冷热归档负责历史数据生命周期。

这样能避免把所有复杂度都压在分库分表中间件上。

9. 查询改造重点

分库分表后,查询模型需要同步改造。

9.1 核心查询必须带分片键

理想查询:

sql 复制代码
WHERE device_id = ?
  AND create_time >= ?
  AND create_time < ?

这类查询可以根据设备和时间精准路由。

不理想查询:

sql 复制代码
WHERE status = ?
ORDER BY create_time DESC
LIMIT 20

这种查询没有分片键,容易广播到所有分片。

9.2 非分片键查询需要辅助结构

如果业务确实需要按非分片键查,例如状态、手机号、外部编号,可以考虑:

  • 索引表。
  • 搜索引擎。
  • 汇总表。
  • 缓存映射。

例如:

text 复制代码
external_no -> sharding_key

先通过索引表找到分片键,再查询真实分片表。

9.3 跨分片分页要谨慎

传统分页:

sql 复制代码
LIMIT offset, size

跨分片后可能变成每个分片都取 offset + size 条,再聚合排序。

offset 越大,性能越差。

建议改成游标分页:

sql 复制代码
WHERE create_time < last_create_time
ORDER BY create_time DESC
LIMIT 20

或者:

sql 复制代码
WHERE id > last_id
ORDER BY id
LIMIT 20

9.4 排序字段要稳定

跨分片排序时,单独按时间排序可能不稳定。

建议使用组合排序:

sql 复制代码
ORDER BY create_time DESC, id DESC

这样可以避免同一时间点多条数据时分页重复或丢失。

10. 写入改造重点

10.1 全局唯一 ID

分库分表后,不建议依赖数据库自增主键。

常见方案:

  • UUID。
  • 雪花算法。
  • 号段模式。

建议优先考虑雪花算法或号段模式。

UUID 虽然简单,但作为主键时通常不够友好:

  • 无序。
  • 索引膨胀。
  • 写入局部性差。

10.2 批量写入

批量写入需要注意:

  • 单批大小。
  • 参数数量限制。
  • 连接池占用。
  • 事务范围。
  • 失败重试。

如果一批数据会落到多个分片,需要评估是否拆批:

text 复制代码
按目标分片分组 -> 分批写入 -> 单分片事务

10.3 幂等写入

分库分表后,重复写入和补偿写入更常见。

建议设计:

  • 全局唯一业务 ID。
  • 唯一索引。
  • 幂等插入。
  • 可重试的补偿逻辑。

11. 事务问题

分库分表后,单库事务不一定能覆盖完整业务操作。

要优先通过设计规避分布式事务:

  • 同一业务聚合内的数据尽量落到同一分片。
  • 跨分片操作尽量拆成异步流程。
  • 能最终一致的场景,不强求强一致。
  • 核心强一致场景再评估分布式事务。

如果必须跨分片事务,可以评估:

  • XA。
  • BASE。
  • Seata。
  • 事务消息。
  • 本地消息表。

需要注意的是,分布式事务会带来性能和复杂度成本,不应作为默认方案。

12. 数据迁移方案

在线系统改造分库分表时,数据迁移是难点。

常见步骤:

  1. 建新分片表。
  2. 增加双写或变更捕获。
  3. 全量迁移历史数据。
  4. 增量同步新数据。
  5. 校验数据一致性。
  6. 灰度切读。
  7. 灰度切写。
  8. 保留回滚窗口。
  9. 下线旧表或归档。

校验维度包括:

  • 行数。
  • 主键集合。
  • 核心字段 hash。
  • 时间范围抽样。
  • 业务接口比对。

不要只看迁移任务成功,还要验证业务查询结果是否一致。

13. MySQL 切换 PostgreSQL 的注意点

如果系统后续可能从 MySQL 切换到 PostgreSQL,分库分表设计时要尽量减少数据库方言绑定。

13.1 驱动差异

R2DBC 驱动不同:

text 复制代码
MySQL: r2dbc-mysql
PostgreSQL: r2dbc-postgresql

如果使用 ShardingSphere-Proxy,应用连接 Proxy,后端数据库类型变化主要体现在 Proxy 配置和 SQL 方言兼容性上。

13.2 自增主键

MySQL:

sql 复制代码
AUTO_INCREMENT

PostgreSQL:

sql 复制代码
SERIAL
BIGSERIAL
GENERATED AS IDENTITY

分库分表场景建议使用分布式 ID,避免绑定数据库自增能力。

13.3 时间函数

MySQL:

sql 复制代码
NOW()
DATE_FORMAT()
UNIX_TIMESTAMP()

PostgreSQL:

sql 复制代码
now()
to_char()
extract()
date_trunc()

建议将时间计算统一封装,避免业务 SQL 到处散落数据库函数。

13.4 Upsert 语法

MySQL:

sql 复制代码
INSERT ... ON DUPLICATE KEY UPDATE

PostgreSQL:

sql 复制代码
INSERT ... ON CONFLICT (key) DO UPDATE

如果业务大量依赖幂等写,建议抽象统一写入接口,再按数据库方言生成 SQL。

13.5 JSON 差异

MySQL 和 PostgreSQL 的 JSON 能力差异较大。

PostgreSQL 的 jsonb、表达式索引、部分索引能力更强,但 SQL 写法不同。

建议:

  • 高频查询字段不要长期放在 JSON 中。
  • 核心过滤字段拆成普通列。
  • JSON 用于保存扩展属性。

13.6 索引能力

PostgreSQL 可以重点评估:

  • btree index。
  • partial index。
  • expression index。
  • BRIN index。

对于按时间递增的大表,BRIN 索引在某些范围查询中非常有价值。

13.7 事务隔离级别

MySQL InnoDB 默认通常是:

text 复制代码
Repeatable Read

PostgreSQL 默认是:

text 复制代码
Read Committed

如果业务依赖特定一致性语义,迁移前必须验证。

14. 压测和验证指标

分库分表不是配置完成就结束,必须压测。

建议至少验证:

  • 单写吞吐。
  • 批量写入吞吐。
  • 单分片查询耗时。
  • 跨分片查询耗时。
  • 分页查询耗时。
  • Proxy 路由耗时。
  • Proxy CPU 和内存。
  • 后端连接池使用率。
  • 慢 SQL 数量。
  • P95 / P99 响应时间。

建议对比三组数据:

text 复制代码
单库单表基线
分片后单分片查询
分片后跨分片查询

只有这样才能判断分库分表到底有没有实际收益。

15. 监控和运维

上线后需要监控:

  • Proxy 存活状态。
  • Proxy 路由耗时。
  • Proxy 后端连接数。
  • 数据库连接池。
  • 分片表数据量。
  • 分片热点。
  • 慢 SQL。
  • 跨分片查询数量。
  • 迁移任务状态。
  • 数据一致性校验结果。

分库分表后的系统,问题往往不再只发生在数据库,也可能发生在 Proxy、连接池、路由规则、应用 SQL 和数据迁移链路上。

16. 落地路线建议

比较稳妥的路线:

  1. 梳理核心大表和慢查询。
  2. 先做索引、SQL、归档和读写分离优化。
  3. 明确核心查询是否能带分片键。
  4. 设计时间分片和业务哈希分片。
  5. 搭建 ShardingSphere-Proxy 验证环境。
  6. 编写核心 SQL 兼容性测试。
  7. 做压测和容量评估。
  8. 设计迁移和回滚方案。
  9. 小流量灰度。
  10. 逐步切换读写流量。

不要一开始就全量切换。

17. 最终 checklist

分库分表落地前,建议确认:

  • 是否已经证明单库单表成为瓶颈。
  • 是否已经做过低成本优化。
  • 分片键是否稳定。
  • 核心查询是否都能带分片键。
  • 是否有跨分片查询兜底方案。
  • 是否有全局 ID 方案。
  • 是否避免了大 offset 分页。
  • 是否有数据迁移方案。
  • 是否有数据校验方案。
  • 是否有回滚方案。
  • 是否压测过核心链路。
  • 是否监控 Proxy 和后端数据库。
  • 是否评估过 MySQL / PostgreSQL 方言差异。

18. 总结

分库分表不是简单地把一张大表拆成多张小表,而是一整套数据架构改造。

它会影响:

  • 查询模型。
  • 写入模型。
  • 主键生成。
  • 事务边界。
  • 分页排序。
  • 数据迁移。
  • 运维监控。
  • 数据库迁移。

对于 WebFlux + R2DBC 场景,ShardingSphere-Proxy 是一个值得优先评估的方案,因为它能把分片能力下沉到代理层,降低应用侧侵入。

但无论选择 Proxy 还是自研路由,最关键的问题仍然是:

text 复制代码
能否通过分片键精准路由。

如果核心查询无法携带分片键,分库分表可能不会带来预期收益,反而会引入更多跨分片聚合、分页、事务和运维问题。

一句话总结:

text 复制代码
分库分表的价值,不在于"拆",而在于让核心读写能够稳定落到可控范围内。
相关推荐
键盘上的猫头鹰3 小时前
【从零学MySQL(二)】数据库基础操作、数据类型与约束(附Navicat演示)
数据库·mysql·数据分析·数据可视化
阮胜昌4 小时前
在CentOS 7.9上安装MySQL8.4.4 LTS
mysql·mysql8.4
岳麓丹枫0016 小时前
Windows 环境下创建 pgcrypto 扩展失败问题分析解决
windows·postgresql
码上谈兵6 小时前
一次 MySQL 连接池打满,我花一晚上重构了 Go 的数据库操作
mysql
丷丩7 小时前
Postgresql基础实践教程(五)
数据库·postgresql
夕除7 小时前
spring boot 13
java·mysql·spring
WAIT_TIME7 小时前
基于 Docker 快速构建 MySQL InnoDB Cluster 高可用集群与 Router 读写分离
mysql·docker·集群·innodb cluster
Full Stack Developme7 小时前
SQL like 与 正则 区别
数据库·sql·mysql
我是一颗柠檬7 小时前
【MySQL全面教学】MySQL多表查询与JOIN Day6(2026年)
数据库·后端·sql·mysql