外观
分库分表避坑实录:ShardingSphere 分片键与扩容迁移
单表过亿后,为什么分库分表仍然要谨慎
当订单单表从千万增长到数亿,仅靠 MySQL 索引优化往往已经无法同时满足低延迟写入、快速查询和运维窗口要求。常见现象是:二级索引树变高,范围扫描回表放大;热点更新导致行锁、间隙锁竞争;一次在线 DDL 或备份恢复都需要数小时。此时分库分表可以把数据和请求打散到多个物理库表,但它不是简单加一层中间件,而是重新定义数据路由、主键生成、扩容迁移和查询边界。
ShardingSphere 的价值在于提供标准化分片路由、读写分离、分布式事务和数据加密能力,但如果分片键选错,后续迁移成本会非常高。实战中应先判断瓶颈是否真实存在:能否通过冷热数据归档、联合索引、覆盖索引、SQL 改写、参数调优解决?只有单表规模、写入 QPS 和业务增长预期都明确超过单实例容量,再进入分片设计。
分片键怎么选:订单号、用户 ID 还是商户 ID
分片键的第一原则是让核心查询携带分片键,第二原则是避免热点,第三原则是为未来扩容预留空间。以订单系统为例,C 端最高频查询是“查询我的订单列表”和“根据订单号查订单”,运营侧还可能按商户、时间、状态筛选。若只用 user_id 分片,用户维度查询定位单分片;但订单号查询若无法解析出用户归属,就可能扫所有分片。若只用 order_id 取模,用户订单列表会变成跨片查询;若按商户分片,大客户又可能形成严重热点。
更稳妥的设计是:以 user_id 作为路由分片键,订单号使用可反查用户信息的业务号段,或建立 order_id -> user_id 的映射索引。时间字段用于归档和局部排序,不单独承担均匀分片职责。
sql
-- 逻辑订单表:实际会映射到 ds_0.order_0、ds_0.order_1 等物理表
CREATE TABLE `t_order` (
`id` BIGINT UNSIGNED NOT NULL COMMENT '雪花算法生成的主键',
`order_id` VARCHAR(32) NOT NULL COMMENT '业务订单号',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '分片键',
`merchant_id` BIGINT UNSIGNED NOT NULL COMMENT '商户 ID,用于辅助查询',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',
`amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`),
KEY `idx_user_status_time` (`user_id`,`status`,`created_at`),
KEY `idx_merchant_time` (`merchant_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 订单明细通过 order_id 关联,并与主表使用同一 user_id 作为分片键
CREATE TABLE `t_order_item` (
`id` BIGINT UNSIGNED NOT NULL,
`order_id` VARCHAR(32) NOT NULL,
`user_id` BIGINT UNSIGNED NOT NULL,
`sku_id` BIGINT UNSIGNED NOT NULL,
`quantity` INT NOT NULL,
`price` DECIMAL(12,2) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_user_order` (`user_id`,`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意:物理库中唯一键只能约束单个分片,不能把 UNIQUE(order_id) 误认为分布式唯一保障。若订单号不是分片键,又要求全局唯一,应由业务号段、雪花算法或统一发号器保证;需要强校验时可额外使用全局映射表。
ShardingSphere 分片配置:让路由、绑定表和广播表各司其职
下面配置使用 2 个库、每个库 4 张表,共 8 个分片。实际分片数建议按未来 2 到 3 年容量规划,不要一开始就创建过多物理表;但算法要预留扩容路径,例如采用一致性哈希或基因法,而不是简单写死 user_id % 8。
yaml
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:mysql://mysql-0:3306/ds_0?useSSL=false&serverTimezone=Asia/Shanghai
username: app_user
password: App@Passw0rd
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:mysql://mysql-1:3306/ds_1?useSSL=false&serverTimezone=Asia/Shanghai
username: app_user
password: App@Passw0rd
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..3}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_db_mod
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_table_mod
t_order_item:
actualDataNodes: ds_${0..1}.t_order_item_${0..3}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_db_mod
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_table_mod
bindingTables:
- t_order,t_order_item
broadcastTables:
- t_dict_region
shardingAlgorithms:
order_db_mod:
type: MOD
props:
sharding-count: 2
order_table_mod:
type: MOD
props:
sharding-count: 4
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1绑定表用于保证主表和子表落在同一分片,避免关联查询产生笛卡尔积。地区、字典这类小表适合配置成广播表,每个库存一份,查询本地完成。
sql
-- 推荐:携带分片键,路由到单个分片
SELECT order_id, amount, status, created_at
FROM t_order
WHERE user_id = 10001
AND status = 2
ORDER BY created_at DESC
LIMIT 20;
-- 推荐:绑定表关联,ShardingSphere 可在同一分片内完成 JOIN
SELECT o.order_id, i.sku_id, i.quantity, i.price
FROM t_order o
JOIN t_order_item i
ON o.order_id = i.order_id
AND o.user_id = i.user_id
WHERE o.user_id = 10001
ORDER BY o.created_at DESC
LIMIT 50;
-- 谨慎:不带分片键的运营分页,会扇出到所有分片再归并
SELECT order_id, user_id, merchant_id, amount
FROM t_order
WHERE merchant_id = 88 AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 10000, 20;扩容迁移实战:双写、校验、追平和切流
从 8 分片扩到 16 分片时,最忌讳直接停机 INSERT INTO ... SELECT 后立刻切配置。数据量越大,停机时间和回滚风险越高。推荐采用灰度双写加增量追平方案:先通过 ShardingSphere 构建新旧两套逻辑规则,应用写入同时投递旧路由和新路由;历史数据用分批任务迁移;再通过 binlog 订阅补偿延迟;最后比对校验并按用户灰度读新。
历史迁移必须按主键有序、小批量提交,避免大事务拖垮源库。每批迁移前后记录最大主键和校验点,失败可重跑。
sql
-- 源端按主键游标分批抽取,避免 OFFSET 深分页
SELECT id, order_id, user_id, merchant_id, status, amount, created_at, updated_at
FROM t_order
WHERE id > ?
ORDER BY id
LIMIT 1000;
-- 目标端幂等写入;迁移前应先禁用外键,结束后再统一校验
INSERT INTO t_order
(id, order_id, user_id, merchant_id, status, amount, created_at, updated_at)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
amount = VALUES(amount),
updated_at = VALUES(updated_at);
-- 按分片做数量、金额和最大更新时间校验
SELECT user_id DIV 4 MOD 2 AS db_no,
user_id MOD 4 AS table_no,
COUNT(*) AS cnt,
COALESCE(SUM(amount),0) AS total_amount,
MAX(updated_at) AS max_updated_at
FROM t_order
WHERE id BETWEEN ? AND ?
GROUP BY db_no, table_no;如果使用的是固定取模算法,扩容后大量数据需要搬迁;更理想的方案是在首次分片时引入 1024 或 4096 个虚拟槽,物理节点只负责槽位范围。扩容时迁移槽,而不是重算全量用户。
跨片查询优化:能避免全扫,就不要依赖中间件兜底
跨片查询并不等于不能用,但要明确它的代价。ShardingSphere 会把一条逻辑 SQL 改写成多个物理 SQL 并行执行,再做归并排序。等值查询影响较小;ORDER BY + LIMIT 深分页会让每个分片都取到 offset + n 行;不带分片键的 COUNT、SUM 还会占用连接池和应用内存。
优化方式分三类:第一,核心链路强制携带分片键;第二,商户、运营、报表等多维查询交给独立索引服务或异构数仓;第三,对低频条件使用 ES、ClickHouse 或汇总表,只把 MySQL 作为事务库。
sql
-- 深分页反例:每个分片都可能扫描 10020 行
SELECT * FROM t_order
WHERE status = 2
ORDER BY created_at DESC
LIMIT 10000, 20;
-- 游标分页改写:记录上一页最后一条记录的时间和主键
SELECT id, order_id, user_id, amount, created_at
FROM t_order
WHERE status = 2
AND (created_at, id) < ('2026-03-01 10:00:00', 9000123)
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- 运营报表不要在事务分片库上实时大范围聚合,可维护按天汇总表
CREATE TABLE `t_order_stat_daily` (
`stat_date` DATE NOT NULL,
`merchant_id` BIGINT UNSIGNED NOT NULL,
`order_count` INT NOT NULL DEFAULT 0,
`total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0.00,
PRIMARY KEY (`stat_date`,`merchant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;避坑总结:分库分表最容易踩的 6 个问题
错误现象:按
order_id分片后,用户订单列表 RT 抖动。 根因是用户查询扇出所有分片。修正方法是改用user_id路由,并让订单号包含用户基因或维护映射索引。错误现象:应用认为
UNIQUE(order_id)能保证全局唯一,实际出现重复订单。 单机唯一索引只在当前物理表生效。修正方法是使用统一发号器,并在写入前做业务幂等键校验。错误现象:主表和明细表 JOIN 后请求暴涨。 通常是没有配置绑定表或两表分片键不一致。修正方法是让
t_order与t_order_item同库同表路由,关联条件同时带上user_id和order_id。错误现象:扩容迁移时大事务把源库复制延迟打高。 常见于一次性导出百万行或单事务大批量写入。修正方法是按主键游标每批 500 到 2000 行,配合限速、断点和 binlog 延迟监控。
错误现象:跨片深分页越翻越慢。 因为每个分片都要取
OFFSET + N行再归并。修正方法是改为游标分页,运营后台走搜索或报表系统。错误现象:分片表上的全局事务、外键和存储过程失效。 分库后数据库无法替你维护跨库约束。修正方法是用最终一致消息、本地消息表或 Seata 处理事务;外键下沉到应用校验,并定期运行数据一致性巡检。
分库分表的本质不是把一张表机械拆开,而是围绕访问模式设计数据边界。只要在分片键、扩容槽位、索引和跨片查询上提前规划,ShardingSphere 可以显著降低改造成本;反之,仓促分片只会把简单的 SQL 问题升级为长期的数据迁移和一致性问题。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。