腾讯云 TDSQL 分布式数据库:架构拆解、分片设计与迁移落地

TDSQL分布式数据库分片键
于 2026-10-01 06:20:56 修改
·本内容遵循CC 4.0 BY-SA版权协议

做后端架构这十来年,我最怕听到的一句话就是“这张表再顶半年应该没问题”。单机 MySQL 从几十万行涨到几十亿行,中间不会给你任何预警,某天凌晨三点主库 IO 打满、慢查询堆成山、DDL 卡死整张表,你才知道什么叫“账迟早要还”。这几年我经手过几个日订单千万级的项目,从最初的读写分离加缓存硬扛,到后来整体迁到腾讯云的 TDSQL 分布式数据库,中间踩的坑足够写一本小册子。这篇内容就围绕“腾讯云数据库 TDSQL:企业级分布式数据库产品概要”这个主题,把我理解的架构逻辑、选型判断、部署实操和排查经验完整梳理一遍。它讲的是一件很具体的事:当单机数据库的容量、并发、可用性三条线同时逼近极限时,分布式数据库到底用什么办法把问题拆开,以及一个普通后端团队该怎么落地。不管你是刚接触分布式数据库的新手,还是已经用过分库分表中间件想找更彻底方案的老兵,下面这些内容应该都能直接用上。

1. 先把需求说清楚:什么场景才真的需要 TDSQL

1.1 单机数据库撞墙的三条线

判断要不要上分布式数据库,其实不需要看什么复杂的容量模型,盯住三条线就够了:单表数据量的写入线、连接数与 QPS 的并发线、故障恢复的可用性线。这三条线的表现不太一样,但它们的共同点是——一旦撞上就不会自动回落。

先说数据量。MySQL 单表在 InnoDB 引擎下,数据量超过 2000 万行左右,B+ 树索引层级会从三层涨到四层,每次查询多一次磁盘随机 IO,延迟直接从微秒级跳到毫秒级;超过 5000 万行以后,加索引、改字段这类 DDL 操作基本就没法在业务低峰期做完了,因为 MySQL 的执行方式是建影子表再拷贝全量数据,一张 5000 万行的表加个字段可能要跑两三个小时,中间还得占用双倍磁盘和大量 IO。这时候常见的做法是分库分表,可分库分表一旦落到应用层,分片键、路由规则、跨片聚合、跨片事务全都变成业务代码的事,运维复杂度是几何级上升的。

再说并发。单实例 MySQL 的连接数通常在几千级别,但真正能干活的活跃连接受制于 CPU 核数和磁盘 IOPS。一台 16 核的机器,活跃连接超过几百个,QPS 到几万,CPU 的上下文切换开销就会吃掉大量算力。磁盘这边更直接,普通 SSD 单盘 IOPS 大约几万,一主两从同步复制的话,写入放大到两三倍,很快就把 IO 打满。

最后是可用性。传统的一主多从加上半同步复制,主库宕机后从库升主,中间必然有一段不可写窗口。业务容忍度低的话,这段窗口就是事故。而且主从切换之后,原来主库上没同步过去的事务会丢,RPO 不为零,对账类业务直接对不上。

提示:不要因为“数据量看起来很大”就急着上分布式。先确认瓶颈到底在哪条线上,如果只是读多写少,加从库和缓存往往比换数据库划算得多。

1.2 TDSQL 到底把哪些问题接了过去

TDSQL 是腾讯云上的企业级分布式数据库产品,内核基于 MySQL 做了深度改造,对上保持 MySQL 协议和语法兼容。它做的事情说白了就三件:把一张大表拆成很多小表分散到多台机器上、把跨机器的事务和查询收拢到统一入口、把单点故障变成自动切换。

拆表这件事在 TDSQL 里叫分片,一个分片叫一个 SET,每个 SET 内部仍然是一主多从的 MySQL 实例组,只不过单 SET 只存全量数据的一部分。业务侧看到的还是“一个库、一张表”,SQL 写法基本不变,路由和聚合都交给中间的 SQL 引擎层。这一点和 ShardingSphere 这类应用层中间件最大的区别在于——路由信息、分片元数据、扩缩容动作都归数据库自己管,应用不再需要维护分片配置,也不会因为某个分片规则写错导致数据写歪。

统一入口这层其实更值钱。跨 SET 的写入需要保证原子性,TDSQL 用两阶段提交把分布式事务封装起来,业务侧写 BEGIN ... COMMIT 就行,不用自己实现补偿逻辑。跨 SET 的查询同理,SQL 引擎会并行下推到各分片,再把结果集在中间层做排序、聚合、去重,最后返回给客户端。这个过程对上层是透明的,代价是中间层会消耗额外内存,尤其是大结果集的排序聚合要特别注意。

自动切换则依赖强同步复制机制。主库在提交事务之前,必须等至少一个备库确认落盘,这样主库挂了以后备库升主不会丢数据,RPO 等于零。切换过程由调度集群自动完成,业务侧通常只感受到连接被重置,重连之后指向新的主库。这里有个细节:强同步虽然保住了数据不丢,但会抬高写入延迟,因为每次提交都要多等一次网络往返。

1.3 什么样的团队适合上手

不是所有团队都适合直接上分布式数据库,我见过太多“为了技术先进而迁移”最后又迁回来的案例。适合上 TDSQL 的,通常是这么几类团队。

第一类是数据量已经确定性会突破单机上限的业务,比如订单、流水、日志、物联网点位数据这类只增不减的场景。这类业务的特点是写入量大、数据生命周期长、历史数据查询频率低,天然适合分片。

第二类是并发压力集中且波动剧烈的业务,比如秒杀、抢购、票务。这类业务平时 QPS 不高,一到大促就翻几十倍,单机扩容只能垂直加配置,成本极高。分布式的横向扩展能力在这种场景下性价比明显。

第三类是可用性要求达到金融级的业务,对数据一致性和故障恢复时间有硬性要求。TDSQL 在腾讯内部支撑过支付类核心系统,这块的工程积累是比较扎实的。

反过来说,如果你的业务单表数据量还在千万级以内、QPS 在几千以下、团队里没人懂分布式事务和分片键设计,那硬上分布式数据库就是给自己找麻烦。分片键选错了导致的跨片查询,性能可能比单机还差,这类案例我后面会专门讲。

1.4 和自建分库分表方案的真实对比

很多人纠结的点是:我已经用 ShardingSphere 或者 MyCat 做了分库分表,还有必要换成 TDSQL 吗?这个问题得从几个维度拆开看。

从开发成本看,应用层中间件需要业务代码里维护分片规则,改分片键基本等于重写数据访问层;TDSQL 把这些收进数据库,业务只改连接串。

从运维成本看,应用层中间件的扩缩容需要人工搬数据、改配置、灰度切流,一次扩容排期两周起步;TDSQL 支持在线扩容,调度层自动做数据重分布,业务侧基本无感。

从事务能力看,这是差距最大的一块。应用层中间件做跨片事务,要么用柔性事务加补偿,要么自己搞两阶段提交,正确性全靠业务保证;TDSQL 在数据库内核层面实现了分布式事务,一致性由数据库兜底。

从成本看,自建方案看起来省了数据库费用,但算上人力、机器、故障损失的隐性成本,规模大了以后反而更贵。这就是典型的“看起来免费的东西最贵”。

2. 架构拆解:TDSQL 是怎么把一台数据库变成一群数据库的

2.1 计算与存储分离与不分离的两条路线

讲 TDSQL 的架构,得先分清腾讯云上两个不同的产品线,这是最容易混淆的地方。

TDSQL MySQL 版走的是 Shared-Nothing 路线,也就是每个分片 SET 各自带自己的本地存储,计算和存储绑在一起。这种架构的好处是单分片内没有网络存储的额外延迟,写入路径短,适合写密集和高并发的 OLTP 场景;代价是扩容时要以 SET 为单位搬迁数据,重分布过程会占用网络和磁盘带宽。这套架构继承自腾讯内部的分布式数据库实践,在支付、计费这类场景里跑了十几年。

TDSQL-C(原名 CynosDB)走的是计算存储分离路线,计算节点只管算,数据统一放在分布式共享存储池里,一份数据多个计算节点共享。这种架构的好处是扩容快,加只读节点几乎是秒级生效,存储按实际用量计费;代价是写入路径上多了一层网络存储访问,对存储网络的稳定性要求很高。它更适合读多写少、需要快速弹性伸缩的业务,比如内容类、报表类系统。

选哪条路线,本质上是在问自己一个问题:我更需要写入延迟低,还是更需要弹性快?写密集的核心交易走前者,读写比悬殊的业务走后者。

2.2 分片机制:SET 是怎么组织起来的

TDSQL MySQL 版的骨架可以这样理解:最上层是SQL 引擎层(网关),中间是调度集群,下层是一堆SET 分片组。

SQL 引擎层对外暴露 MySQL 协议端口,客户端用普通 MySQL 驱动就能连上。它负责解析 SQL、判断走哪个分片、生成执行计划、下发到各 SET、回收结果并聚合。这一层是无状态的,可以水平扩展,前面挂负载均衡就行。

调度集群负责维护全局元数据——哪个表有哪些分片、每个分片在哪些物理机器上、主备关系是什么、当前谁是主。它还负责故障检测和自动切换,通常用一致性协议保证自身的高可用。

SET 是真正的数据承载单元。每个 SET 内部是一主多从的 MySQL 实例组,主库负责读写,从库负责容灾和读扩展。分片规则支持 HASH、RANGE、LIST 几种,也支持二级分片——先按一个维度分,再在分片内部按另一个维度细分。二级分片在时间维度的数据上特别有用,比如先按用户 ID HASH 分,再按月份做二级分区,这样清理历史数据可以用 DROP PARTITION 秒级完成,不用 DELETE 扫全表。

注意:分片数不是越多越好。分片数太多会导致每个 SET 数据量很小但元数据开销大,跨片查询的概率也高。经验值是单分片数据量控制在 500GB 到 1TB 之间比较舒服。

2.3 强同步复制与自动切换的细节

数据不丢这件事,靠的是强同步复制。它的工作方式和 MySQL 原生的半同步有点像但更严格:主库在每个事务提交前,必须等待至少一个备库返回“已写入 relay log 并落盘”的确认,收到确认之后才向客户端返回提交成功。

这个机制带来的直接影响是写入延迟会升高一个网络往返的时间。同机房内通常零点几毫秒,跨可用区可能到一两毫秒。对延迟敏感的业务要做取舍,TDSQL 一般提供了强同步和异步两种模式,可以在不同从库上分别配置——比如一个同城从库做强同步保数据不丢,一个异地从库做异步保容灾,两全其美。

自动切换的流程大致是:调度集群通过心跳检测发现主库失联,先确认备库数据是否追平(强同步模式下必然是追平的),然后把备库提升为主库,更新路由元数据,通知 SQL 引擎层刷新连接指向。整个过程通常在几十秒内完成,业务侧看到的是连接被断开重连。

这里有个实操经验:切换之后一定要检查自增 ID 和连接池。虽然 TDSQL 对自增做了全局管理避免跳变和冲突,但应用侧的连接池里可能还缓存着指向旧主库的连接,需要配置连接有效性检测和最大存活时间,否则会持续报错。我遇到过连接池不做检测导致切换后一半请求失败的情况,排查了半天才发现是池子里的死连接。

2.4 产品形态与选型对照

为了少走弯路,把常见的几种形态和适用场景摆在一起对比会更清楚。

维度 TDSQL MySQL 版 TDSQL-C 单机 MySQL 高可用版
存储架构 Shared-Nothing,本地盘 计算存储分离,共享存储 本地盘
扩展方式 分片水平扩展,在线扩容 计算节点秒级增减 垂直升配
单库容量上限 PB 级 数十 TB 级 数 TB 级
写入延迟 低,路径短 略高,走共享存储网络 最低
跨片事务 内核级分布式事务 单节点本地事务 无
适用场景 交易、订单、计费、账务 内容、报表、读写分离重 中小规模业务
运维复杂度 较高,需理解分片 中等 低

这张表的用法很简单:先看数据量预期,再看读写比例,最后看一致性要求。三项都指向分布式的时候再上,否则别折腾。

3. 核心机制深挖:分片键、分布式事务与 SQL 兼容

3.1 分片键怎么选才不踩坑

分片键的选择直接决定系统未来三年的命运,这话一点不夸张。选错了,你会得到一个跨片查询满天飞、性能还不如单机的分布式系统。

第一条原则是高频查询条件必须是分片键。如果你的业务 80% 的查询都是按商户 ID 查订单,那分片键就该是商户 ID,而不是订单 ID。反过来,如果按订单 ID 查,那就得保证订单 ID 里包含商户信息,或者建立额外的路由索引。这一点在设计之初就要想清楚,后期调整代价极大。

第二条原则是数据分布要均匀。用自增 ID 做 HASH 分片没问题,用时间戳直接做分片键就是灾难——所有新数据都落在最新的分片上,形成热点,其他分片闲着。如果是时间序列数据,正确做法是用“业务 ID 做一级 HASH,时间做二级 RANGE”,既保证写入分散,又方便按时间清理。

第三条原则是避免分片键被更新。分片键一旦确定,修改它就意味着这条记录要从一个分片搬到另一个分片,也就是跨片写,性能开销巨大,而且很多分布式数据库根本不支持更新分片键。如果业务上确实存在“归属变更”的需求,那就得设计成删除加插入。

我踩过的最典型的坑是拿 UUID 做分片键。UUID 的 HASH 值虽然均匀,但它是随机的,导致同一个业务主体的数据被分散到不同分片,任何关联查询都变成跨片查询。正确做法是用带业务含义的组合键,比如 商户ID_订单序号,既能保证分布均匀,又能让同一商户的数据落到同一个分片。

3.2 分布式事务的两阶段提交与性能代价

跨分片写入的事务,TDSQL 用两阶段提交(2PC)来保证原子性。流程是:协调者先向所有参与分片发送 prepare 请求,每个分片把事务的 redo 日志写好但不提交,返回准备成功;协调者收到全部成功后,再向所有分片发送 commit,各分片真正提交。只要有一个分片在 prepare 阶段失败,协调者就向所有分片发送回滚。

这套机制的正确性没问题,但性能代价是实打实的。一次跨片事务至少需要两轮网络往返,而且事务进行期间,参与分片上的相关行会被锁定,锁持有时间比本地事务长得多。如果业务里有大量跨片事务,TPS 可能只有本地事务的十分之一。

所以有一条硬性经验:能用本地事务解决的,绝不用分布式事务。具体做法有两种。

第一种是让相关数据落到同一个分片。比如订单和订单明细,如果订单明细的分片键直接从主订单继承,那订单的创建和明细的插入就在同一个 SET 内,是一个本地事务。这个设计思路叫“分片键继承”或者“同组存储”,是分布式数据库设计里最实用的技巧之一。

第二种是把分布式事务降级为最终一致。像积分发放、消息通知、日志记录这类不影响主流程一致性的操作,完全可以改成异步。主事务提交成功后发一条消息,下游消费者慢慢处理,失败就重试。

提示:做分布式事务压测的时候,务必构造真实的跨片场景。很多团队压测时数据恰好落在同一个分片,跑出来的 TPS 特别好看,上线后一跨片就崩。

3.3 MySQL 兼容性到底“兼”到什么程度

TDSQL 号称兼容 MySQL,但兼容性这件事必须拆开看,否则上线才发现某个语法不支持就很被动。

基础的 DDL、DML、事务控制、常见的聚合函数和 JOIN,兼容度很高,常规业务代码基本不用改。存储过程、触发器、事件调度这些也支持,但跨分片的场景下行为可能和单机不同,比如触发器里如果访问了其他分片的表,就会触发跨片操作。

不兼容或者需要注意的主要是三类。

第一类是跨分片的复杂操作。比如跨片的多表 JOIN,性能会很差,因为需要把数据拉到中间层做笛卡尔积。再比如跨片的 SELECT ... FOR UPDATE,锁的粒度和行为跟单机完全不同,容易死锁。

第二类是依赖单机特性的功能。比如 LOAD DATA INFILE 这种直接读服务器本地文件的方式,在分布式环境下语义就变了。再比如一些 INFORMATION_SCHEMA 的查询,返回的是全局视图,跟单机看到的表结构信息不完全一致。

第三类是自定义函数和特定版本语法。TDSQL 内核基于某个 MySQL 版本分支,如果你用了更新版本才有的语法,可能不被支持。

实操建议是:迁移前把应用里所有的 SQL 抓出来跑一遍兼容性检查,尤其是那些写得花哨的、用了各种 MySQL 特有函数的语句。腾讯云提供了一些评估工具,但最靠谱的办法还是在测试环境全量回归一遍。

4. 实操:从零部署一套 TDSQL 并跑通业务

4.1 容量与规格估算的具体算法

部署之前先算账。这一步做扎实了,后面能省掉大量返工。

估算的输入有三个:当前数据量、日增量、增长率。假设现在有一张订单表 8000 万行,平均每行 500 字节,加上索引约占 1.5 倍空间,那么当前占用约 8000万 × 500B × 1.5 ÷ 1024³ ≈ 56GB。日增订单 200 万行,一年后增加约 200万 × 365 × 500B × 1.5 ÷ 1024³ ≈ 51GB。看起来不大,但如果考虑业务三年翻三倍,三年后总量约 56 + 51 × 3 × 3 ≈ 515GB。

再看并发。假设峰值 QPS 是 5 万,其中写 1 万、读 4 万。单个 SET 的一主两从配置,通常能扛住 1 万到 2 万 QPS 的混合负载(具体取决于 SQL 复杂度)。按 1.5 倍冗余算,需要 5万 ÷ 1.5万 ≈ 4 个 SET,向上取整到 8 个 SET 留出余量。

最后看连接数。应用侧连接池总连接数除以分片数,就是每个 SET 承受的连接数。这个数值要控制在单实例 MySQL 的合理范围内,一般单 SET 2000 个连接以内比较稳妥。

把三个维度的结论取最大值,就是初始分片数。我一般建议初始分片数取 2 的幂次,比如 8、16、32,因为 HASH 分片的扩容通常是以倍数进行的,这样重分布的时候可以用“分裂”的方式做,迁移量最小。

4.2 实例创建与网络接入的关键配置

在腾讯云控制台上创建 TDSQL 实例,需要重点关注的几个配置项是:可用区分布、网络类型、字符集、安全组。

可用区分布建议选择同城多可用区部署。主库在一个可用区,强同步备库在另一个可用区,这样单个可用区故障不会影响数据一致性,同时延迟也在可接受范围。如果是异地容灾需求,再额外配一个异地异步从库。

网络类型这块,强烈建议走私有网络 VPC 内网访问,不要开公网。原因有两个:一是公网访问延迟不稳定,分布式事务对延迟敏感;二是安全风险,数据库端口暴露在公网上是典型的被攻击目标。如果确实需要本地开发连测试环境,走跳板机或者专线,别图省事直接开公网白名单。

字符集要统一。建议在建实例时就固定为 utf8mb4,排序规则用 utf8mb4_general_ci 或 utf8mb4_0900_ai_ci。这个问题看起来小,但一旦有分片用了不同字符集,跨片 JOIN 会出现乱码或者报错。改字符集在分布式环境下比单机麻烦得多,因为要逐分片处理。

安全组配置遵循最小权限原则,只放通应用服务器的网段。这一点在分布式环境下尤其重要,因为实例暴露的端口不止一个,SQL 引擎层、管理端口都要管好。

4.3 建库建表与分片绑定的实际操作

TDSQL 的分片信息通常不写在标准的 CREATE TABLE 语句里,而是通过独立的语法或者控制台操作来绑定。以一个订单表为例,基本的建表语句和单机 MySQL 差不多:

SQL
CREATE TABLE `t_order` (
`order_id` bigint(20) NOT NULL,
`merchant_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
`amount` decimal(14,2) NOT NULL,
`status` tinyint(4) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ext_info` varchar(1024) DEFAULT NULL,
PRIMARY KEY (`order_id`),
KEY `idx_merchant_time` (`merchant_id`,`create_time`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键在分片绑定这一步。分片键必须包含在主键或者唯一索引里,这是分布式数据库的硬性要求——因为唯一性校验只能在单个分片内完成,如果分片键不在唯一索引里,数据库没法保证全局唯一。

绑定分片时一般要指定:分片字段、分片算法(HASH/RANGE/LIST)、分片数量。比如按 merchant_id 做 HASH 分 16 片:

SQL
-- 语法示意,具体以官方文档为准
CREATE TABLE t_order (...)
SHARDKEY = merchant_id
SHARDING = HASH(16);

这里有个容易忽略的点:广播表和小表。像配置字典、地区表、类目表这类数据量小、几乎不更新、但经常被 JOIN 的表,应该设为广播表,全量复制到每个分片。这样 JOIN 就变成片内 JOIN,性能提升非常明显。反过来如果忘了设,每次 JOIN 都要跨片拉数据,性能差几十倍。我第一次用的时候就吃过这个亏,一个商品分类表的 JOIN 拖慢了整个订单列表接口。

另一个技巧是单表。有些表确实只在某个业务模块用,不参与跨片 JOIN,也不大,可以声明为单表存在某一个分片,减少元数据复杂度。

4.4 连接配置与读写分离的落地

应用侧接入 TDSQL,连接串和普通 MySQL 几乎一样,只是主机换成 SQL 引擎层的地址。真正需要调的是连接池参数。

核心参数有四个:最大连接数、最小空闲连接、连接最大存活时间、连接有效性检测。最大连接数按应用实例数乘以分片数来算,别把总连接数打满数据库。连接最大存活时间建议设置成几分钟,这样主备切换后旧连接能被自然淘汰。连接有效性检测必须开,否则切换后池子里的死连接会持续报错。

读写分离这块有两种做法。一种是应用侧配置多个地址,写走主地址、读走只读地址,代码里显式区分。这种做法控制力强,但业务代码侵入大。另一种是数据库侧自动分离,SQL 引擎根据语句类型自动路由,读语句下发到从库。

我的经验是:核心交易链路走第一种,报表和查询类走第二种。理由是核心交易对一致性要求高,写完之后立刻读的场景(比如下单后马上查订单状态)如果被路由到从库,可能因为复制延迟读到旧数据。而报表类业务对延迟容忍度高,自动分离能省不少事。

注意:如果一定要在读从库的同时保证一致性,可以在事务内读取,或者用强制走主库的 Hint。跨分片的主从延迟比单机更复杂,因为每个 SET 的延迟可能不同。

4.5 监控指标与压测验证

上线前必须做压测,而且要压得真实。需要盯的指标分三层。

数据库层看 CPU 使用率、磁盘 IOPS 和写入延迟、活跃连接数、慢查询数量、主备复制延迟。这几个指标能反映底层资源是否吃紧。特别是主备复制延迟,一旦某个 SET 的从库延迟拉大,说明这个分片的写入压力超过从库处理能力,要么加从库要么调分片。

SQL 引擎层看 QPS、平均响应时间、P99 响应时间、跨片查询占比、分布式事务占比。跨片查询占比是最关键的指标,如果超过 20%,说明分片键设计有问题。分布式事务占比超过 10%,TPS 上不去是必然的。

业务层看接口成功率和超时率。这一层最能反映真实体验,数据库指标好看但接口超时率高,通常是连接池配置或者网络链路的问题。

压测方法上,建议先单分片压,再全分片压,最后混合场景压。先单分片是为了确认单个 SET 的承载能力上限,作为后续扩容的基准。全分片压是为了验证 SQL 引擎层是否存在瓶颈,这层如果成为瓶颈,加再多分片也没用。混合场景压是为了模拟真实的读写比例和跨片比例。

压测数据要尽量贴近真实业务分布。我见过用随机 ID 压测跑出完美曲线的案例,上线换成真实用户 ID 之后性能掉了一半——原因是真实数据存在明显的头部集中,少数商户占了大部分流量,形成了热点分片。

5. 常见问题排查实录

5.1 热点分片:最隐蔽的性能杀手

热点分片的典型表现是:整体 CPU 使用率不高,但监控上某一个分片的 CPU 持续 90% 以上,这个分片上的慢查询明显多于其他分片,整体 QPS 却上不去。

根因通常是分片键分布不均。常见的有三种情况。一是大商户效应,某个商户的业务量是其他商户的几百倍,按商户 ID HASH 之后,这个商户的数据全部落在同一个分片。二是时间分片导致的写热点,所有新数据都写最新分片。三是热点商品或热点活动,秒杀场景下单商品被集中访问。

解决思路分几层。最直接的是在分片键里加盐,比如把 merchant_id 改成 merchant_id + hash(子维度) % N,把一个大商户的数据打散到 N 个分片。代价是查询时要合并多个分片的结果。

第二层是二级分片,把热点分片再拆成子分片,让调度层把子分片分散到不同物理机。这个方案对应用透明,但需要数据库支持。

第三层是业务侧拆分,把热点商户单独迁到一个独立实例,或者把热点活动的流量引到缓存和消息队列,减少直接写库的压力。

排查手段上,先看慢查询日志,确认慢查询集中在哪个分片;再看分片的数据量分布,如果某个分片的数据量明显大于其他分片,就是分片键分布不均;最后看业务维度的维度聚合,确认是不是头部客户导致的。

5.2 分布式事务报错与死锁排查

跨片事务最常见的两类问题是超时和死锁。

超时的表现是提交阶段报 Lock wait timeout exceeded 或者事务提交超时。根因通常是事务持锁时间过长,把其他事务堵住了。在分布式环境下,持锁时间等于所有参与分片的锁持有时间的最大值,所以只要有一个分片慢,整个事务就慢。

处理办法有三条:缩短事务范围,只把必须原子性的操作放进事务,查询和无关写入移出去;统一加锁顺序,多个分片的情况下,如果不同事务加锁顺序不一致就会死锁,规范做法是按分片 ID 排序后再加锁;设置合理的事务超时时间,不要用默认的长超时,早点失败早点重试比长时间挂着强。

死锁的排查比单机难,因为涉及多个分片。需要抓取每个分片的死锁日志,然后按时间对齐分析。这个工作比较繁琐,最好的办法是事前预防:把跨片事务的数量降到最低,从设计上避免死锁的可能。

有一个很实用的经验:给事务加业务唯一标识并打日志。出问题的时候,可以拿这个标识去各分片搜索相关事务,快速定位参与方和加锁顺序,比盲猜效率高得多。

5.3 扩容与数据重分布要注意什么

在线扩容是分布式数据库的核心卖点,但实际执行时有不少细节要注意。

扩容的时机选择很重要。不要等到容量到 80% 才扩容,因为重分布过程本身会占用大量磁盘和网络资源,容量紧张的时候做扩容风险很高。建议在 60% 左右就开始规划,留足余量。

扩容过程中的性能会下降,因为迁移数据要读源分片、写目标分片,占用 IO 和网络带宽。所以必须做限速,控制迁移速率,避免把业务流量挤掉。TDSQL 一般提供迁移限速参数,调到业务低峰能承受的范围内。

重分布期间要关注主备延迟。大量数据迁移会让从库的复制压力剧增,延迟可能从毫秒级涨到秒级甚至分钟级。如果这时候主库故障需要切换,从库可能还没追上,切换会失败或者丢数据。所以扩容窗口最好选在业务低峰,并且确保强同步从库有足够的处理能力。

扩容完成后一定要做数据校验。校验方式包括行数比对、关键字段聚合值比对(比如按分片算 SUM 和 COUNT)。这一步不能省,我见过迁移过程中因为网络抖动丢了几百行数据的案例,靠业务对账才发现,那时候已经过去好几天了。

5.4 常见问题速查表

把实际运维中遇到的高频问题整理成表,出问题的时候可以直接对照排查。

现象 可能原因 排查方向 处理建议
单分片 CPU 高、整体 QPS 低 分片键分布不均形成热点 查各分片数据量和慢查询分布 分片键加盐或二级分片
跨片查询占比高 分片键与高频查询条件不匹配 抓 SQL 分析 WHERE 条件 调整分片键或建路由索引
写入 TPS 上不去 分布式事务占比过高 统计跨片事务比例 合并分片、异步化非核心操作
主备切换后大量报错 连接池缓存死连接 检查连接有效性检测配置 开启检测并设置最大存活时间
扩容后数据对不上 迁移过程丢数据或重复 行数、聚合值比对 补数据并复查迁移限速参数
慢查询突然增多 某分片数据倾斜或索引缺失 看慢日志的执行计划 补索引,必要时重建分片
JOIN 性能奇差 小表没设为广播表 检查表的分片属性 改为广播表或单表
事务超时频繁 跨片事务持锁时间过长 分析事务参与分片数量 缩短事务范围、统一加锁顺序

6. 迁移落地与长期运维经验

6.1 从单机 MySQL 迁到 TDSQL 的实操路径

迁移这件事,最忌讳的就是“一次性全量切换”。我经历过的最稳的迁移路径是四步走。

第一步是评估与改造。把现网所有的 SQL 抓出来,逐条检查兼容性;把表按访问模式分类,确定哪些是分片表、哪些是广播表、哪些是单表;设计分片键并做数据分布模拟,确认不会形成热点。这一步大概占总工作量的 40%,但决定了后面 60% 的顺利程度。

第二步是双写与数据同步。用数据同步工具把单机 MySQL 的数据实时同步到 TDSQL,同时应用侧开始双写。双写期间要定期做数据比对,确保两边一致。这个阶段通常跑一到两周。

第三步是灰度切读。把一部分只读流量切到 TDSQL,观察性能指标和错误率。因为读流量风险低,出问题可以立刻切回。灰度比例从 1% 逐步提到 50%,跑通之后再考虑切写。

第四步是切写与下线。选一个业务低峰期,把写流量切到 TDSQL,单机 MySQL 保留只读状态作为回滚兜底。观察一到两周没问题再正式下线。

注意:切写的时候一定要准备好回滚方案。回滚不是简单地把流量切回去,还要考虑切换期间 TDSQL 上新写入的数据怎么回补。所以切写窗口要选得足够短,并且提前做好反向同步的准备。

6.2 应用侧需要改的代码清单

分布式数据库号称“对应用透明”,但完全不改代码是不可能的。实际需要改的地方大概有这么几类。

连接管理要改。从单实例连接改成连接池加负载均衡,配置连接有效性检测和最大存活时间。

SQL 语句要改。重点是去掉那些依赖单机特性的写法:跨库 JOIN 改成多次查询加内存拼装;SELECT FOR UPDATE 的粒度要重新审视;分页查询在跨片场景下要改写成“各分片取前 N 条再合并”的模式,否则 LIMIT 100000, 20 这种深层分页会扫全表。

事务边界要改。把事务范围缩到最小,非核心操作移出事务。这一点在单机环境下可能不重要,在分布式环境下是性能的关键。

自增主键要改。虽然 TDSQL 提供了全局自增,但高并发下全局自增可能成为瓶颈。推荐的方案是用雪花算法或者号段模式在应用侧生成分布式 ID,保证趋势递增,对 B+ 树索引友好。

批量操作要改。大批量的 INSERT 和 UPDATE 要控制单次提交的数量,避免一次事务涉及太多行导致持锁时间过长。经验值是单次批量不超过 500 行。

6.3 长期运维该养成的几个习惯

用了一年多 TDSQL,我总结出几个特别有用的运维习惯。

第一,每周看一次分片数据量分布。画出各分片数据量的柱状图,如果发现某个分片明显偏大,说明分片键有问题,早发现早处理。等到差了几倍再处理,迁移成本高得多。

第二,建立跨片查询的监控告警。把跨片查询占比作为一个核心指标,超过阈值就告警。这个指标最能反映分片设计是否健康,比 CPU、IOPS 那些资源指标更有预警价值。

第三,给所有关键表的分片键变更留好退路。分片键一旦确定基本不能改,所以设计阶段要多花时间。如果真的设计错了,退路是新建一张表按新分片键分布,然后做数据迁移,用双写过渡。这条路径成本很高,所以设计阶段值得反复推敲。

第四,定期做故障演练。主动触发一次主备切换,看看业务侧的表现,看看连接池和重试逻辑是否生效。演练过和没演练过,真出故障的时候差别巨大。我第一次做切换演练时发现应用重试逻辑有 bug,会重复提交订单,幸好是在演练环境发现的。

第五,把慢查询治理当成日常。分布式环境下慢查询的影响会放大,因为一个分片的慢查询可能拖慢整个跨片查询。建立慢查询的定期 review 机制,把 EXPLAIN 结果和分片信息结合起来看,能发现不少隐藏问题。

关于部署工程师这个角色,我观察到一个现象:分布式数据库的交付越来越依赖专业的部署实施团队,因为他们能根据业务的实际访问模式来设计分片方案、配置参数、规划扩容。纯靠文档自助式的部署,很容易在分片键设计这一步就埋下隐患,后面出问题排查起来成本极高。所以如果团队里没有分布式数据库经验,找有经验的人一起过一遍设计方案,比自己摸索几个月划算得多。

最后说个我自己的体会:分布式数据库不是银弹,它把单机的复杂度转化成了分布式的复杂度,总量并没有减少,只是换了一种形式。真正决定成败的不是数据库本身有多强,而是你在设计阶段有没有把数据分布、事务边界、查询模式这三件事想清楚。想清楚了,TDSQL 用起来会非常顺手,扩容、切换、迁移这些操作都能在线完成,业务几乎无感;没想清楚,再好的数据库也救不回来。