Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql分库分表

MySql分库分表深度指南之从策略到落地

作者:廋到被风吹走

文章介绍了MySQL分库分表的策略及实施方法,包括分片键设计、中间件选型(如ShardingSphere、Mycat),以及如何处理跨分片查询和数据迁移,通过案例和最佳实践,文章展示了如何解决高并发、大数据量下的查询和事务问题,实现数据自治和高可用,感兴趣的朋友跟随小编一起看看吧

MySQL 分库分表深度指南:从策略到落地

当单表数据突破 5000万行 时,B+树索引深度达到5层,磁盘I/O暴增,简单查询耗时超10秒。此时分库分表成为必然选择。本文将详解 ShardingSphere/Mycat 中间件选型、分片键设计哲学及 Snowflake 基因改造方案。

一、分库分表核心策略与时机

1.1 什么时候必须分库分表?

触发条件

架构演进路径

  1. 垂直分库:按业务拆分(用户库、订单库),解决耦合问题
  2. 垂直分表:大字段拆分到扩展表,单表体积减少60%
  3. 水平分表:单库内分表,缓解单表压力
  4. 水平分库:跨实例分片,支撑亿级数据

1.2 分片类型对比

分片类型拆分维度优点缺点适用场景
垂直分库业务模块业务清晰,隔离故障跨库事务复杂微服务化改造
垂直分表字段冷热减少单表大小,提升缓存命中率增加 JOIN 查询大字段(text/blob)分离
水平分表行数据单库内优化,无分布式事务无法突破单库性能瓶颈数据量<5000万
水平分库行数据无限扩展,支撑 PB 级数据分片键设计复杂10亿+订单、日志场景

二、中间件选型:ShardingSphere vs Mycat vs ShardingCore

2.1 三大中间件核心能力对比

特性ShardingSphereMycatShardingCore
定位生态化平台(JDBC + Proxy + Sidecar)独立中间件(Proxy).NET 生态分片框架
架构模式支持 JDBC 和 Proxy 混合部署仅 Proxy 模式仅 JDBC 模式
SQL 支持完整支持(子查询、UNION、JOIN)部分支持(复杂 SQL 需优化)完整支持(LINQ 集成)
分布式事务XA、Seata 柔性事务XA 弱事务依赖外部事务方案
数据迁移提供 Scaling 迁移工具手动迁移为主支持运行时动态建表
社区活跃度Apache 顶级项目,持续更新社区维护放慢.NET 生态活跃
性能JDBC 模式性能损耗<3%网络代理损耗 10-15%与原生 EF Core 持平
云原生支持 K8s Operator支持较弱需自建

选型建议

2.2 ShardingSphere 架构详解

混合部署模式

# sharding-proxy 配置示例(透明代理)
schemaName: order_db
dataSources:
  ds_0: { url: jdbc:mysql://db0:3306/order_db0, ... }
  ds_1: { url: jdbc:mysql://db1:3306/order_db1, ... }
rules:
  - !SHARDING
    tables:
      orders:
        actualDataNodes: ds_${0..1}.orders_${0..15}
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: gene_hash
shardingAlgorithms:
  gene_hash:
    type: CLASS_BASED
    props:
      strategy: standard
      algorithmClassName: com.example.GeneShardingAlgorithm

JDBC 模式优势:应用直连数据库,无网络代理损耗,性能接近原生 SQL。

2.3 Mycat 快速接入

核心配置(schema.xml):

<schema name="order_db" checkSQLschema="true">
    <table name="orders" dataNode="dn$0-15" rule="mod-long" />
</schema>
<dataNode name="dn0" dataHost="dh0" database="order_db0" />
<dataNode name="dn1" dataHost="dh0" database="order_db1" />
<dataHost name="dh0" balance="1" writeType="0" dbType="mysql">
    <writeHost host="hostM1" url="db0:3306" user="root" password="xxx"/>
    <readHost host="hostS1" url="db1:3306" user="root" password="xxx"/>
</dataHost>

适用场景:遗留系统无法修改代码时,通过 Mycat 透明代理实现分片。

三、分片键选择:架构设计的关键战役

3.1 分片键选择三原则

原则1:离散性(避免数据热点)

-- 错误:status 只有 3 个值,导致 3 个分片成为热点
PARTITION BY HASH(status) PARTITIONS 64; -- 只有 3 个分区有数据
-- 正确:user_id 哈希,数据均匀分布
PARTITION BY HASH(user_id) PARTITIONS 64;

原则2:业务相关性(80%查询需携带)

订单系统高频查询:
1. 用户查历史订单 → 必须带 user_id ✅
2. 商家查订单 → 必须带 merchant_id ✅
3. 客服按订单号查 → 必须带 order_no ✅
分片键选择:user_id(覆盖场景最多)

原则3:稳定性(值不随业务变更)

-- 错误:手机号可能变更,导致数据迁移
-- 正确:user_id 是主键,永不改变

3.2 高级分片策略

基因分片:订单系统的终极方案

问题:订单系统有三大查询维度(user_id, merchant_id, order_no),如何保证每个维度都能快速定位分片?

解决方案:将 user_id 基因嵌入订单号中

Snowflake 改造

// 64位ID结构:符号位(1) + 时间戳(41) + 分片基因(12) + 序列号(10)
public class OrderIdGenerator {
    private static final int GENE_BITS = 12; // 12位基因支持4096个分片
    public static long generateId(long userId) {
        long timestamp = System.currentTimeMillis() - 1288834974657L;
        long gene = userId & ((1 << GENE_BITS) - 1); // 提取user_id后12位作为基因
        long sequence = getNextSequence();
        return (timestamp << 22) | (gene << 10) | sequence;
    }
    // 从订单ID反推分片位置
    public static int getShardKey(long orderId) {
        return (int) ((orderId >> 10) & 0xFFF); // 提取中间12位基因
    }
}

路由逻辑

public class OrderShardingRouter {
    private static final int DB_COUNT = 8;          // 8个库
    private static final int TABLE_COUNT = 16;      // 每库16张表
    public static String route(long orderId) {
        int gene = OrderIdGenerator.getShardKey(orderId);
        int dbIndex = gene % DB_COUNT;              // 基因决定库
        int tableIndex = gene % TABLE_COUNT;        // 基因决定表
        return String.format("order_db_%d.orders_%d", dbIndex, tableIndex);
    }
}

突破点

一致性哈希:平滑扩容方案

传统取模问题user_id % 8 扩容到 16 时,87.5% 数据需迁移。

一致性哈希:将哈希空间虚拟为 2^32 个节点,数据映射到虚拟节点,扩容时仅迁移相邻节点数据

实现框架

// 使用 Ketama 算法
public class ConsistentHashSharding {
    private final SortedMap<Long, String> circle = new TreeMap<>();
    public void addServer(String server) {
        for (int i = 0; i < 160; i++) { // 160个虚拟节点
            circle.put(hash(server + "-" + i), server);
        }
    }
    public String getServer(Long key) {
        if (circle.isEmpty()) return null;
        long hash = hash(key);
        SortedMap<Long, String> tailMap = circle.tailMap(hash);
        hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        return circle.get(hash);
    }
}

四、全局ID生成:Snowflake 基因注入方案

4.1 Snowflake 标准结构

64位ID组成

位段分布:
0-0   : 符号位(1位,始终为0)
1-41  : 时间戳(41位,支持69年,从2020起算)
42-52 : 机器ID(10位,支持1024个节点)
53-63 : 序列号(12位,每毫秒4096个ID)

问题:标准 Snowflake 无法携带分片基因,路由需查询映射表。

4.2 基因注入改造

改造后结构

0-0   : 符号位(1位)
1-41  : 时间戳(41位)- 支持到 2089年
42-53 : 分片基因(12位)- 支持4096个分片
54-63 : 序列号(10位)- 每毫秒1024个ID

Java 实现

public class GeneSnowflake {
    private final long twepoch = 1288834974657L; // 起始时间戳
    private final long geneBits = 12L;           // 基因位数
    private final long sequenceBits = 10L;       // 序列号位数
    private final long geneShift = sequenceBits; // 基因左移10位
    private final long timestampShift = geneBits + sequenceBits; // 时间戳左移22位
    public synchronized long nextId(long userId) {
        long timestamp = System.currentTimeMillis();
        long gene = userId & ((1 << geneBits) - 1); // 提取基因
        long sequence = getSequenceInSameMs(timestamp); // 毫秒内序列号
        return ((timestamp - twepoch) << timestampShift) |
               (gene << geneShift) |
               sequence;
    }
}

性能指标

4.3 ID 反解与分片定位

从 order_id 提取路由信息

// 提取基因(分片键)
public static int extractGene(long orderId) {
    // 基因位于第10-21位:orderId >> 10 & 0xFFF
    return (int) ((orderId >> 10) & 0xFFF);
}
// 提取生成时间
public static Date extractTime(long orderId) {
    long timestamp = (orderId >> 22) + twepoch;
    return new Date(timestamp);
}
// 完整路由示例
public class OrderService {
    public Order getOrderById(long orderId) {
        int gene = extractGene(orderId);
        String dbTable = OrderShardingRouter.routeByGene(gene);
        return executeQuery("SELECT * FROM " + dbTable + " WHERE order_id = ?", orderId);
    }
}

五、跨分片查询:三大解决方案

5.1 异构索引表(最常用)

方案:在 Elasticsearch 中建立二级索引,存储分片路由信息

ES 索引结构

{
  "order_index": {
    "mappings": {
      "properties": {
        "order_no": { "type": "keyword" },
        "shard_key": { "type": "integer" },  // 分片基因
        "user_id": { "type": "long" },
        "merchant_id": { "type": "long" }
      }
    }
  }
}

查询流程

// 商家查询订单(先查ES定位分片)
public List<Order> getOrdersByMerchant(Long merchantId) {
    // 1. ES 中查询 shard_key
    SearchResponse response = esClient.search(
        new SearchRequest("order_index")
            .source(new SearchSourceBuilder()
                .query(QueryBuilders.termQuery("merchant_id", merchantId))
                .fetchField("shard_key")
                .size(10000))
    );
    // 2. 按 shard_key 分组
    Map<Integer, List<Long>> shardGroups = groupByShard(response);
    // 3. 并发查询各分片
    return shardGroups.entrySet().parallelStream()
        .map(entry -> queryShard(entry.getKey(), entry.getValue()))
        .flatMap(List::stream)
        .collect(Collectors.toList());
}

5.2 全局二级索引(GSI)

ShardingSphere 实现

-- 创建全局索引(自动同步到指定存储节点)
CREATE SHARDING GLOBAL INDEX idx_merchant ON orders(merchant_id)
    BY SHARDING_ALGORITHM(merchant_hash)
    WITH STORAGE_UNIT(ds_0, ds_1);
-- 查询时自动路由
SELECT * FROM orders WHERE merchant_id = 10086;
-- ShardingSphere 自动改写为:先查 GSI 表获取 order_id,再路由到主表

适用场景:低频但强一致性的跨分片查询

5.3 CQRS 模式:读写分离

架构设计

写操作(Command):
  应用服务 → 分片路由 → 写入分片库
读操作(Query):
  应用服务 → ES/HBase → 聚合结果

优势

六、数据迁移:双写方案与灰度切换

6.1 双写架构

迁移期架构

双写伪代码

public void createOrder(Order order) {
    try {
        // 1. 写新库(主库)
        orderNewDao.insert(order);
        // 2. 写旧库(备份)
        orderOldDao.insert(order);
    } catch (Exception e) {
        // 3. 新库失败必须回滚旧库
        if (isNewSuccess()) {
            orderNewDao.delete(order.getId());
        }
        throw e;
    }
}

关键原则

6.2 灰度切换四阶段

阶段操作流量比例回滚策略
1. 双写阶段新旧库同时写入0%可随时切回旧库
2. 全量迁移历史数据分批导入0%校验收据
3. 增量验证实时比对数据一致性0%自动修复不一致
4. 灰度引流按用户ID百分比切换1% → 50% → 100%发现问题立即回滚

切换命令

// 动态分片路由(按用户ID灰度)
public String routeByUserId(Long userId) {
    if (userId <= 10000) { // 1%用户切新库
        return "order_new";
    } else {
        return "order_old";
    }
}

七、避坑指南与性能陷阱

7.1 热点数据分片倾斜

现象:某网红店铺订单全部分到同一分片,导致该分片成为热点

根因merchant_id 哈希不均,大商家数据量占 30%

解决方案

-- 复合分片键:(merchant_id + user_id) % 1024
-- 路由逻辑:将大商家数据按 user_id 二次打散
public int getShardKey(long merchantId, long userId) {
    if (isBigMerchant(merchantId)) {
        return (int) ((merchantId * 31 + userId) % 1024);
    } else {
        return (int) (merchantId % 1024);
    }
}

7.2 分布式事务:最终一致性方案

问题:跨库事务无法使用本地 ACID

RocketMQ 最终一致性

@Transactional
public void createOrder(Order order) {
    // 1. 本地事务:写订单主库
    orderDao.insert(order);
    // 2. 发送事务消息(半消息)
    TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
        "order_create_event",
        MessageBuilder.withPayload(order.toJson()).build(),
        null
    );
    // 3. 消息确认后,下游消费加积分、扣库存
}
// 消费者异步处理
@RocketMQMessageListener(topic = "order_create_event")
public void handleEvent(OrderEvent event) {
    bonusService.addPoints(event.getUserId());      // 异步加积分
    inventoryService.deduct(event.getSkuId());      // 异步扣库存
}

优势:避免分布式锁,吞吐量提升 10 倍

7.3 跨分片分页陷阱

现象LIMIT 100, 10 跨分片查询需扫描所有分片,内存聚合后排序,性能极差

解决方案

-- 方案1:业务折衷(禁用深分页,仅支持前100页)
-- 方案2:ES 聚合查询(推荐)
GET /order_index/_search
{
  "from": 100,
  "size": 10,
  "sort": [{"create_time": "desc"}]
}
-- 方案3:游标分页(记录上次查询的 order_id)
SELECT * FROM orders 
WHERE create_time < '2024-01-01' AND order_id < #{lastOrderId}
ORDER BY create_time DESC LIMIT 10;

八、性能指标与架构演进

8.1 拆分前后性能对比

场景拆分前拆分后提升倍数
用户订单查询3200ms68ms47倍
商家订单导出超时失败8秒可用
全表统计不可用1.2秒(近似)可用
写入并发2000 QPS8000 QPS4倍

8.2 分库分表架构最佳实践

1. 分片键选择大于努力

// 基因分片是订单系统的最佳拍档
// 12位基因支持 4096 个分片 = 8库 × 16表 × 32冗余

2. 预留扩容空间

-- 初始设计:8库 × 16表 = 128分片
-- 支持单分片 500万行 → 总容量 6.4亿行
-- 预留 2 年数据增长

3. 避免过度设计

// 小表(<1000万行)无需分片
// 大表关联查询:优先冗余字段,避免跨分片 JOIN

4. 监控驱动优化

-- 监控分片倾斜率
SELECT db, table_name, COUNT(*) AS rows 
FROM information_schema.tables 
WHERE table_schema LIKE 'order_db_%' 
GROUP BY db, table_name 
HAVING rows > AVG(rows) * 1.5; -- 找出超平均分片50%的热点

8.3 终极架构方案

核心分层

总结

决策点推荐方案避免方案
分片键user_id + 基因注入手机号、状态码
中间价ShardingSphere (Java)自研(成本高)
全局IDSnowflake 基因改造UUID(无序)
跨分片查询ES 异构索引跨库 JOIN
数据迁移双写 + 灰度停机迁移
分布式事务最终一致性(MQ)XA(性能差)

黄金法则:分库分表不是架构的终点,而是数据治理的起点。真正的架构艺术,是在分与合之间找到平衡点,通过基因分片实现数据自治,通过 ES 索引实现查询自由,通过 MQ 实现事务自由,最终支撑从 10亿 到 100亿 的平滑演进。

到此这篇关于MySql分库分表深度指南之从策略到落地的文章就介绍到这了,更多相关mysql分库分表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文