单表5000万数据如何破局?这才是分库分表的正确打开方式
当你的单表数据突破5000万,查询从毫秒变成秒级,是时候考虑分库分表了。
一个真实案例:订单表5000万的噩梦
去年我们接手一个电商项目,订单表数据量达到5200万。一个简单的SELECT * FROM orders WHERE user_id = 12345 LIMIT 10,从原来的30ms飙升到3.2秒。索引优化、SQL调优都试过了,收效甚微。
这就是单表数据量过大带来的性能衰减魔咒——B+Tree索引层级加深,IO次数增加,内存放不下热点数据。
破局点:分库分表
分库分表不是银弹,但确实是解决大数据量下数据库性能问题的核心手段。
第一步:明确分表策略
我们选择了水平分表,按user_id取模:
-- 原表结构CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `order_no` varchar(64) NOT NULL, `amount` decimal(10,2) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`)) ENGINE=InnoDB;-- 分表后:拆成16张表 orders_0 ~ orders_15-- 分片规则:user_id % 16第二步:分库分表落地
分库数量:4个库,每个库4张表 = 16张表
// 分片算法public String getTableName(Long userId) { int dbIndex = userId % 4; // 库路由 int tableIndex = userId % 16; // 表路由 return "db_" + dbIndex + ".orders_" + tableIndex;}建表脚本:
-- 在4个库中分别执行CREATE TABLE `orders_0` LIKE `orders_template`;CREATE TABLE `orders_1` LIKE `orders_template`;-- ... 直到 orders_15第三步:使用ShardingSphere-JDBC
引入依赖:
org.apache.shardingsphere shardingsphere-jdbc-core-spring-boot-starter 5.3.2 配置分片规则:
spring: shardingsphere: datasource: names: ds0,ds1,ds2,ds3 ds0: url: jdbc:mysql://localhost:3306/db_0 username: root password: 123456 # ds1,ds2,ds3 类似配置 rules: sharding: tables: orders: actual-data-nodes: ds$->{0..3}.orders_$->{0..15} table-strategy: standard: sharding-column: user_id sharding-algorithm-name: user-mod sharding-algorithms: user-mod: type: MOD props: sharding-count: 16第四步:改造业务代码
插入数据:
@Servicepublic class OrderService { @Autowired private OrderMapper orderMapper; public void createOrder(Order order) { // ShardingSphere自动根据user_id路由到对应分片 orderMapper.insert(order); } public Order getOrderByUser(Long userId, Long orderId) { // 查询条件必须包含分片键 user_id return orderMapper.selectByUserIdAndOrderId(userId, orderId); }}跨分片查询:
// 不包含分片键的查询,会扫描所有分片public List getOrdersByAmount(BigDecimal amount) { // 这种查询会广播到16张表,性能较差 return orderMapper.selectByAmount(amount);}// 解决方案:建立冗余表或使用ES 第五步:数据迁移方案
使用停机迁移(凌晨2-4点):
-- 1. 创建分表-- 2. 暂停业务写入-- 3. 迁移数据INSERT INTO db_0.orders_0 SELECT * FROM old_db.orders WHERE user_id % 16 = 0;INSERT INTO db_0.orders_1 SELECT * FROM old_db.orders WHERE user_id % 16 = 1;-- ... 共16条INSERT-- 4. 切换数据源配置-- 5. 恢复业务如果要求不停机,用binlog同步 + 双写方案。
效果对比
指标 | 优化前(单表5200万) | 优化后(16张分表) |
单条查询(带分片键) | 3.2秒 | 25ms |
插入操作 | 120ms | 18ms |
统计查询(count) | 8.7秒 | 0.6秒
|
索引大小 | 2.8GB | 每表180MB |
关键避坑指南
1. 分片键必须慎选
选错分片键,80%的查询都会变成全表扫描。我们选user_id是因为90%的业务查询都带用户维度。
2. 全局ID生成
不能用自增ID,改用雪花算法:
public class SnowflakeIdWorker { private static final SnowflakeIdWorker INSTANCE = new SnowflakeIdWorker(1, 1); public static long nextId() { return INSTANCE.nextId(); }}3. 避免跨分片JOIN
-- 禁止这种查询SELECT o.*, u.name FROM orders_0 o JOIN users u ON o.user_id = u.id;改为冗余存储或多次查询聚合。
4. 扩容方案
用一致性哈希替代取模,扩容时只需迁移1/n的数据:
// 一致性哈希路由public String getTableNameConsistentHash(Long userId) { int hash = Hashing.consistentHash(userId, 32); // 32个虚拟节点 return "orders_" + (hash % 16);}什么时候该分库分表?
不是所有表都要分。我们的判断标准:
- 单表数据 > 2000万,准备分表方案
- 单表数据 > 5000万,必须分表
- 单表数据 > 1亿,分库分表联合使用
小表示例(配置表、字典表)保持单表即可。
总结
分库分表的核心三要素:
- 分片键——决定90%的查询性能
- 分片数量——2的幂次方,方便扩容
- 路由算法——取模 or 一致性哈希
记住一句话:分库分表不是技术炫耀,是业务增长到一定阶段的必然选择。 提前规划,从容应对。
你的单表数据到多少了?欢迎在评论区交流。
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有
