零基础实战:使用 pt-archiver 实现 MySQL 千万级大表的水平分表(Hash分片)
本文适合人群:MySQL新手、想低成本实践数据库分表的开发者
环境要求:MySQL 5.7+、Linux系统(建议CentOS/Ubuntu)
你将学会:
- 📦 1分钟生成百万测试数据
- 🔧 安全拆解大表为5个哈希分片表
- 🛠 零停机分表迁移实操技巧
一、为什么需要分表?
假设您有一张 订单表 orders
数据达到 1000万行,面临以下问题:
1️⃣ 查询变慢:SELECT * FROM orders WHERE user_id='xxx'
需要扫描全表
2️⃣ 维护困难:索引膨胀、备份耗时
3️⃣ 容灾风险:单表损坏可能导致业务瘫痪
解决方案:
👉 Hash分表:将数据按 user_id
的哈希值分散到 orders_0
~orders_4
五个分片中,查询永远只需扫一张小表!
二、5分钟快速搭建测试环境
环境准备清单
-
安装 MySQL 5.7(已安装可跳过)
# Ubuntusudo apt install mysql-server-5.7# CentOSsudo yum install mysql-community-server
-
安装 Percona Toolkit
# 所有Linux版本通用wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.debsudo dpkg -i percona-release_latest.$(lsb_release -sc)_all.debsudo apt-get updatesudo apt-get install percona-toolkit
-
创建测试数据库
CREATE DATABASE test_sharding;USE test_sharding;
-
开启LOCAL INFILE参数
允许客户端从本地文件系统中读取数据文件,并将其内容加载到远程 MySQL 服务器的表中,使用pt-archiver分片导入的前提参数。
local_infile = on
三、1分钟生成百万测试数据
复制下列代码到 MySQL客户端,快速生成100万测试订单:
-- 创建原始订单表
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(20) NOT NULL, -- 分片键(关键字段)
amount DECIMAL(10,2) NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 快速填充100万数据(耗时约30秒)
INSERT INTO orders (user_id, amount)
SELECT
CONCAT('user_', FLOOR(RAND() * 1000000)),
ROUND(RAND() * 100, 2)
FROM
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) a,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) b,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) c,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) d,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) e,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) f,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) g,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) h,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) i;
执行成功后验证:
SELECT COUNT(*) FROM orders;
四、哈希分表实战:拆解大表为5个分片
1. 创建分片表
-- 创建5个分片表(结构与原表一致)
CREATE TABLE orders_0 LIKE orders;
CREATE TABLE orders_1 LIKE orders;
CREATE TABLE orders_2 LIKE orders;
CREATE TABLE orders_3 LIKE orders;
CREATE TABLE orders_4 LIKE orders;
2. 关键分片逻辑
通过 哈希函数 CRC32 将数据均匀分散到分片中:
CRC32(user_id) % 5 = 0 → 存入 orders_0
CRC32(user_id) % 5 = 1 → 存入 orders_1
... 以此类推
3. 使用 pt-archiver 迁移数据
将原表数据按 Hash 规则迁移到分片表(不锁表、不影响线上业务):
迁移到 orders_0 分片
/opt/percona-toolkit-3.6.0/bin/pt-archiver --source h=127.0.0.1,P=3306,u=root,p=密码,D=test_sharding,t=orders --dest h=127.0.0.1,P=3306,u=root,p=密码,D=test_sharding,t=orders_0 --where "CRC32(user_id) %5=0" --limit 500 --commit-each --bulk-insert --no-delete --no-check-charset
迁移其他分片
修改 --where
条件和目标表名,依次执行:
# orders_1(余数1)
pt-archiver ... --where "CRC32(user_id) %5=1" --dest t=orders_1
# orders_2(余数2)
pt-archiver ... --where "CRC32(user_id) %5=2" --dest t=orders_2
...(省略orders_3/4)
五、分片结果验证
1. 核对各分片数据量
SELECT
table_name AS '分片表',
FORMAT(table_rows,0) AS '数据量'
FROM information_schema.tables
WHERE table_schema = 'test_sharding'
AND table_name LIKE 'orders_%';
期望结果(误差应小于2%):
+-----------+-----------+
| 分片表 | 数据量 |
+-----------+-----------+
| orders_0 | 390,780 |
| orders_1 | 390,112 |
| orders_2 | 389,778 |
| orders_3 | 388,776 |
| orders_4 | 389,778 |
+-----------+-----------+
2. 数据完整性检验
-- 所有分片总和 = 原表总数
SELECT
(SELECT COUNT(*) FROM orders_0) +
(SELECT COUNT(*) FROM orders_1) +
(SELECT COUNT(*) FROM orders_2) +
(SELECT COUNT(*) FROM orders_3) +
(SELECT COUNT(*) FROM orders_4) AS total_count;
-- 应与原表数据量一致
SELECT COUNT(*) FROM orders;
注意!pt-archiverBug不会迁移max(id)那条数据,需要手动完成最后一行数据的迁移
定位数据差异
SELECT *
FROM orders
WHERE NOT EXISTS (
SELECT 1 FROM orders_0 WHERE orders_0.id = orders.id UNION ALL
SELECT 1 FROM orders_1 WHERE orders_1.id = orders.id UNION ALL
SELECT 1 FROM orders_2 WHERE orders_2.id = orders.id UNION ALL
SELECT 1 FROM orders_3 WHERE orders_3.id = orders.id UNION ALL
SELECT 1 FROM orders_4 WHERE orders_4.id = orders.id
)
LIMIT 1;
+---------+-----------+--------+---------------------+
| id | user_id | amount | create_time |
+---------+-----------+--------+---------------------+
| 1953125 | user_9823 | 39.33 | 2025-03-14 17:03:24 |
+---------+-----------+--------+---------------------+
使用insert 插入最后一条数据到orders_4表
INSERT INTO orders_4 (id, user_id, amount, create_time)
SELECT id, user_id, amount, create_time
FROM orders
WHERE id=1953125;
六、常见问题排雷指南
1. 迁移性能慢(每秒不到1000条)
-
✅ 优化技巧:
-- 增加批量大小(视内存调整) --limit 1000 \ --bulk-insert \
2. 主键冲突错误
-
✅ 解决方案:
- 迁移前清空分片表:
TRUNCATE orders_0
- 或使用
--replace
参数覆盖已有数据
- 迁移前清空分片表:
3. 数据分布不均匀
-
✅ 原因排查:
- 检查分片键是否有倾斜(如某user_id占比过高)
- 改用 一致性哈希算法(如
SHA1(user_id)
)
七、分表后如何查询数据?
通过业务代码计算目标分片,直连对应表:
# Python示例:按user_id定位分片
user_id = "user_12345"
shard_no = crc32(user_id.encode()) % 5 # 计算分片编号
table_name = f"orders_{shard_no}"
query = f"SELECT * FROM {table_name} WHERE user_id = %s"
总结
通过本方案,我们实现了:
- 🚀 100万数据安全拆分:无锁迁移、分钟级完成
- 📊 分布式查询提速:查询性能提升5~10倍
- 🔒 数据零丢失保证:每一步操作均可回滚验证
下一篇预告:
1、如何完成垂直分表?将单表中高频访问的"热数据"与低频使用的"冷数据"分离存储!
2、如何可以将分片逻辑封装到中间件(如 MyCAT),彻底告别单表性能瓶颈!