预见猿份
主题
首页面试题模拟面试SQL练习在线工具关于我们老苗一对一私教学员评价
实战项目
零基础学习路线项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通达梦数据库从入门到实战MongoDB从入门到实战PostgreSQL从入门到实战TiDB从入门到实战Java数据结构与算法Java 并发编程(JUC)实战课程老苗一对一私教学员评价
blog我的会员
  • TiDB从入门到实战|做过分库分表的Java程序员的分布式SQL课程

    • 课程介绍
    • 第1章 认识TiDB与整体架构
    • 第2章 MySQL兼容性与差异清单
    • 第3章 TiDB vs 分库分表全面对比
    • 第4章 数据建模与分布式特性
    • 第5章 Java集成实战
    • 第6章 HTAP、执行计划与调优
    • 第7章 迁移与日常运维







----- 到底线了 -----

第3章 TiDB vs 分库分表 全面对比 ​

🎯 本章学习目标 ​

  1. 把你 yunlan 第九章 里 ShardingSphere-JDBC 分库分表的每一条复杂度,逐条对应到 TiDB 的原生能力,建立「TiDB 替你做了什么」的清晰清单。
  2. 从开发维度对比五个痛点:分片键心智负担、跨库 JOIN/聚合/分页、分布式事务、全局唯一 ID、热点与数据倾斜——说清 TiDB 分别在哪儿消灭了它们。
  3. 从运维维度对比五个痛点:扩容 rebalance、DDL 变更铺表、存量数据迁移、监控排障、高可用与副本——说清 TiDB 如何自动化。
  4. 保持批判视角:TiDB 不是银弹,它也有分库分表没有的新成本(集群资源门槛、跨节点网络开销、对 MySQL 程序性对象不支持、小规格不如单机 MySQL 划算)。
  5. 形成一棵选型决策树:什么数据量/团队/场景该继续分库分表,什么该上 TiDB。

3.1 一张总对比表(先建立全局印象) ​

维度分库分表(MySQL + ShardingSphere)TiDB
编程模型单库单表 SQL + 时刻想着分片键单库单表 SQL,无需分片键
数据切分人为按分片键拆成 N 库 M 表引擎自动按 Region 切,应用无感
跨分片 JOIN受限,常需绑表/广播表/应用层拼装原生支持,引擎分布式执行
全局排序/聚合/分页中间件归并,深分页痛引擎原生,跨节点并行归并
分布式事务XA / 柔性事务(Seata)额外引入内置 Percolator 两阶段提交
全局唯一 ID自建雪花/号段自增/AUTO_RANDOM 原生全局唯一
扩容rebalance:搬数据+双写+校验加节点即扩,PD 自动迁移 Region
DDL 变更铺到 N 张物理表逐个执行一条 ALTER,在线异步执行
高可用主从 + MHA/哨兵,运维拼装多副本 Raft,自动故障转移
HTAP 分析需抽数到数仓/单独 OLAPTiFlash 列存副本原生加速
运维复杂度高(中间件+多库+同步链路)中(一套集群,组件自动协调)
起步资源门槛低(几台 MySQL 即可)中高(分布式集群,最少 3+ 节点生产)

看最后两行就能体会到本章的批判基调:TiDB 用「更高的起步资源门槛」换来「开发与日常运维的大幅简化」。它适合数据量大、增长快、不想被分库分表牵着走的团队;不适合「数据其实没多少、却非要上分布式」的小规模场景。

3.2 开发维度逐项对比(你最直接的体感) ​

3.2.1 分片键:从「必须记着它」到「不用管它」 ​

分库分表:你选 user_id 做分片键,于是——

java
// yunlan 第九章的典型写法:查订单"必须"带分片键,否则广播全库
// 不带 user_id 的查询会被迫扫所有分片再归并,性能塌方
SELECT * FROM t_order WHERE order_no = ?;   // ⚠️ order_no 不是分片键 → 全分片扫描
// 为了能按 order_no 查,你还得额外建"基因法/映射表"把 order_no 也编码进分片信息
1
2
3
4

TiDB:没有「分片键」这个应用层概念。数据按主键/RowID 自动切 Region,任何条件的查询都由优化器定位相关 Region 并行取数:

sql
-- 不带任何"分片键",TiDB 照样高效执行;要更快就给 order_no 建普通索引即可
SELECT * FROM t_order WHERE order_no = ?;   -- ✅ 走 order_no 上的索引,直达对应 Region
1
2

省掉的事:不用再设计「所有查询维度都尽量带分片键」的取巧方案、不用基因法/映射表解决「按非分片键查」、不用背「哪些表是分片表、分片算法是什么」的心智包袱。

3.2.2 跨库 JOIN、聚合、分页 ​

分库分表:JOIN 两张分片表是老大难——ShardingSphere 要靠「绑定表」(分片规则一致避免笛卡尔积)或「广播表」(小字典表每库都放一份),复杂聚合要在中间件做「改写 + 各分片执行 + 归并」,深分页 LIMIT 100000,10 更是每个分片都取 100010 条再归并,痛。

TiDB:JOIN/GROUP BY/ORDER BY/LIMIT 就是一条普通 SQL,优化器把它编成分布式执行计划,在多节点并行完成并归并(第 6 章看计划)。你不需要「绑定表」这种概念。

3.2.3 分布式事务 ​

分库分表:一次业务写要跨多个库(订单写库 A、库存扣库 C),本地 @Transactional 管不了跨库,得引入 Seata(AT/TCC/SAGA)或 XA,链路复杂、有性能损耗、还有一致性边界要处理。这与站内分布式事务专题讨论的难题同源。

TiDB:跨节点、跨 Region 的 ACID 分布式事务是内置的(Percolator 模型、两阶段提交 + PD 的 TSO 全局时钟)。你写的还是一个 @Transactional,TiDB 底层自动保证原子与隔离——不需要 Seata。

java
// 分库分表里要跨库、需要 Seata 的场景,在 TiDB 就是一个本地事务:
@Transactional
public void placeOrder(Order o, List<OrderItem> items) {
    orderMapper.insert(o);          // 可能落在 Region 群 A
    itemMapper.batchInsert(items);  // 可能落在 Region 群 B —— TiDB 自动保证两者原子提交
}
1
2
3
4
5
6

3.2.4 全局唯一 ID ​

分库分表:多库各自自增会撞,所以你必须上雪花算法/号段模式(yunlan 第九章就做了全局 ID 设计)。

TiDB:主键自增即全局唯一(不保证连续,见第 2 章),或 AUTO_RANDOM 防热点(第 4 章)。业务单号类仍建议应用层雪花——这条经验不变,但你不再被迫为「技术主键」设计发号器。

3.2.5 热点与数据倾斜 ​

分库分表:按 user_id 取模,若某大户数据特别多,它所在分片就倾斜;自增主键写入永远打到「最后一个分片」形成写热点,要靠各种手段打散。

TiDB:自增主键同样可能写热点(尾部 Region 过热),但 TiDB 原生给了打散工具:AUTO_RANDOM 把 RowID 打散到不同 Region、SHARD_ROW_ID_BITS 无主键表打散、PD 自动把过热 Region 迁移到空闲节点(第 4 章详解)。打散是数据库能力,不再是你中间件层的土办法。

3.3 运维维度逐项对比(你凌晨被告警叫醒的次数) ​

运维动作分库分表TiDB
加机器扩容规划新分片→搬存量数据→双写灰度→校验→切流,一轮数周,心惊胆战加 TiKV/TiDB 节点,tiup scale-out,PD 自动 rebalance Region,对应用透明
缩容/下线极麻烦,要把数据从待下线库迁走缩容节点,PD 自动补齐其上 Region 副本
DDL 加字段/索引在 N 个物理库 × M 张表逐个执行(gh-ost/pt-osc 铺一遍)一条 ALTER TABLE,在线异步自动应用到所有底层数据
存量数据迁移写脚本按分片规则灌入各库各表DM 工具全量+增量自动同步(第 7 章),分库分表还能自动合并
高可用/故障主从切换靠 MHA/Orchestrator,切换可能丢数据、需人工兜底多副本 Raft,少数副本挂了自动选主/补副本,无需外部组件
备份多库分别 xtrabackup/mysqldump,再统一编排BR 工具对整个集群一致性备份/恢复(第 7 章)
监控排障每库各配监控 + 中间件日志,链路长Prometheus+Grafana 统一大盘 + TiDB Dashboard(SQL 洞察/慢查询/Heatmap)
读写分离配 ShardingSphere 读写分离规则 + 主从延迟处理加 TiDB 节点即扩写入口;分析走 TiFlash,天然分流

一句话:分库分表的运维复杂度根源是「逻辑一张表 = 物理 N 份,任何全局动作都要 ×N 并保持一致」。TiDB 把「N 份」这件事收回内核,于是扩容、DDL、备份、故障转移都变成「对集群发一个指令,内核自己协调」。

3.4 批判视角:TiDB 的新成本,别只听好消息 ​

上 TiDB 不是没有代价,选型时必须认清:

  1. 资源门槛高:生产级 TiDB 至少 3 TiKV + 3 PD + 若干 TiDB(还可选 TiFlash),起步就是 6~8 台像样的机器。数据量没到亿级/没到单库瓶颈,硬上 TiDB 是「杀鸡用牛刀」,成本反而高于「一台 MySQL + 偶尔分个表」。
  2. 网络与延迟:计算与存储分离 + 多副本 Raft,意味着每次提交要跨节点多数派确认,同机房还好,跨城部署延迟敏感型事务会受影响。单机 MySQL 没有这层网络往返。
  3. 运维技能栈切换:你不再管分库分表,但要管 TiUP 集群、PD 调度、Region、Grafana 大盘——复杂度是转移不是消失,团队要有分布式数据库的运维心智(第 7 章)。
  4. MySQL 程序性对象不支持:存储过程/触发器/外键/FULLTEXT 要改造(第 2 章清单)。重度依赖这些的存量系统,迁移改造量不小。
  5. 小查询不如单机 MySQL 极致的低延迟:TiDB 强在「大规模 + 并发 + 弹性」,纯低延迟单行点查、小库场景,单机 MySQL/PG 性价比更高。

3.5 选型决策树 ​

数据量/增速?
├─ 单表 < 千万级且增长可控 ─────────► 优先单机 MySQL/PG;别为分布式而分布式
└─ 单表趋近亿级 / 增长快 / 频繁触到单库瓶颈
      │
      ├─ 团队重度依赖 存储过程/触发器/外键/全文索引,且短期无法改造
      │        ────────────────────────────► 谨慎:评估 MySQL 优化/读写分离/局部分表,或改造后再上 TiDB
      │
      ├─ 需要 弹性扩容 + 跨库分布式事务 + HTAP 分析 中任意多项
      │        ────────────────────────────► TiDB 价值最大(免分库分表、内置分布式事务、TiFlash)
      │
      └─ 只是「单库单表撑不住、但业务简单、能接受运维拼装」
               ────────────────────────────► 分库分表(ShardingSphere) 仍是成熟低成本选项;
                                             但要清楚后续扩容/JOIN/DDL 的持续成本
1
2
3
4
5
6
7
8
9
10
11
12
13

经验总结:

  • 分库分表是「用应用层和运维层的复杂度,换一台 MySQL 撑不住的容量」;
  • TiDB 是「用更高的硬件/运维门槛,换掉应用层的分库分表复杂度 + 分布式事务 + 弹性 + HTAP」。
  • 数据真的巨大、团队不想长期背分库分表的债、还要分布式事务/分析能力 → TiDB 香;规模有限、预算紧、业务简单 → 别勉强上。

3.6 本章小结 ​

  1. 开发上 TiDB 免掉了:分片键心智、跨库 JOIN/聚合/分页的中间件归并、Seata 式分布式事务(内置 Percolator)、被迫自建全局 ID、打散热点的土办法。
  2. 运维上 TiDB 自动化了:扩容 rebalance、在线 DDL、DM/BR 迁移备份、Raft 自动故障转移、统一 Dashboard 监控。
  3. 代价要认清:起步资源门槛高、跨节点网络与延迟、运维技能栈切换、MySQL 程序性对象不支持、小场景不如单机划算。
  4. 选型看三点:数据规模与增速、是否要分布式事务/HTAP/弹性、团队能否接受分布式运维。分库分表与 TiDB 都对,只是复杂度放在不同层。

✏️ 课后练习 ​

  1. 拿出 yunlan 第九章 订单分库分表的配置,列出你当时为「分片键、全局 ID、跨库查询、扩容」各做的处理,逐条对应到 TiDB 的原生能力(写成一张「我之前做的 → TiDB 帮我做的」对照表)。
  2. 用决策树给三个场景做选型判断并说明理由:① 日活百万的评论系统、单表已 8000 万;② 内部后台、数据百万级、大量存储过程;③ 风控:交易库 OLTP + 同时要多维实时分析报表。
  3. 针对 3.4 的「网络与延迟」,思考:一个对单行读延迟极度敏感(要求 < 1ms)的缓存前置场景,为什么 TiDB 未必比「Redis + 单机 MySQL」更合适?
  4. 用自己的话回答面试题:「我们都上 TiDB 了,为什么还要关心业务单号用雪花 ID 而不是数据库自增?」(提示:结合第 2 章自增跳号 + 3.4 成本视角)

📖 参考答案 ​

这章答案需要哪种环境? 第 1、3 题的表实验只要任意一种能连上 SQL 终端的环境就行:① WSL2 + tiup playground(第 1 章第 1 题搭的);② TiDB Cloud Serverless 免费实例(https://tidbcloud.com/free-trial,浏览器 SQL Editor 直接贴)。第 2 题是纯分析题,不用环境。第 1 题的第 6 步、第 3 题的「扩容看 Region」属于生产集群动作,本机 playground 跑不了,答案里只给命令和预期输出形态,不需要你真的有 6 台机器。

本章实验表统一用前缀 t_3x_,跑完可以 DROP TABLE 清掉。所有对账数字都是可手算的:12 条订单总额 900.00、PAID 总额 750.00、1000 行分页表第 981~990 行 id 之和 9855。

第 1 题:拿出 yunlan 第九章订单分库分表的配置,列出你当时为「分片键、全局 ID、跨库查询、扩容」各做的处理,逐条对应到 TiDB 的原生能力(写成一张「我之前做的 → TiDB 帮我做的」对照表)。

第 1 步,先把对照基准抄准。打开 yunlan 第九章,当时的分片规则是 2 库 × 每库 2 表 = 4 张物理表:

yaml
# ShardingSphere-JDBC(原书配置,节选)
rules:
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: db-orders-${0..1}.t_order_${1..2}
        databaseStrategy:
          standard:
            shardingColumn: user_id          # ★ 分库键:按用户取模
            shardingAlgorithmName: db-mod
        tableStrategy:
          standard:
            shardingColumn: id               # ★ 分表键:按订单 id 取模
            shardingAlgorithmName: tbl-mod
      t_order_item:
        actualDataNodes: db-orders-${0..1}.t_order_item_${1..2}
        # ★ 与 t_order 用同样规则 → 这叫「绑定表」,否则 JOIN 会笛卡尔积
    shardingAlgorithms:
      db-mod:  { type: INLINE, props: { algorithm-expression: db-orders-${user_id % 2} } }
      tbl-mod: { type: INLINE, props: { algorithm-expression: t_order_${id % 2 + 1} } }
    keyGenerators:
      snowflake: { type: SNOWFLAKE }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

第 2 步,把这张配置翻译成你当时被迫做的四件事(这是后面对照表的左列,先立住):

你要处理的维度当时具体做了什么
分片键所有 SQL 尽量带 user_id;按 order_no 查要在单号里埋「基因位」(order_no 末两位 = user_id % 2)或建映射表
全局 IDkeyGenerator: SNOWFLAKE,主键和业务单号都应用层发号,绝不能靠 MySQL 自增
跨库查询t_order_item 配成绑定表;小字典表配成广播表;GROUP BY/ORDER BY/深分页靠中间件改写 + 归并
扩容2 库扩到 4 库要重建分片规则 → 新库建表 → 存量按 user_id % 4 搬迁 → 双写 → 校验 → 切流

第 3 步,在 TiDB 上建等价的两张单表(不再分片),并灌入可手算的 12 条订单 + 12 条明细:

sql
CREATE TABLE t_3x_order (
  id BIGINT PRIMARY KEY,
  order_no VARCHAR(32) NOT NULL,
  user_id BIGINT NOT NULL,
  status VARCHAR(16) NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_uid (user_id),
  UNIQUE KEY uk_no (order_no)          -- ★ 按 order_no 查靠这条索引,不需要基因法
);

CREATE TABLE t_3x_order_item (
  id BIGINT PRIMARY KEY,
  order_id BIGINT NOT NULL,
  product VARCHAR(32) NOT NULL,
  qty INT NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  KEY idx_oid (order_id)
);

INSERT INTO t_3x_order (id, order_no, user_id, status, amount) VALUES
 (1,'O20260101001',1,'PAID',100.00),( 2,'O20260101002',1,'PAID',200.00),
 (3,'O20260101003',2,'PAID', 50.00),( 4,'O20260101004',2,'WAIT', 60.00),
 (5,'O20260101005',2,'WAIT', 70.00),( 6,'O20260101006',3,'PAID', 80.00),
 (7,'O20260101007',3,'PAID', 90.00),( 8,'O20260101008',3,'PAID',110.00),
 (9,'O20260101009',4,'CANCEL',20.00),(10,'O20260101010',4,'PAID', 30.00),
 (11,'O20260101011',1,'PAID', 40.00),(12,'O20260101012',2,'PAID', 50.00);

-- 每张订单一条明细,金额刻意等于订单金额,方便下一步 JOIN 校验
INSERT INTO t_3x_order_item (id, order_id, product, qty, price) VALUES
 (1, 1,'A',2,10.00),( 2, 2,'A',5,40.00),( 3, 3,'C',1,50.00),( 4, 4,'A',6,10.00),
 (5, 5,'D',1,70.00),( 6, 6,'A',8,10.00),( 7, 7,'B',1,90.00),( 8, 8,'A',11,10.00),
 (9, 9,'E',1,20.00),(10,10,'A',3,10.00),(11,11,'F',1,40.00),(12,12,'A',1,50.00);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

第 4 步,先确认数据没灌错(对账基数):

sql
SELECT COUNT(*) AS cnt, SUM(amount) AS total FROM t_3x_order;
SELECT status, COUNT(*) AS cnt, SUM(amount) AS amt FROM t_3x_order GROUP BY status ORDER BY status;
1
2
text
cnt | total
 12 | 900.00

status | cnt | amt
CANCEL |   1 |  20.00
PAID   |   9 | 750.00
WAIT   |   2 | 130.00
1
2
3
4
5
6
7

手算:100+200+50+60+70+80+90+110+20+30+40+50 = 900;20 + 750 + 130 = 900。后面每一步都用这三个数对账,只要对不上就说明表里数据被改过。

第 5 步,验证「不带分片键的查询」在 TiDB 上是什么行为(对应你当时的基因法/映射表):

sql
-- 分库分表时代:这条 SQL 的 WHERE 里没有 user_id,中间件不知道去哪个库 → 4 张物理表全扫再归并
EXPLAIN
SELECT id, order_no, user_id, status, amount FROM t_3x_order WHERE order_no = 'O20260101007';

SELECT id, user_id, status, amount FROM t_3x_order WHERE order_no = 'O20260101007';
1
2
3
4
5
text
| id | user_id | status | amount |
  7  |    3    | PAID   |  90.00 |

-- 计划里出现的是(算子名字随版本略有差异,认准这两点):
--   Point_Get / IndexLookUp(走 uk_no 唯一索引) 而不是  FullScan
--   并且没有 “广播 / 全分片” 这种概念
1
2
3
4
5
6

判读要点:TiDB 的计划里没有「该去哪个库」这一步,因为只有一张逻辑表;uk_no 是唯一索引,优化器直接定位到索引条目所在的 Region。而在 ShardingSphere 里,同一条 SQL 会被改写成 4 份分别发到 db0.t_order_1/2、db1.t_order_1/2,这就是你当初做「基因法」的全部原因。

第 6 步,验证跨分片 JOIN + 聚合归并变成一条普通 SQL:

sql
-- 分库分表时代:t_order 与 t_order_item 必须是「绑定表」才能这样写;
-- 在 TiDB 上不存在绑定表这个概念
SELECT o.user_id, COUNT(DISTINCT o.id) AS orders, SUM(o.amount) AS order_amt,
       SUM(i.qty * i.price) AS item_amt
FROM t_3x_order o
JOIN t_3x_order_item i ON i.order_id = o.id
WHERE o.status = 'PAID'
GROUP BY o.user_id
ORDER BY o.user_id;
1
2
3
4
5
6
7
8
9
text
user_id | orders | order_amt | item_amt
   1    |   3    |  340.00   | 340.00
   2    |   2    |  100.00   | 100.00
   3    |   3    |  280.00   | 280.00
   4    |   1    |   30.00   |  30.00
1
2
3
4
5

手算对账(PAID 的订单:user1 → 1/2/11 号 = 100+200+40 = 340;user2 → 3/12 = 50+50 = 100;user3 → 6/7/8 = 80+90+110 = 280;user4 → 10 = 30;合计 750,与第 4 步的 PAID 750.00 对上)。order_amt 与 item_amt 每行都相等,这条断言顺便证明了 JOIN 没有漏行也没有重复行——分库分表里绑定表配错就会出现「金额对不上」,TiDB 直接把这类 bug 从根上消灭。

第 7 步,验证深分页:先用 0~9 数字表交叉连接造出 1000 行,再做 offset 翻页 vs 游标翻页对比:

sql
CREATE TABLE t_3x_digit (n INT PRIMARY KEY);
INSERT INTO t_3x_digit VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);

CREATE TABLE t_3x_page (id BIGINT PRIMARY KEY, seq INT, name VARCHAR(20));
INSERT INTO t_3x_page (id, seq, name)
SELECT a.n*100 + b.n*10 + c.n + 1, a.n*100 + b.n*10 + c.n + 1, CONCAT('r', a.n*100 + b.n*10 + c.n + 1)
FROM t_3x_digit a, t_3x_digit b, t_3x_digit c;

SELECT COUNT(*) AS cnt, MIN(id) AS mn, MAX(id) AS mx FROM t_3x_page;   -- 1000 | 1 | 1000

-- 写法一:offset 翻页(要跳过 980 行)
SELECT SUM(id) AS s FROM (SELECT id FROM t_3x_page ORDER BY id LIMIT 980, 10) x;
-- 写法二:游标翻页(记住上一页最后一行的 id = 980)
SELECT SUM(id) AS s FROM (SELECT id FROM t_3x_page WHERE id > 980 ORDER BY id LIMIT 10) x;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
text
s
9855      ← 两种写法都必须等于 981+982+...+990 = (981+990)*10/2 = 9855
1
2

两种写法结果必须一模一样(都是第 981~990 行),但代价不同:

场景offset LIMIT 980,10 实际要处理多少行说明
分库分表(4 个分片)每个分片各取 990 行 → 中间件归并 3960 行公式:分片数 × (offset + pageSize),offset 越大越爆炸
TiDB仍需扫过 990 条主键,但只有一处排序归并,Region 间并行公式:offset + pageSize,不再 ×分片数
TiDB + 游标只读 10 行走主键 Range,WHERE id > ? 直接把起点定位过去

结论一句话:TiDB 让深分页从 n × (offset + k) 降到 offset + k,但根治靠游标翻页这件事在哪个数据库都一样——别以为上了 TiDB 就能 LIMIT 1000000, 10。

第 8 步,验证扩容不需要搬数据(生产集群动作,本机照读即可):

sql
-- 看这张表被切成多少 Region、Leader 分别落在哪个 store
SHOW TABLE t_3x_order REGIONS;
SELECT store_id, address, status_address FROM information_schema.tikv_store_status ORDER BY store_id;
1
2
3
text
# 扩容前(3 个 TiKV):Leader 均匀分布在 store 1/2/3
REGION_ID | START_KEY | END_KEY | LEADER_STORE_ID | PEERS
101       | t_85_r1   | t_85_r5 | 1               | 1,4,7
...

# 生产上执行(tiup cluster,不是 playground):
#   tiup cluster scale-out tidb-prod ./scale-out.yaml     # 新增 2 台 TiKV
#   tiup cluster display tidb-prod
# 之后不需要任何一条 SQL 或应用改动:PD 检测到新 store 是空的,
# 自动把部分 Region 的副本 move/copy 过去,Leader 也会被 transfer-leader 均衡。
# 再 SHOW TABLE t_3x_order REGIONS,LEADER_STORE_ID 就会包含新节点的 store_id。
(具体 Region_ID / store_id 以你实测为准)
1
2
3
4
5
6
7
8
9
10
11
12

第 9 步,把第 2、5、6、7、8 步的结论压成交付用对照表(这就是题目要的表):

我之前做的(ShardingSphere + 多 MySQL)TiDB 帮我做的本章验证方式
databaseStrategy: user_id % 2 选分库键无分片键概念,按 RowID/主键自动切 Region第 5 步:不带 user_id 的查询照样走索引
order_no 里埋基因位 / 建映射表直接 UNIQUE KEY uk_no(或普通索引)第 5 步:WHERE order_no='O20260101007' → Point_Get/IndexLookUp
keyGenerator: SNOWFLAKE 全局发号自增/AUTO_RANDOM 全局唯一(但业务单号仍建议应用层发号,见第 4 题)第 2 章 Q1 实测跳号 6 → 30001
t_order_item 配绑定表防笛卡尔积普通 JOIN,优化器自己决定顺序与算法第 6 步:order_amt = item_amt 每行相等
小字典表配广播表(每库一份)不需要,一张表就是全局的第 6 步:单表 JOIN 单表
中间件改写 + 归并 GROUP BY/ORDER BY分布式执行 + Coprocessor 下推(第 6 章)第 6 步:一次 SQL 出 4 行结果
深分页 n × (offset+k)offset+k,Region 间并行第 7 步:两种写法 SUM(id)=9855
跨库写要 Seata/XA内置 Percolator 分布式事务,@Transactional 一把梭第 3.2.3 节;第 5 章有完整工程
扩 2 库到 4 库:搬数据 + 双写 + 校验 + 切流tiup cluster scale-out,PD 自动 rebalance第 8 步:SHOW TABLE ... REGIONS 前后对比
DDL 要在 4 张物理表铺 gh-ost一条 ALTER TABLE,在线执行第 2 章:ADMIN SHOW DDL JOBS

第 10 步,收尾清场:

sql
DROP TABLE t_3x_order_item, t_3x_order, t_3x_page, t_3x_digit;
1

这题的坑:

坑现象说明
以为「上了 TiDB 就不用管分页」LIMIT 1000000,10 依然很慢TiDB 只是把 ×分片数消掉了,offset 本身还要扫;游标翻页才是根治
把 t_3x_order_item 的 id 与 order_id 写混第 6 步两列金额不等每行明细的 order_id 必须等于订单 id,用第 6 步的等式做自检
在 playground 上尝试 scale-outtiup cluster scale-out 报找不到集群playground 不是 cluster 实例,tiup cluster list 结果为空;本题只读命令
SHOW TABLE t REGIONS 在单容器 Docker 版报错mock tikv 没有 Region第 1 章说过的 pingcap/tidb 单容器限制,换 WSL2 + tiup 或 TiDB Cloud 专属版

第 2 题:用决策树给三个场景做选型判断并说明理由:① 日活百万的评论系统、单表已 8000 万;② 内部后台、数据百万级、大量存储过程;③ 风控:交易库 OLTP + 同时要多维实时分析报表。

第 1 步,先给决策树装上可算的量——不要凭感觉说「数据很大」。用这条 SQL 拿到 MySQL 上单表的真实体量(TiDB 上同样能查):

sql
SELECT table_schema, table_name,
       table_rows                              AS est_rows,
       ROUND(data_length/1024/1024, 1)         AS data_mb,
       ROUND(index_length/1024/1024, 1)        AS index_mb,
       ROUND((data_length+index_length)/1024/1024/1024, 2) AS total_gb
FROM information_schema.tables
WHERE table_name = 't_comment'
ORDER BY total_gb DESC;
1
2
3
4
5
6
7
8

table_rows 对 InnoDB 是估算值(可能偏差 10%~20%),只做量级判断;要精确就 SELECT COUNT(*)(大表很慢,放到从库或低峰期)。

第 2 步,场景①:日活百万的评论系统,单表 8000 万 → 结论:上 TiDB(或至少进入 TiDB 评估名单)。

  • 增速测算(这就是要写进评审文档的数):日活 100 万 × 人均 3 条评论 = 300 万行/天 → 月增约 9000 万行 → 年增约 10.95 亿行。现有 8000 万 ≈ 不到一个月的量,一年后单表破 12 亿。
  • 反推「能不能靠单机 MySQL 撑」:InnoDB 单表经验值几千万~1 亿后 DDL、二级索引维护、慢查询明显恶化 → 撑不住。
  • 反推「继续分库分表行不行」:评论系统有两个高频维度——按 post_id(看某篇文章的评论,还要按点赞数排序)和按 user_id(我的评论)。分片键只能选一个,另一个维度就要走映射表/异构索引表;再加「运营后台按内容关键词 + 时间范围的多维检索」,中间件归并直接失控。
  • TiDB 方案要点:
sql
CREATE TABLE t_comment (
  id BIGINT PRIMARY KEY AUTO_RANDOM(5),     -- 第 4 章:高频写 + 大导入,防尾部热点
  post_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  content VARCHAR(2000),
  like_count INT NOT NULL DEFAULT 0,
  created DATETIME NOT NULL,
  KEY idx_post_like (post_id, like_count),      -- 注意:TiDB 不支持降序索引(第 2 章),只建普通联合索引
  KEY idx_user (user_id, created)
) PRE_SPLIT_REGIONS = 3;                     -- 冷启动不集中写一个 Region
1
2
3
4
5
6
7
8
9
10

起步拓扑(生产最小骨架,第 7 章 7.5):3 PD + 2 TiDB + 3 TiKV;存储估算:单行约 500B × 1 亿 ≈ 50GB 原始,× 3 副本 ≈ 150GB 起,NVMe SSD。加节点随时 scale-out。

  • 被否方案与理由:①「继续 MySQL 分库分表」——能撑容量,但多维查询要再造一套异构索引 + 每次扩容搬数据,团队已经在背这个债;②「只加读从库」——解决不了写入热点和单表体量,DDL 还是要铺 N 份。

第 3 步,场景②:内部后台、数据百万级、大量存储过程 → 结论:别上 TiDB,继续单机 MySQL/PG。

  • 量级:百万级行、total_gb 大概率 < 5GB → 决策树第一个分支「单表 < 千万级且增长可控」直接命中。
  • 改造成本:TiDB 不支持存储过程/函数/触发器/事件(第 2 章清单),「大量存储过程」意味着业务逻辑要从数据库搬回应用层或外部调度——这是重写级别的工作量,换来的收益是零(百万级数据单机毫无压力)。
  • 如果立项动机是「国产化/信创替代」:那不是 TiDB 的题,应该看达梦/PostgreSQL(站内有达梦教程);TiDB 是分布式架构选型,不是国产替代选型。
  • 如果非上 TiDB 不可(比如集团统一技术栈),落地路径只有两条:存储过程逐条改 Java(MyBatis-Plus)+ 外部调度(XXL-JOB);或先把存储过程当黑盒留在 MySQL,用 DM 把表结构同步到 TiDB 做灰度。先做改造评估再决定,不要先建集群。
  • 被否方案与理由:①「TiDB 更先进所以选它」——起步 6~8 台像样的机器 vs 一台 8C32G 云主机,成本和运维技能栈都是净增;②「顺手分个表」——百万级不需要。

第 4 步,场景③:风控,交易 OLTP + 同时多维实时分析报表 → 结论:TiDB(TiKV 行存 + TiFlash 列存)最贴合,这是 HTAP 的教科书场景。

  • 需求拆解:①交易写入要 ACID + 峰值弹性(OLTP);②报表是「大范围扫描 + 多表关联 + 多维聚合」(OLAP);③「实时」意味着不能接受 T+1 抽数(风控要当下就能查可疑模式)。
  • 以前怎么做:MySQL 主库 + 抽数到 ClickHouse/Doris/Hive,链路 = binlog → Kafka → 数仓,延迟分钟到天,双份运维 + 双份数据一致性对账。
  • TiDB 方案:
sql
-- 交易表(OLTP):高频写打散
CREATE TABLE t_risk_txn (
  id BIGINT PRIMARY KEY AUTO_RANDOM(5),
  account_id BIGINT NOT NULL, txn_time DATETIME NOT NULL,
  channel VARCHAR(16), amount DECIMAL(18,2), device_id VARCHAR(64),
  KEY idx_acc_time (account_id, txn_time)
) PRE_SPLIT_REGIONS = 4;

-- 给同一张表加一份列存副本(第 4 章 4.6):不用抽数、不用第二条链路
ALTER TABLE t_risk_txn SET TIFLASH REPLICA 1;
SELECT TABLE_NAME, REPLICA_COUNT, AVAILABLE FROM information_schema.tiflash_replica
WHERE TABLE_NAME = 't_risk_txn';   -- 必须 AVAILABLE=1 才能用

-- 报表侧:点名列存(第 6 章)
SELECT channel, DATE(txn_time) d, COUNT(*) cnt, SUM(amount) amt
FROM t_risk_txn
GROUP BY channel, d ORDER BY amt DESC LIMIT 20;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 拓扑与隔离:3 PD + 2 TiDB + 3 TiKV + 2 TiFlash(TiFlash 副本数 ≥2 才有冗余);OLTP 和报表用不同账号跑在同一集群,靠 Resource Control 隔离:
sql
CREATE RESOURCE GROUP rg_oltp RU_PER_SEC=20000 BURSTABLE;
CREATE RESOURCE GROUP rg_report RU_PER_SEC=1000;
CREATE USER 'risk_app'@'%' RESOURCE GROUP rg_oltp;
CREATE USER 'bi_ro'@'%'   RESOURCE GROUP rg_report;
1
2
3
4
  • 被否方案与理由:①「TiDB 不加 TiFlash」→ 大范围聚合压在 TiKV 上会抢交易资源,行存扫描慢;②「MySQL + ClickHouse」→ 数据链路长、实时性和一致性都要妥协,团队要维护两套存储 + 一条同步链路。

第 5 步,把三题答案压成一页纸(面试/评审可直接用):

场景关键量结论一句话理由被否方案
① 评论系统300 万行/天,年增 10.95 亿TiDB单表已撑不住 + 双维度查询,分片键选不出来继续分库分表(扩容搬数据 + 异构索引)
② 内部后台百万行、<5GB、大量存储过程单机 MySQL/PG杀鸡用牛刀,且存储过程要重写,收益为零上 TiDB(成本/运维净增)
③ 风控 HTAPOLTP + 实时多维报表TiDB + TiFlash一份数据两种引擎,免抽数链路;Resource Control 做隔离只上 TiKV(报表抢交易);MySQL+CK(链路太长)

第 6 步,给自己的团队补一个量化门槛(避免「感觉该上了」):

满足下面任意 2 条,TiDB 的「起步门槛」才划算:
□ 单表 > 5000 万行,或年新增 > 5000 万行
□ 查询维度 ≥ 2 个且都高频(分片键选不出来)
□ 需要跨库 ACID(现在在用/准备用 Seata)
□ 需要「同一份数据的实时分析」,不接受 T+1
□ 一年内必须扩容,且不能接受「搬数据 + 双写 + 切流」
1
2
3
4
5
6

第 3 题:针对 3.4 的「网络与延迟」,思考:一个对单行读延迟极度敏感(要求 < 1ms)的缓存前置场景,为什么 TiDB 未必比「Redis + 单机 MySQL」更合适?

第 1 步,先把「一次 TiDB 单行主键点查的耗时构成」拆开——这是回答的关键,别停在「分布式所以慢」:

客户端 ──MySQL协议──► TiDB Server ──gRPC──► TiKV Region Leader ──► 返回
        ①解析+优化        ②跨节点 RPC(一次网络往返)    ③读 MemTable/BlockCache + 快照读(MVCC,无需 Raft 多数派)

· 读单行只需访问 Leader 副本一个节点,不触发 Raft 多数派;
  但 ② 这一趟网络往返是「跑不掉」的:TiDB 与 TiKV 是计算/存储分离的两个进程(哪怕同机也是 RPC)。
· 写单行才要 Raft 多数派确认(默认 3 副本里的 2 个),同机房约 1~2 个 RTT。
1
2
3
4
5
6

第 2 步,确认 TiDB 已经为这种查询准备了最快路径——PointGet / TablePointGet:

sql
CREATE TABLE t_3x_kv (id BIGINT PRIMARY KEY, v VARCHAR(64));
INSERT INTO t_3x_kv VALUES (1,'a'),(2,'b'),(3,'c');
EXPLAIN SELECT v FROM t_3x_kv WHERE id = 2;      -- 主键等值 → PointGet/TablePointGet(跳过完整算子树)
EXPLAIN ANALYZE SELECT v FROM t_3x_kv WHERE id = 2;
1
2
3
4
text
| id                   | estRows | task | access object | operator info |
Point_Get_1           | 1.00    | root | table:t_3x_kv, indexed value, ... |
(EXPLAIN ANALYZE 里 time≈0.5~2ms 量级,本机 playground 与生产集群差别很大,以实测为准)
1
2
3

也就是说 TiDB 不是「把点查当大查询跑」,它专门做了短路优化。但它省不掉跨节点 RPC 和 RPC 排队,这就是 1ms 门槛的来源。

第 3 步,做可跑的对照实验(延迟只有实测才算数)。用一段 Java 计时脚本打 2000 次主键点查:

java
// TidbPointLatency.java   编译:javac TidbPointLatency.java
// 运行:java -cp .;mysql-connector-j-8.3.0.jar TidbPointLatency
long[] ns = new long[2000];
for (int i = 0; i < ns.length; i++) {
    long t0 = System.nanoTime();
    try (PreparedStatement ps = conn.prepareStatement("SELECT v FROM t_3x_kv WHERE id=?")) {
        ps.setLong(1, (i % 3) + 1);              // 循环打同 3 行,让 BlockCache 命中,测的是「最好情况」
        ps.executeQuery();
    }
    ns[i] = System.nanoTime() - t0;
}
java.util.Arrays.sort(ns);
System.out.printf("avg=%.3fms p50=%.3fms p99=%.3fms max=%.3fms%n",
    java.util.Arrays.stream(ns).average().orElse(0)/1e6,
    ns[(int)(ns.length*0.5)]/1e6, ns[(int)(ns.length*0.99)]/1e6, ns[ns.length-1]/1e6);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第 4 步,把结果和典型量级对照(下面是社区常见实测区间,你的网络拓扑决定最终数字,务必以第 3 步实测为准):

路径P50P99 毛刺来源
Redis GET(同机房,pipeline 关闭)0.05~0.3ms大 key、RDB/AOF fork、 eviction、单线程排队
单机 MySQL 主键点查(buffer pool 命中)0.2~0.8ms锁等待、刷脏、慢 SQL 抢 CPU
TiDB 主键点查(同机房,BlockCache 命中)0.5~2msRegion Leader 迁移/调度、PD TSO 获取、TiKV 线程池排队、GC、Raft 日志同步抖动

第 5 步,回答「为什么未必更合适」的四条实质原因:

  1. 多一跳网络:计算存储分离让「最少一次 RPC」成为下限;单机 MySQL 和 Redis 都在一个进程内完成。1ms 的预算里,RPC 就吃掉大半。
  2. P99 才是这类场景的生死线:单行点查的 SLA 通常看 P99/P999。TiDB 的毛刺源(Region 调度、Leader 迁移、TSO、GC、副本补齐)比单机 MySQL 多得多;Redis 毛刺源最少。
  3. 架构定位不同:TiDB 是「带 ACID 的分布式关系数据库」,它给你的是强一致 + 分布式事务 + 弹性;Redis 是「内存数据结构缓存」,它给你的是亚毫秒 + 极高空闲 QPS,代价是没有 SQL、没有跨命令事务(只有 MULTI/EXEC 的弱保证)、数据结构要你自己设计。
  4. 成本方向相反:为了 <1ms 把 TiDB 的 TiDB/TiKV 部署在同机房低延迟网络、甚至上 NVMe + 高配,这笔钱花下去,点查延迟仍然不如一个 4GB 内存的 Redis。

第 6 步,给出这类场景的正解架构(别把答案写成「TiDB 不行」):

请求 → Redis(热点对象,命中率目标 > 95%,亚毫秒)
         ↑ miss 时回填,写路径「先更新 TiDB/MySQL,再删缓存」
      → TiDB(持久化 + SQL 灵活性 + 分布式事务 + 多条件查询)

TiDB 负责「算得对、存得下、撑得住增长」;Redis 负责「读得快」。
两者不互相替代 —— 上了 TiDB 也照样要缓存层。
1
2
3
4
5
6

第 7 步,如果确实要在 TiDB 上压点查延迟,按性价比排序的手段(都有具体开关,别只会加机器):

sql
-- ① 让读走最新但不必严格线性一致(v6.3+,会话/全局):适合「能容忍极短陈旧」的展示类读
SET SESSION tidb_replica_read = 'read-latest';   -- 可选值:strict(默认)/only-replica/adaptive/read-latest

-- ② 把热点表的 Leader 拉到离应用近的节点(第 4 章 Placement)
CREATE PLACEMENT POLICY near_app LEADER_CONSTRAINTS="[+zone=shanghai]" FOLLOWERS=2;
ALTER TABLE t_3x_kv PLACEMENT POLICY=near_app;
-- 注意:playground 只有一台 TiKV 且没打 zone label,上面的策略会处于「无法满足」状态:
-- SHOW PLACEMENT 能看到绑定关系,但 scheduling_state 会一直停在 PENDING/INPROGRESS(凑不齐副本);
-- 真实效果要在多 TiKV + 打过 label 的集群上看

-- ③ 缩短 SQL 解析与事务链路:用 PointGet 形态(主键/唯一索引等值),避免 SELECT *,
--    事务尽量短(避免长事务持锁 + 撞 GC 生命周期,第 5 章)

-- ④ 定位瓶颈看数据别看感觉:Dashboard → SQL 语句分析,或命令行
SELECT DIGEST_TEXT, EXEC_COUNT, AVG_LATENCY/1000 AS avg_us, MAX_LATENCY/1000 AS max_us
FROM information_schema.statements_summary
WHERE SCHEMA_NAME = 'test' AND DIGEST_TEXT LIKE '%t_3x_kv%'
ORDER BY AVG_LATENCY DESC LIMIT 5;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第 8 步,面试式结论(一句话背下来):

「TiDB 换掉的是分库分表的复杂度,不是缓存层的必要性。单行点查它已经做了 PointGet 短路,但计算存储分离让至少一次跨节点 RPC 成为延迟下限,P99 还受 Region 调度影响;所以 <1ms 的缓存前置场景仍然是 Redis 更合适,TiDB 放在缓存后面的持久化 + 强一致这一层。」

第 4 题:用自己的话回答面试题:「我们都上 TiDB 了,为什么还要关心业务单号用雪花 ID 而不是数据库自增?」

第 1 步,先把「TiDB 自增只保证唯一」这件事说清(引用第 2 章 Q1 的实测,别只讲道理):

sql
CREATE TABLE t_2x_id_def (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(10));
CREATE TABLE t_2x_id_c1  (id BIGINT PRIMARY KEY AUTO_INCREMENT AUTO_ID_CACHE = 1, name VARCHAR(10));
INSERT INTO t_2x_id_def (name) VALUES ('a'),('b'),('c'),('d'),('e'),('f');
INSERT INTO t_2x_id_c1  (name) VALUES ('a'),('b'),('c'),('d'),('e'),('f');
SELECT MAX(id) AS def_max, (SELECT MAX(id) FROM t_2x_id_c1) AS c1_max FROM t_2x_id_def;
-- 然后重启 TiDB 实例(tiup 同 tag 重启)后再各插一行
INSERT INTO t_2x_id_def (name) VALUES ('g');
INSERT INTO t_2x_id_c1  (name) VALUES ('g');
SELECT id, name FROM t_2x_id_def ORDER BY id;
SELECT id, name FROM t_2x_id_c1  ORDER BY id;
1
2
3
4
5
6
7
8
9
10
text
# 默认 AUTO_ID_CACHE(批量 30000):重启后号直接跳到 30001
def_max=6  →  重启后插入的 g 得到 30001
# AUTO_ID_CACHE=1:连续
def_max=6  →  重启后插入的 g 得到 7
1
2
3
4

→ 默认配置的 TiDB 自增:全局唯一 ✓、连续 ✗、单调递增 ✗(多 TiDB 实例各自持有一段号,见第 2 章 --db 2 实验)。而 MySQL 单机自增在你的直觉里是「连续的」,很多人把直觉带进了 TiDB 就出事。

第 2 步,说清业务单号和主键是两个东西(这是本题的题眼):

维度技术主键 id业务单号 order_no
面向对象数据库自己(聚簇、索引、外键、分片定位)人、下游系统、对账、客服、发票
需要可读不需要需要(2026010112345678 带日期、渠道)
需要幂等无所谓需要:重复下单靠 order_no 唯一索引挡住
需要跨系统一致不需要需要:出库单、支付流水、退款都引用同一个号
生命周期建库才有下单瞬间就存在,甚至先落库前就要返回给前端

关键推论:order_no 天然要在事务提交之前就生成(前端要立刻回显、支付要先建流水)——数据库自增做不到(它是 INSERT 之后才知道值)。这一条和 TiDB 无关,和 MySQL 也无关。

第 3 步,给「连续」标个价(成本视角,3.4 的延伸):想要全局连续/单调,TiDB 侧只有 AUTO_ID_CACHE = 1:

sql
SHOW CREATE TABLE t_2x_id_c1\G     -- 能看到 AUTO_ID_CACHE=1
1
  • AUTO_ID_CACHE=1 意味着不能本地缓存号段,发号要走中心分配器(v6.4+)→ 高并发写入时发号变成串行瓶颈,吞吐明显低于默认批量分配。
  • 所以「要连续」不是免费的:你要用吞吐换语义。业务真的需要连续编号(发票号、凭证号)时,正确做法是应用层号段表 + 悲观锁/乐观锁发号,而不是把主键自增调成连续。

第 4 步,说清 TiDB 自增/AUTO_RANDOM 的一个隐蔽坑:值可能超过 JS 安全整数(这一条答出来面试基本就稳了):

sql
CREATE TABLE t_3x_ar (id BIGINT PRIMARY KEY AUTO_RANDOM(5), name VARCHAR(10));
INSERT INTO t_3x_ar (name) VALUES ('a'),('b'),('c');
SELECT id, name, (id > 9007199254740991) AS exceeds_js_safe FROM t_3x_ar ORDER BY id;
1
2
3
text
id                 | name | exceeds_js_safe
1152921504606846978 | a    |      1
1152921504606846979 | b    |      1
...
(具体 id 由随机分片位决定,必然远大于 2^53,以你实测为准)
1
2
3
4
5
  • AUTO_RANDOM 为了打散,把高位留给 shard 位 → 生成的值天生就是 10^18 量级,而 Number.MAX_SAFE_INTEGER = 9007199254740991(约 9×10^15)。
  • 后果:如果这个 id 被当作 order_no 直接 JSON 返回给前端,浏览器解析时会丢精度(...6978 变成 ...6800),前端拿着丢精度的号来回传 → 查不到/改错单。Java 侧 Long 没事,所以后端测试全绿,到前端才炸,非常难查。
  • 对策(任选,别裸奔):
java
// ① Jackson:Long 序列化成字符串(全站一刀切,最稳)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
// ② 或业务单号根本不用数据库 id:应用层雪花/号段 + 日期渠道前缀,天然可读、可控长度
String orderNo = "20260101" + channelCode + snowflake.nextIdStr();
1
2
3
4
5

第 5 步,再补一个多活/数据回流的理由(第 7 章视角):

· TiCDC / DM 做「TiDB → 下游」或双向同步时,如果发号权在数据库:
  下游库的自增序列不知道上游用了哪些号 → 会撞;回流场景尤其明显。
· 应用层雪花把 workerId/datacenterId 按机房分配 → 天然分域不撞,
  这也是你在 yunlan 第九章做全局 ID 的原始动机,上 TiDB 后这个动机依然成立。
1
2
3
4

第 6 步,30 秒答案(可直接说出口):

「三个层次。第一,语义不同:id 是给数据库用的,order_no 是给人和下游系统用的,业务单号必须在下单瞬间就生成、要带日期渠道、要靠唯一索引做幂等——数据库自增是 INSERT 之后才拿到值,时序上就不满足。第二,TiDB 自增只保证全局唯一,不保证连续也不保证单调(默认批量 30000,重启直接跳号,我实测过 6 → 30001);要连续得 AUTO_ID_CACHE=1,那是拿吞吐换语义。第三,防热点用的 AUTO_RANDOM 会生成 10^18 量级的值,超过 JS 的 2^53 安全整数,直接当前端单号会丢精度。所以业务单号继续用应用层雪花,数据库这边用 AUTO_RANDOM 做聚簇主键打散,两者各干各的事。」

这题(第 4 题)的坑:

坑现象说明
把 AUTO_RANDOM 的 id 直接当业务单号返给前端前端展示的号被四舍五入,按号查询失败超过 2^53 必丢精度,Long 序列化成 String
以为「TiDB 自增和 MySQL 一样连续」对账/发票校验发现断号就报错只有 AUTO_ID_CACHE=1 才连续,且有吞吐代价
用自增 id 做游标分页 WHERE id > ?翻页结果重复或漏行多 TiDB 实例并发写时 id 不保证全局单调(各自持有号段),游标应该用「业务时间 + 唯一键」复合排序
显式往 AUTO_RANDOM 列插值报 Error 1062/1064 或被静默分配显式插入需 SET GLOBAL allow_auto_random_explicit_insert = TRUE(第 4 章)
← 第2章 MySQL兼容性与差异清单第4章 数据建模与分布式特性 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系我们
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节