我接触过不少从自建MySQL迁到腾讯云TDSQL MySQL版的团队,大家的心理预期通常是“MySQL怎么用,TDSQL就怎么用”。这个方向没错,但真到了写代码、跑SQL、调应用的时候,你会发现事情没有这么简单。TDSQL MySQL版本质上是分布式数据库,它对MySQL 5.7/8.0的协议和语法做了大量兼容,但分布式架构决定了它不可能100%复刻单机MySQL的所有行为。本文我就围绕TDSQL MySQL版开发指南里最核心的“兼容性”三个字展开,结合我自己在迁移和联调过程中踩过的坑、验证过的结论,把哪些能用、哪些不能照搬、哪些需要改写法一次说清楚。适合正准备评估TDSQL、正在做兼容性改造,或者已经在云上但被各种SQL报错卡住的开发同学参考。
1. 先搞清楚TDSQL MySQL版到底是“什么形态”的MySQL
1.1 从“兼容性”这个词说起
很多人一听到“兼容MySQL”,第一反应是拿MySQL 8.0的官方文档逐条对比TDSQL支持哪些函数、哪些关键字。这个思路过于线性了。TDSQL MySQL版的兼容性设计目标,不是让你把单机MySQL所有犄角旮旯的语法都搬上来,而是让你在业务层面做到“低成本迁移”。换句话说,它保证的是90%以上的常规SQL写法,在TDSQL里可以原样执行,剩下的10%可能涉及跨节点数据访问、全局一致性、分布式事务等特殊场景,需要开发上做一些必要的妥协。理解这一点,你就不会在遇到某个冷门函数不支持的时候,觉得是腾讯云“偷工减料”,而是会去思考这类函数在分布式架构下本身存在的天然难点。
1.2 TDSQL MySQL版的整体架构:计算与存储分离、分布式事务
要理解兼容性边界,先得知道TDSQL MySQL版底层长什么样。它采用了计算节点(proxy)与存储节点(set)分离的架构,proxy负责SQL解析、路由、分布式事务协调,set负责真实的数据存储和主从复制。对开发者来说,你连接的TDSQL地址其实是一个接入网关,SQL进来之后会被proxy拆分成多个分片上的子查询,再把结果聚合返回。这个设计思路决定了三件事:第一,单表数据量超过单分片容量时,可以通过水平拆分扩展,这在自建MySQL里是做不到的;第二,跨分片查询的代价会比单库内查询高一个量级,所以SQL写法直接影响性能;第三,分布式事务的提交协议(TDSQL用的是类两阶段提交机制)会带来额外的锁等待和延迟,事务粒度越大,影响越明显。开发指南里列的很多“不推荐”或“受限支持”的语法,本质上都是因为这三条边界。
2. 开发指南里的兼容性:哪些MySQL能力可以直接用
2.1 基础SQL语法与常用DML的兼容情况
先说结论:常规的SELECT/INSERT/UPDATE/DELETE、多表JOIN、子查询、UNION、聚合函数、排序分组,在TDSQL MySQL版里基本都是兼容的。我自己验证过复杂嵌套子查询、CASE WHEN配合聚合、窗口函数(MySQL 8.0语法)等场景,只要这些查询最终能被下推到单个分片执行,执行结果和单机MySQL完全一致。关键就在“能否下推”这四个字上。如果查询条件里带上了分布键(即建表时指定的shardkey),proxy可以准确判断数据落在哪个分片,直接把SQL路由到对应分片执行,这种场景下你几乎感觉不到分布式带来的差异。但如果查询条件里没有分布键,proxy就需要把SQL广播到所有分片,再把结果汇聚排序,这种全表扫描式的查询在数据量大时会有明显的性能损耗,开发指南里通常建议这种SQL“尽量避免”或者“低频使用”。所以我的经验是,写DML之前先问自己一个问题:这条SQL的WHERE条件里,有没有包含分布键?
2.2 数据类型、字符集与排序规则的兼容细节
TDSQL MySQL版在数据类型上覆盖了MySQL的主要类型:数值型(TINYINT到BIGINT、DECIMAL、FLOAT、DOUBLE)、字符串型(CHAR、VARCHAR、TEXT、BLOB系列)、时间型(DATE、DATETIME、TIMESTAMP、TIME、YEAR)、JSON、ENUM、SET。一个容易忽略的细节是,TDSQL对TIMESTAMP和DATETIME的行为做了统一的时区处理,建议应用层统一用UTC存储、本地时区展示,避免因为云主机和数据库实例时区不一致导致时间偏移。字符集方面,默认是utf8mb4,排序规则为utf8mb4_general_ci,这一点和大部分云上MySQL实例保持一致。如果你的业务需要区分大小写,建表时要把排序规则显式改成utf8mb4_bin,否则模糊查询和等值匹配的行为会和本地开发库不一致。我在实际迁移中遇到过开发环境用utf8mb4_general_ci、生产TDSQL也默认这个规则,但代码里做字符串精确匹配时忽略了大小写校验,导致数据查重出现偏差,最后就是靠修改排序规则解决的。
2.3 索引、视图、存储过程这些“老熟人”能用吗
索引方面,TDSQL MySQL版支持普通二级索引、唯一索引、复合索引,也支持全文索引,基本能做到和MySQL一致。但有一个显著差异:分布式表的主键和分片键的设计是绑定的,官方建议把分片键作为主键的一部分,否则当主键不包含分片键时,跨分片唯一性校验会变得非常复杂,性能和一致性都受影响。这一点开发指南里有明确说明,我强烈建议新手建表前先把分片键和主键的关系想清楚。视图(View)是支持的,但前提是视图定义里的SQL能够被下推到分片执行,或者说视图内查询涉及的表最好共享同一个分片键分布策略。如果视图里关联了两张分片策略不同的表,实际执行时会触发跨分片JOIN,性能很难看。存储过程和函数属于另一个典型差异区。TDSQL MySQL版对存储过程、函数、触发器的支持是分阶段的,常规的BEGIN...END、IF/ELSE、循环、游标、异常处理基本可用,但跨分片事务中的存储过程调用,尤其涉及多条跨节点写入时,需要应用层保证事务边界清晰,不能把大量业务逻辑堆在存储过程里,否则分布式事务协调的开销会让数据库CPU居高不下。触发器我建议慎用,尤其是跨分片的数据联动,这类操作很容易因为分布式执行计划的限制出现“部分成功”的风险,能放在应用层做就放在应用层做。
3. 不能照搬的MySQL习惯:分布式带来的“差异区”
3.1 分区键:分布式表绕不开的第一个设计决策
这是TDSQL MySQL版和自建MySQL最核心的分水岭。在自建MySQL里,你只需要设计主键、二级索引、分区表,从来不用考虑“这条数据该放哪台机器”的问题;但在TDSQL里,建表时必须指定分片键(shardkey),例如shardkey=user_id。分片键的选择直接决定了数据分布的均匀度、查询的路由效率、JOIN能否下推。我的实践结论有三条:分片键一定要选业务中最常见的等值查询条件字段,比如用户ID、订单ID;分片键的基数要大,枚举值不能太少,否则数据会堆积到极少数分片上形成热点;分片键最好稳定不变,业务上不要轻易修改分片键的值,因为修改分片键在底层意味着数据在分片间迁移,开销非常大。举个例子,如果做电商订单系统,选order_id做分片键,那所有按订单号查询的SQL都能精确定位分片;如果选order_status做分片键,那等于把数据按状态拆堆,一个“已完成”状态的订单量可能占了全表80%,这种设计基本是灾难。
3.2 自增列、唯一约束与分布式事务的使用边界
自增列在MySQL里用得非常多,但在分片环境下,全局唯一的自增ID是没法靠单机auto_increment实现的。TDSQL MySQL版提供了两种方案:一种是使用分布式自增(AUTO_INCREMENT),在分片内保证自增、全局通过时间片+分片号等组合保证不重复,但生成的ID不是严格连续递增的,而是趋势递增;另一种是使用应用层生成全局唯一ID,比如雪花算法。我建议优先用应用层生成ID,因为TDSQL的分布式自增在批量插入场景下还是会有性能瓶颈,且ID可读性不高,不方便做日志排查。唯一约束也有坑。单分片内的唯一约束是强约束,但跨分片的全局唯一只能通过唯一全局二级索引来实现,建这类索引的成本较高。如果一张分布式表同时有多个字段需要全局唯一,比如手机号和邮箱都要唯一,我的建议是拆到多张表或者用应用层加分布式锁去保证,而不是在一张表上堆多个全局唯一索引。分布式事务方面,TDSQL支持跨分片强一致事务,但事务的粒度和耗时直接影响系统吞吐。官方开发指南的实践建议是:把跨分片事务控制在极小范围内,能用最终一致就用最终一致,实在需要强一致再考虑开启分布式事务。我自己压测下来的数据是,单分片事务的TPS可以到数万,但跨两个分片的写事务,TPS会降到原来的40%左右,跨三个分片更低。所以表设计阶段就要主动避免让一个核心业务事务横跨太多分片。
3.3 SQL限制:跨节点JOIN、子查询、聚合查询的注意点
开发指南里有一类“支持但需注意”的SQL,我理解为“最好不要这么写”。跨节点JOIN就是最典型的。假设表A按user_id分片,表B按order_id分片,那么A JOIN B ON A.user_id=B.user_id时,两个表的分布键不一致,proxy只能把所有分片的数据拉到一个计算节点上做JOIN,数据量大时内存和CPU都扛不住。解决思路有三个:一是把两张表设计成相同的分片键,让JOIN下推到分片内执行,这叫按分片键对齐;二是小表复制,TDSQL支持把维表设置为广播表(每个分片都存一份全量数据),这样大表和小表JOIN时也能下推;三是干脆在应用层拆成多次查询,内存里做关联。子查询的限制类似,如果子查询内部和外层的表数据分布不一致,优化器很难对子查询做下推,一些深层次嵌套的IN子查询在数据量上来后会非常慢,需要改写成JOIN或者分步查询。聚合查询里,GROUP BY和ORDER BY一般没问题,但要注意COUNT(DISTINCT)和复杂DISTINCT在跨分片场景下的处理,TDSQL需要把所有非重复值汇聚到计算节点去重,数据基数很大时内存压力不小,可以考虑抽样估算或者提前在业务层做去重。
4. 迁移与联调实操:从MySQL切到TDSQL的完整检查清单
4.1 使用兼容性评估与数据迁移的常见路径
腾讯云提供DTS(数据传输服务)做数据迁移,支持从自建MySQL、云上MySQL以及其他云厂商数据库迁移到TDSQL MySQL版,迁移前建议先在测试环境完整跑一遍评估。我的迁移流程大致分五步:第一步,梳理业务表清单,标记每张表的数据量、主键、查询特征,确定哪些表适合做分布式表、哪些表适合做广播表、哪些表可以使用单分片表;第二步,建表DDL改造,把分布式表的shardkey显式加到建表语句里,去掉或改写不适用的存储引擎参数,检查字符集排序规则;第三步,做兼容性预检查,用TDSQL控制台的兼容性评估工具扫描存量SQL,重点看有没有跨分片JOIN、大事务、触发器、自定义函数、锁表操作;第四步,用DTS做全量+增量迁移,观察延迟和校验结果,对比源库和目标库的关键业务数据;第五步,业务只读切读验证,再灰度切写,最后全量切换。整个过程中,最容易被忽略的是存量SQL里的隐式类型转换和依赖MySQL特定排序规则的查询,这两类问题迁移后表现非常隐蔽,不容易第一时间发现。
4.2 连接方式与连接串参数说明
TDSQL MySQL版的连接方式和MySQL几乎一样,支持标准的MySQL客户端、JDBC、Python的pymysql、Go的go-sql-driver等。连接地址通常在控制台的实例详情里获取,分为内网地址和公网地址,生产环境务必使用内网地址。JDBC连接串模板大概是:
jdbc:mysql://tdsql-mysql-proxy.example.tencentcdb.com:5258/dbname?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true&socketTimeout=30000有几个参数需要特别说明。zeroDateTimeBehavior=convertToNull是处理MySQL零日期值的惯用参数,建议保留;rewriteBatchedStatements=true对批量INSERT优化非常明显,TDSQL支持批量写入下推,开启后性能提升明显,我自己测试过一万行批量插入,开启该参数后耗时缩短约60%;socketTimeout一定要设置,否则应用和数据库之间的连接异常时,Java端会长时间阻塞。另外,TDSQL控制台可以创建只读账号和读写账号,建议应用层区分使用,只读流量走只读账号还能自动分发到只读节点。
4.3 典型开发配置事项汇总
我把日常联调过程中涉及到的开发配置事项整理成了一张速查表,非常实用。分片键字段建议使用int类型或字符串类型,避免使用text;建议所有分布式表都显式指定主键,且主键包含分片键;建议使用utf8mb4字符集;不建议在分布式表上使用外键约束,外键检查在分片环境下经常出现行为不一致,改为应用层逻辑控制;建议开启强制访问控制、SSL加密传输等安全配置;建议把持久连接的空闲超时时间设置在60秒以内,避免因proxy层面的连接超时导致连接被重置;建议SQL里所有参数都使用预编译绑定,不要拼接字符串,这样不仅安全,还能让proxy的SQL缓存命中率更高。这些事项看起来琐碎,但每一条背后都对应着一类线上故障,踩过坑的人自然懂。
5. 常见兼容性报错与排查实录
5.1 报错类型整理与解决思路
我在迁移和联调过程中收集了一批典型报错,这里用表格形式分享出来,方便读者按图索骥。
| 报错现象 | 根本原因 | 解决思路 |
|---|---|---|
| ERROR 1105 (HY000): unknown error | 常见于没有根据分片键查询时proxy路由失败 | 检查查询条件是否包含分片键,必要时改表结构设计 |
| ERROR 1062 (23000): Duplicate entry | 跨分片唯一索引约束冲突 | 检查是否使用了全局唯一二级索引,改用应用层保证唯一 |
| ERROR 1205 (HY000): Lock wait timeout exceeded | 跨分片事务持锁时间过长,锁等待超时 | 缩小小事务范围,避免跨分片大事务,检查是否存在热点行更新 |
| ERROR 1146 (42S02): Table doesn't exist | 视图或存储过程引用的表状态不一致 | 检查对象是否在异常切换过程中丢失,重建对象绑定关系 |
| Sort aborted: Out of memory | 跨分片大量数据排序时计算节点内存不足 | 优化SQL增加分片键条件,避免全量排序,或者升级计算节点规格 |
| Unknown system variable | 使用TDSQL不支持的部分MySQL变量 | 检查JDBC连接串和SQL前置变量,改用支持的参数 |
5.2 我之前踩过的几个坑
第一个坑是关于INSERT ... ON DUPLICATE KEY UPDATE。在单机MySQL里这个语法用得爽,但在TDSQL分布式表上,如果唯一键不是分片键,冲突检测需要跨分片查询,性能很差,而且某些场景下行为会和单机MySQL不一致。后来我把业务改成先查询再决定插入还是更新,或者把唯一键和分片键对齐,问题才解决。第二个坑是SQL里的ORDER BY CASE WHEN写法。单机MySQL支持在ORDER BY里用CASE WHEN实现定制排序,TDSQL的优化器在跨分片排序时对这种表达式的下推支持不好,数据量一大就性能退化,最后改成在应用层用Java代码做二次排序。第三个坑是长时间运行的存储过程。早期把一个复杂的月度结算逻辑写成了存储过程,里面包含多表更新、游标循环,还涉及跨分片数据,上线后直接拖垮了数据库的CPU,最后把结算逻辑拆成了多个独立任务,用消息队列串起来,系统才算稳定。这些经验说明一个问题:TDSQL兼容性的底线是“常规操作没问题”,但如果你想把它当成任性的单机MySQL来用,报错和性能问题一定接踵而至。
6. 最后再分享一点运维侧的经验
在开发指南之外,我还想补充几个运维层面的体会。TDSQL实例在控制台可以监控到每个分片的QPS、慢查询、CPU使用率、磁盘容量等指标,很多人只看实例总览,忽略了分片维度的监控。我建议把分片维度的监控图挂到大屏上,因为分布式数据库最怕的就是数据倾斜,一个分片满了而其他分片还很空闲,这种状态通过总览数据是看不出来的。另外一个建议是慢查询日志一定要定期分析。TDSQL的慢查询日志记录了SQL文本、执行耗时、返回行数、扫描行数,通过对比扫描行数和返回行数,可以快速找到那些缺少分片键条件导致的“广播式查询”。还有一点,发版前一定要在预发环境完成SQL Review,让有经验的人审核一下每条新上线SQL的分布键使用情况,这比线上出问题再紧急处理成本低得多。根据我的经验,只要守住“分片键必带、事务必小、大查询必改”这三条原则,TDSQL MySQL版在绝大多数业务场景下都能稳定运行,兼容性基本不会成为瓶颈。