自建MySQL到TDSQL:分布式数据库选型、分片键与迁移实践
1. 单机数据库在企业级场景里先撑不住的那几个瞬间
单机 MySQL 撑不住,往往不是"突然崩了",而是几个信号先后亮起红灯。我印象最深的一次是 2021 年给一家做 B 端 SaaS 的团队做巡检,他们的订单库 QPS 峰值只有 3000 出头,配置是 32C128G 的物理机,看上去远远没到极限,但监控上三个指标同时在报警:活跃连接数长期停在 2800 以上、binlog 同步延迟在夜间批量任务时飙到 90 秒、单表 order_detail 已经到了 6.2 亿行。这三件事单独看都不致命,叠在一起就成了死结——连接数打满导致新请求排队,主从延迟导致报表读到旧数据,大表 DDL 和二级索引维护把 IO 吃掉一大半。
1.1 三个红灯指标背后的真实瓶颈
连接数打满这件事,很多团队第一反应是调 max_connections,我见过有人直接调到 5000。这个操作基本等于把问题往后挪:MySQL 每个连接是一个线程,线程调度和内存开销随连接数线性上升,调到 5000 之后 CPU 的上下文切换会明显变高,表现就是"没有慢 SQL,但整体都慢了"。真正的解法是先在应用侧确认连接池配置——很多框架默认 maxPoolSize 给 20 甚至 50,几十个实例一叠就是上千连接。把连接池收紧到单实例 10 到 20,再配合连接复用,往往能直接砍掉一半连接数,这一步比换数据库便宜得多。
主从延迟的根因通常在写入的串行化程度。MySQL 的并行复制依赖组提交(group commit)的并发度,如果业务写入集中在少数几个热点行上,即使开了 slave_parallel_workers,实际并行度也上不去。这时候要么把大事务拆小(比如批量导入从 5 万行一次改成 2000 行一次),要么把串行写入的逻辑挪到更上层的队列里做削峰,要么接受延迟并让读请求走主库——最后这条在生产环境要慎用,因为读流量打回主库等于把扩展性又收回来了。
单表行数的问题更微妙。行数本身不直接决定性能,决定性能的是索引的层级和回表次数。B+ 树三层大概能放两千万行左右(取决于行长),超过之后树高变成四层,每一次主键查询多一次磁盘 IO。如果业务还有大量二级索引回表,这个代价会被放大好几倍。所以业内常说的"单表两千万"不是玄学,是一个基于树高的经验值,实际阈值要看你行长和索引宽度。
1.2 四条扩展路径,代价差异比想象中大
扛不住之后有四条路,我把它们的真实成本列一下,这些都是我在不同项目里踩过或旁观过的:
| 路径 | 适用拐点 | 主要代价 | 隐性坑 |
|---|---|---|---|
| 垂直升级硬件 | QPS 3000 到 10000 | 成本陡增,单机上限大概在 64C512G 左右 | 升级需要停机或主从切换,硬件价格非线性 |
| 读写分离 | 读多写少,读写比高于 8:1 | 引入主从延迟问题 | 强一致读场景仍需回主库 |
| 中间件分库分表 | 单表超过 5000 万行,写入可拆分 | 应用改造成本高,跨片查询要自己拼 | 分布式事务要自己实现,扩容极痛 |
| 原生分布式数据库 | 需要在线扩容、强一致、多租户 | 学习成本和运维方式变化 | 分片键选错代价很大 |
中间件方案我做过两次,最深的感觉是"头三个月很爽,第二年很难受"。ShardingSphere 这类中间件在 SQL 兼容性和事务处理上已经做得很成熟,但扩容时的一致性校验、跨片 JOIN 的性能、以及全局唯一 ID 的生成,这些都要业务自己兜。原生分布式数据库把这些能力下沉到了存储和计算层,代价是要接受一套新的使用范式,比如分片键一旦定了不好改,跨分片查询的性能和单机不在一个量级。
这也是为什么像腾讯云 TDSQL 这类企业级分布式数据库,会反复强调"兼容 MySQL 协议"和"透明分片"这两件事——它试图让业务改造成本接近零,同时把扩展、容灾、一致性这些难题收到数据库内部。这个思路对不对另说,但至少它回应了上面表格里最后两行那两列痛点。
2. TDSQL 的产品族谱:几套内核各自守着什么场景
TDSQL 不是一个单一产品,而是一个产品族。很多人第一次接触会懵:官网上一会儿 TDSQL,一会儿 TDSQL-C,一会儿 TDSQL-PG,到底哪个是哪个。我按自己的理解把它们的定位理一下,具体版本和命名请以官方文档为准,产品线会迭代。
2.1 分布式实例与云原生实例的分工
**TDSQL MySQL 版(分布式实例)**是大家通常说的那个"分布式数据库"。它的架构是典型的 share-nothing:数据按分片键水平拆分到多个 SET,每个 SET 内部是一主多备的强同步复制,跨 SET 的写入通过两阶段提交保证一致性,SQL 层做路由和结果归并。它解决的是"单库放不下、单机扛不住、还要强一致"这类问题,典型场景是金融账务、订单、交易流水。
TDSQL-C走的是另一条路:计算存储分离,多个计算节点共享一份分布式存储,一写多读。它解决的是"存储弹性"和"副本一致性"的问题——因为共享存储,读节点的数据天然和主节点一致,不存在传统主从延迟。适合读多写少、需要快速扩容只读节点、或者希望存储按量付费的业务。但它本质还是单写节点(单主),写入能力受限于单个计算节点的规格,所以它不是用来解决写入水平扩展的。
TDSQL-PG基于 PostgreSQL 内核,同样是分布式架构,适合那些原本就用 PG、依赖 PG 特有语法(比如丰富的 JSON 操作、数组类型、地理数据类型、窗口函数)的业务。选它的理由通常不是性能,而是兼容性和生态,比如 GIS 类应用、数据分析类业务。
TDSQL-A(分析型)和上面几个 OLTP 产品定位不同,面向的是大宽表、海量数据的分析查询,通常和 OLTP 实例配合做数仓或实时报表。
2.2 兼容性比性能参数更应该先看
挑数据库的时候,很多人第一眼看 benchmark,我建议先看兼容性清单。原因很实际:性能不达标最坏是慢,兼容性不达标是业务跑不起来。
判断兼容性要分四层来看。第一层是协议兼容,也就是客户端驱动能不能直连。TDSQL MySQL 版兼容 MySQL 协议,意味着你现有的 JDBC、连接池、ORM 框架理论上都能直接用,这是改造成本最低的部分。
第二层是语法兼容。大部分 DDL、DML 都没问题,但有几类要重点验证:存储过程和触发器(分布式环境下执行位置不明确,很多版本支持有限)、SELECT ... FOR UPDATE 在跨分片场景的行为、GROUP BY 加 ORDER BY 再加 LIMIT 的归并逻辑、以及 JOIN 的跨分片执行计划。我一般会把这些语句单独写成一个"兼容性验证脚本",在新库上跑一遍,比看文档可靠。
第三层是函数兼容。MySQL 版本之间的函数差异比很多人想的大,比如 JSON_TABLE、窗口函数、CTE 递归查询,在不同内核版本上支持程度不一样。如果你的 SQL 里用了大量 MySQL 8.0 的新特性,迁移到某个兼容 5.7 语义的实例上就会报错。
第四层是隐式行为兼容。这一层最难查,比如 ORDER BY 在无索引情况下的稳定性、GROUP BY 隐式排序是否开启、utf8mb4 排序规则的默认值、以及自增主键在分布式环境下的连续性假设。这些差异不会让 SQL 报错,但会让结果和原来不一样,排查起来极其耗时。
提示:兼容性验证不要用业务库直接跑,准备一套完整的建表语句加存量数据样本(1 万到 10 万行足够),在目标实例上做一次全链路回归,包括 DDL、批量导入、复杂查询、事务回滚四个环节。
2.3 什么时候该上分布式,什么时候云原生实例就够了
我给一个粗糙但实用的判断流程:如果业务写入 TPS 长期在单实例规格的 60% 以下,且单表容量可以通过归档控制,优先考虑 TDSQL-C 或者干脆继续用高规格单实例,没必要为分布式买单。如果写入已经触及单实例上限,或者单表超过 5000 万行且无法归档(比如订单流水要求保留三年),那就要认真评估分布式实例了。
还有一个容易被忽略的因素:团队运维能力。分布式数据库的日常巡检要看的指标,比单机多出一大截——分片间数据分布是否倾斜、全局序列是否打满、跨片事务的成功率和耗时分布、SET 级别的故障切换记录。如果团队只有一两个 DBA,贸然上分布式很可能变成"能用但用不好"。
3. 分片键、路由与一致性:分布式数据库真正的难点
分布式数据库上手最快的部分是建实例,最难的部分是分片键设计。我见过太多项目在这一步返工,返工一次的代价通常是重做数据迁移加业务改造,至少两周起步。
3.1 分片键选错的一次完整返工
有个做在线教育的客户,选的是 course_id 做分片键。理由看起来合理:课程维度是主要查询维度,课中数据量最大。上线三个月后问题来了——他们的核心业务其实是"学生查看自己的课表和进度",这类查询的过滤条件是 student_id,而 student_id 不在分片键里。结果每一张课表查询都要广播到所有分片,再归并排序,响应时间从原来的 15 毫秒涨到 400 毫秒。更麻烦的是,他们的报名表用 course_id 分片,学生表用 student_id 分片,两表 JOIN 直接变成跨分片 JOIN。
返工方案是把分片键换成 student_id,因为绝大部分高频查询都围绕学生维度。课程维度的查询则通过一张冗余的宽表(按 course_id 分片)来支撑,代价是数据写入要双写,一致性要业务层保证。这个方案不算优雅,但确实把 P99 从 400 毫秒压回了 30 毫秒以内。
从这次返工我总结出选分片键的三条硬标准:高频查询的过滤条件优先,分片后各片数据量要均匀(否则会热点),分片键尽量能覆盖多表 JOIN 的关联字段(让关联操作落在同一片内)。三条冲突时,第一条权重最高。因为查询性能影响的是所有用户的体验,数据倾斜影响的是集群稳定性,而跨片 JOIN 至少可以通过冗余表绕过。
还要注意分片键的不可变性。如果业务存在"分片键字段会被 UPDATE"的情况,那就要小心,跨分片的行迁移会带来额外的一致性风险。理想的分片键是那种一旦写入就不变的字段,比如用户 ID、订单号、商户号。
3.2 一致性级别与分布式事务到底怎么选
TDSQL 这类产品通常提供多档一致性,我在实际项目里见过的配置大概分三档:强同步(主库提交事务前必须收到至少一个备库的确认,RPO 接近 0,但写入延迟受备库影响)、异步(主库提交即返回,延迟最低,故障时有数据丢失风险)、以及介于两者之间的半同步或多数派确认。
选择逻辑很简单:金融账务、支付流水必须强同步,宁可多几毫秒也不能丢一笔;日志类、埋点类、推荐特征类数据可以用异步,丢几条无伤大雅;用户行为类的中间态数据可以半同步。这里的关键是把数据按"丢失代价"分级,而不是整个库一刀切。我见过一个项目全库开强同步,结果跨城部署时写入延迟到了 60 毫秒,高峰期的写入队列直接把应用层拖垮。
跨分片写入的分布式事务,底层一般是两阶段提交(2PC)或者优化后的变种。它保证的是原子性,代价是多一次网络往返和锁持有时间变长。实践中的经验是:尽量让事务落在单个分片内。方法包括把关联数据按同一分片键存储、把大事务拆成多个小事务用业务补偿兜底、以及把非核心的从属写入改成异步。
举个具体例子,一个下单流程包含"扣库存、写订单、写流水"三步。如果库存表按 sku_id 分片、订单表按 order_id 分片,那这个事务必然跨片。改造方式是把库存扣减改成"预占"模式:先在库存分片内扣减并记录预占流水,再在订单分片内写订单,最后异步对账。这样每个分片内的事务都是本地的,跨片一致性靠业务状态机保证。这套模式写在纸上很顺,实现的时候要注意幂等和补偿的重试上限,否则会出现"预占了但订单没落库"的悬挂状态。
3.3 全局唯一序列与热点写入的处理
分布式环境下自增主键的连续性基本不用指望了。TDSQL 一般提供全局唯一序列,保证唯一但不保证连续。如果你的业务代码里有"依赖 ID 连续"的逻辑(比如用 ID 差值统计当日新增量),这块一定要提前审计出来,因为这类 bug 通常在生产环境跑一两个月才暴露。
另一种常见做法是应用层生成 ID,比如雪花算法、号段模式、或者基于数据库的号段表。雪花算法的坑在于时钟回拨,号段模式的坑在于号段表本身成了单点。我的建议是:能交给数据库就用数据库的序列,应用层只做兜底。
热点写入是分片之后必然要面对的问题。就算分片键选得好,也可能因为业务特征出现单点:某个大商户的订单量占全站 30%,那么按商户 ID 分片时这片必然过热。常见处理方式有两种,一种是给热点键加后缀再打散,比如把 merchant_id 拼上 0 到 9 的后缀拆成十片,查询时按后缀范围扫描十次再合并。另一种是把热点数据分离到独立的实例,大商户单独一套集群,其他商户共享一套。后者的运维复杂度更高,但在超大客户场景下更常见。
3.4 跨分片查询的限制要提前告知业务方
跨分片查询不是不能做,是性能模型和单机完全不同。单机上的 SELECT COUNT(*) 走索引可能几十毫秒,跨分片归并时每个分片各自算完再汇总,耗时约等于最慢那个分片的耗时加上网络开销。如果是分页查询,尤其是深分页(LIMIT 100000, 20),归并时要取回 100020 条记录再截断,内存和网络都会爆。
我一般会给业务方一份"SQL 白名单",明确列出哪些写法可以接受、哪些必须改写。深分页统一改成游标分页(基于上一次的最大 ID 或时间戳);全表计数改成预聚合表或者近似统计;跨片排序尽量改成分片内排序后再归并,并且限制归并的数据量上限。这份白名单听起来很土,但它能省掉大量的线上沟通成本。
4. 高可用与容灾:企业级的门槛其实在这里
判断一个数据库能不能叫"企业级",我的标准不是性能峰值,而是故障时的表现。性能峰值可以通过加机器达到,故障恢复能力不行就是不行。
4.1 副本架构与故障切换的判定逻辑
TDSQL 的每个 SET 内部通常是一主多备,强同步复制。故障切换的核心问题是怎么判断主库真的挂了。判断太激进,会出现网络抖动导致的误切换,主备角色互换后数据出现分叉;判断太保守,故障恢复时间是分钟级,业务已经受影响。
业界常见做法是"多数派确认加租约"。主库持有一个租约,租约到期前必须续约,续约需要多数副本确认。如果主库因为网络问题联系不上多数副本,租约到期后它自己降级为只读,其他副本选出新主。这套机制的好处是避免了"双主"脑裂,代价是网络分区时可能出现短时间不可写。
对我们这些使用者来说,关键要关注两件事:切换的自动程度和切换后应用的表现。自动切换省事,但如果应用没有正确处理连接失效(比如连接池里握着已经断开的连接重试),切换后反而会出现一批超时报错。所以切换演练必须做,而且要在业务低峰期做,至少每季度一次。演练时要观察连接池的恢复时间,这个数字往往是整个恢复过程中最大的一块。
4.2 同城双活与异地容灾的形态对比
| 部署形态 | 典型 RPO/RTO 目标 | 成本 | 适用场景 |
|---|---|---|---|
| 同城多可用区 | RPO 接近 0,RTO 秒到分钟级 | 中等 | 大多数核心业务 |
| 同城双活(双写) | RPO 接近 0,RTO 秒级 | 高 | 支付、交易核心链路 |
| 异地灾备(异步) | RPO 秒到分钟级,RTO 分钟级 | 中高 | 需要合规灾备的业务 |
| 异地多活 | RPO 接近 0,RTO 秒级 | 很高 | 全国性超大规模业务 |
同城多可用区是最常见的形态,因为同城网络延迟低(通常 1 到 3 毫秒),强同步复制的开销可以接受。异地灾备就麻烦了,跨城延迟通常在 20 到 40 毫秒,强同步会把写入延迟拉高一个量级,所以异地基本都是异步复制,那就得接受一定的数据丢失窗口。
这里有个容易被忽略的细节:灾备实例不是拿来读的。有些团队为了省钱,把报表查询打到异地灾备实例上,结果灾备实例负载升高影响了复制延迟,真出故障时切换过去数据落后一大截。灾备实例的定位就是"待命",保持低负载,最多跑一些轻量的心跳检测。
4.3 扩容不是加机器,是一次数据重分布
分布式数据库扩容的直观理解是"加节点,数据自动搬过去",但实际操作要考虑的细节很多。数据重分布过程会占用网络和磁盘 IO,如果在业务高峰期做,会明显影响在线请求。所以扩容窗口要选在低峰,而且要有暂停和回滚的预案。
重分布的粒度也很关键。如果按分片整体搬迁,迁移的是整片数据,速度慢但一致性简单;如果按行搬迁,速度可控但需要处理迁移期间的双写和冲突。后者对业务透明性更好,但实现复杂度高。实际产品大多采用前者的变种,配合流控来限制迁移速录。
我建议在扩容前做一次数据分布体检。如果各分片数据量已经严重倾斜(最大片是最小片的 3 倍以上),单纯加节点解决不了问题,因为热点数据还是集中在原分片。这时候需要先做数据再平衡或分片键调整,再考虑扩容。
4.4 备份、回档与闪回的实际价值
备份这件事,不看"备份成功"的告警,看"恢复成功"的记录。我参与过一次真实的误删恢复:开发同学在测试环境执行了一条没有 WHERE 条件的 DELETE,因为测试库和生产库用了同一个运维入口,误伤了生产库的 12 万行数据。当时能救回来靠的是两点:一是备份粒度足够细(按小时的全量加 binlog 增量),二是能精确回档到误操作前的时间点。
所以评估备份能力要问三个问题:备份的粒度和保留期是多少、能不能按时间点恢复到任意秒、恢复的目标是实例级还是表级。很多产品支持实例级恢复但不支持单表恢复,意味着恢复时要拉起一个完整实例再导出表,这个流程在大实例上可能要几个小时。
闪回(flashback)能力也值得关注,它通过解析 binlog 反向生成补偿 SQL,能在几分钟内回滚误操作,比全量恢复快得多。但闪回有前提:binlog 格式必须是 ROW,而且表的 DDL 在操作前后没有变化。这两条在真实环境里不一定满足,所以闪回当快速通道用,备份当最终兜底。
5. 从自建 MySQL 迁移到 TDSQL 的完整落地路径
迁移这件事,技术难点不在搬数据,在评估和切换。搬数据有现成的工具和方法,评估和切换才是决定成败的部分。
5.1 兼容性评估清单怎么列
我把评估拆成五个维度,每个维度出一份结论清单:
表结构维度:所有列类型是否支持、字符集是否一致、分区表是否需要改造、外键是否需要去掉(分布式环境下跨片外键一般不支持)、有没有超出限制的索引长度。
SQL 维度:存储过程、触发器、事件调度、自定义函数是否使用;有没有依赖 MySQL 特定语法的 SQL;跨分片 JOIN 的语句有多少条,分别怎么改写。
事务维度:是否存在跨分片的写事务;事务的平均持有时长;有没有长事务(超过 10 秒)需要拆分;隔离级别是否有依赖。
容量维度:各表当前行数和增长速度;预计三年后的数据量;已归档数据的处理方案;大字段(TEXT/BLOB)的占比。
性能维度:QPS、TPS 峰值和均值;读写比;慢 SQL 的 TOP 20 和它们的执行计划;连接数峰值。
这份清单最好做成 Excel 或者表格形式的 checklist,逐项打勾,不通过的项目要么改造要么给出书面风险确认。我吃过一次亏:一个项目上线后才发现某个报表用了触发器做数据同步,而目标实例对触发器的支持有限,结果报表数据对不上,查了两周才定位到。
5.2 数据同步与灰度切换的节奏
标准的迁移流程大致是这样的:先用 DTS 类的工具做一次全量迁移,然后开启增量同步,让源库和目标库保持准实时一致;同步运行期间做业务双跑验证,用影子流量或者抽样比对的方式校验数据一致性;一致率达到要求后,选一个低峰窗口做切换,切完之后源库保留只读状态,观察三天再下线。
切换这一步,核心是应用侧怎么切。最稳的方式是在配置中心里存两个数据源地址,通过开关控制读写走向,切换时先灰度 1% 的流量到新库,观察错误率和延迟,没问题再逐步放大到 10%、50%、100%。每一步之间至少观察 15 分钟,因为有些问题(比如慢查询、连接泄漏)不会立刻暴露。
一致性校验的工具要提前准备好。常见做法是分段对比:按主键区间把数据分成若干段,逐段计算 checksum(比如 CRC32 或者字段拼接后的 hash),只对不匹配的段做逐行比对。全网逐行比对在大表上会跑到天荒地老,分段校验能把耗时压到可接受范围。
5.3 上线后必须盯住的观测指标
切换完成后的一周是最关键的。我会盯这几组指标:
| 指标组 | 具体指标 | 异常信号 |
|---|---|---|
| 连接 | 活跃连接数、连接池等待时间 | 等待时间持续超过 50 毫秒 |
| 写入 | TPS、事务提交耗时 P99、分布式事务占比 | P99 突增或跨片事务比例超过 10% |
| 复制 | 主备延迟、强同步等待耗时 | 延迟超过 1 秒 |
| 分片 | 各分片 QPS/数据量分布、热点分片比例 | 最大片是最小片的 2 倍以上 |
| SQL | 慢查询数量、跨片查询占比 | 跨片查询占比上升 |
| 错误 | 死锁次数、序列等待、切换日志 | 死锁次数单日增长翻倍 |
这里面我最看重的是分片数据量分布和跨片查询占比。前者反映分片键设计是否合理,后者反映应用改造是否到位。这两个指标一旦恶化,性能会呈非线性下降,而且很难靠加机器挽回。
6. 踩过的坑和一份不太受欢迎的选型建议
6.1 五个高频误区
误区一:把分布式数据库当成单机 MySQL 用。 表现是所有 SQL 原样搬过去,不改分片键、不改跨片查询、不改深分页。结果性能比原来还差,然后得出结论"分布式不靠谱"。实际上分布式数据库的性能上限建立在数据局部性上,不按它的规则来,性能一定打折。
误区二:只看峰值性能,不看长尾。 分布式环境下跨片操作天然有长尾,P99 和 P50 的差距会明显大于单机。如果你的 SLA 是 P99 小于 100 毫秒,那么跨片查询的比例必须严格控制,否则再怎么优化平均值也没意义。
误区三:以为加了机器就能解决一切。 分片键选错、数据倾斜、热点集中的问题,加机器是解不了的。加机器解决的是"均匀负载下的容量问题",不是"结构性设计问题"。
误区四:忽略运维方式的改变。 分布式数据库的慢查询分析、锁分析、执行计划解读和单机是不一样的。原来的 DBA 经验需要重新积累,特别是跨片执行计划怎么看、分片间数据怎么校验,这些都需要专门的学习和演练。
误区五:一致性级别一刀切。 前面提过,全库强同步在跨城部署时会把延迟拉高一个量级。分级配置是必须的,而且要定期回头审视分级是否还合理,因为业务的数据重要性排序会变。
6.2 有些业务真的不该上分布式
说句可能不太受欢迎的话:相当一部分号称需要分布式数据库的业务,其实用高规格单实例加归档就能扛三年。判断标准我一般这么看——如果业务的写入峰值在单实例规格的 50% 以下,数据量增长可控,团队 DBA 人数在两人以下,那优先用云原生实例或者干脆用高配单实例。分布式数据库带来的运维复杂度、开发约束和排错难度,在这些场景下是纯成本。
反过来说,什么情况必须上分布式:写入能力已经触及单实例上限并且还在增长;单表数据量无法通过归档控制;业务对容灾有明确的 RPO/RTO 要求(比如 RPO 接近 0);有多租户隔离需求且租户间资源需要独立配额。这几条满足两条以上,就值得认真评估 TDSQL 这类分布式产品了。
最后一个实践建议:如果决定上,先拿一个非核心但有一定数据量的业务做试点,跑满一个完整的季度,包括一次扩容、一次故障切换演练、一次数据恢复演练。这三件事在文档里看都是几行字,真做一遍才知道团队缺哪些能力。试点结束之后再决定要不要把核心业务迁过去,这个顺序比反过来稳妥得多。
我自己在几个项目里最深的体会是,分布式数据库真正难的不是技术选型,是"约束的传递"——你得把分片键不能随便改、跨片查询有代价、自增 ID 不连续这些约束,准确无误地传达到每一个写 SQL 的人手里。技术文档解决不了这件事,代码规范、SQL 审核、上线卡点这些流程性的手段才行。数据库换个名字很容易,让整个团队的写法跟着变,才是真正的工程量。