腾讯云 TDSQL 分布式数据库:架构拆解、分片设计与迁移落地
做后端架构这十来年,我最怕听到的一句话就是“这张表再顶半年应该没问题”。单机 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 差不多:
关键在分片绑定这一步。分片键必须包含在主键或者唯一索引里,这是分布式数据库的硬性要求——因为唯一性校验只能在单个分片内完成,如果分片键不在唯一索引里,数据库没法保证全局唯一。
绑定分片时一般要指定:分片字段、分片算法(HASH/RANGE/LIST)、分片数量。比如按 merchant_id 做 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 用起来会非常顺手,扩容、切换、迁移这些操作都能在线完成,业务几乎无感;没想清楚,再好的数据库也救不回来。