MySQL分库分表方案与实践

75 阅读14分钟

概述

随着业务快速增长,单表数据量突破千万级甚至亿级,MySQL性能急剧下降,成为系统瓶颈。分库分表是解决海量数据存储与高并发访问的核心方案,通过水平拆分将数据分散到多个库表,实现存储容量和并发能力的线性扩展。本文深入剖析垂直拆分与水平拆分策略,详解ShardingSphere-JDBC集成方案,涵盖Snowflake分布式ID生成、跨分片查询优化、深分页问题解决、数据迁移完整流程,结合电商订单系统实战案例,提供从理论到落地的完整技术路径,助力构建支撑亿级数据的高性能数据库架构。


一、理论知识与核心概念

1.1 为什么需要分库分表?

单表数据量瓶颈

MySQL InnoDB存储引擎使用B+树索引,当单表数据量超过500万时,B+树高度增加,查询性能显著下降:

  • 500万数据: B+树高度约4层,查询需要4次磁盘IO
  • 1000万数据: B+树高度5层,性能下降30%
  • 2000万数据: B+树高度6层,性能下降50%以上
  • 5000万数据: 慢查询频发,系统不可用

并发压力瓶颈

单库单表承载所有读写请求,CPU、内存、磁盘IO成为瓶颈:

  • 单机QPS上限: 通常5000-10000 QPS
  • 写入瓶颈: InnoDB行锁冲突,TPS难以突破1000
  • 备份困难: 单表100GB,mysqldump备份耗时数小时

业务增长预测

电商订单系统日订单量100万,单表预估:

  • 1年数据: 3.65亿条记录
  • 3年数据: 10.95亿条记录
  • 单表极限: 建议不超过2000万,必须分库分表

1.2 分库分表核心概念

vertical-horizontal-sharding-comparison.svg

垂直拆分 (Vertical Sharding)

按业务模块或字段维度拆分:

  • 垂直分库: 用户库、订单库、商品库分别部署,业务解耦
  • 垂直分表: 商品基础表(id, name, price)与详情表(description, images)分离

优点: 业务隔离,微服务架构天然支持

缺点: 不解决单表数据量问题,JOIN查询复杂

水平拆分 (Horizontal Sharding)

按数据行拆分到多个库表,同构部署:

  • 分库: order_db_0, order_db_1, order_db_2, order_db_3 (4个库)
  • 分表: t_order_0 ~ t_order_255 (每库256张表)

优点: 单表数据量减少,性能线性提升,支持无限扩展

缺点: 跨分片查询、分布式事务、数据迁移复杂

混合拆分 (推荐生产方案)

先垂直拆分业务,再对大表水平拆分:

用户服务: user_db (垂直拆分,无需分表)
订单服务: order_db_0 ~ order_db_3 (垂直拆分 + 水平分表)
商品服务: product_db_0 ~ product_db_1 (垂直拆分 + 水平分表)

二、分库分表策略详解

2.1 分片键选择

分片键决定数据路由规则,选择不当导致数据倾斜或跨片查询:

常见分片键

分片键适用场景优点缺点
user_id用户维度查询单用户数据集中,查询快大V用户数据倾斜
order_id订单查询数据分布均匀按用户查询需跨片
seller_id卖家维度查询单卖家数据集中大卖家数据倾斜
created_time按时间归档历史数据归档方便当前数据热点集中

电商订单表分片键选择

-- 方案1: 按user_id分片 (推荐)
优点: 查询"我的订单"无需跨片,单用户查询性能极佳
缺点: 卖家查询"我的销售订单"需要跨片聚合

-- 方案2: 按order_id分片
优点: 数据分布均匀,单订单查询快
缺点: 用户订单列表查询需要跨片

-- 生产建议: 按user_id分片,卖家维度查询走ES

2.2 分片算法对比

sharding-strategy-routing-flow.svg

1. 取模算法 (Mod) - 最常用

// 分库分表规则
int dbIndex = user_id % 4;           // 4个库
int tableIndex = user_id % 256;      // 每库256张表

// 示例: user_id = 1001
dbIndex = 1001 % 4 = 1order_db_1
tableIndex = 1001 % 256 = 233       → t_order_233
最终路由: order_db_1.t_order_233

优点: 数据分布均匀,计算简单,性能高

缺点: 扩容需要数据迁移 (4库扩展到8库需rehash)

2. 范围算法 (Range)

// ID范围分片
if (order_id < 10000000) {
    dbIndex = 0;  // order_db_0
} else if (order_id < 20000000) {
    dbIndex = 1;  // order_db_1
} else if (order_id < 30000000) {
    dbIndex = 2;  // order_db_2
}

优点: 范围查询性能好,扩容无需迁移历史数据

缺点: 数据分布可能不均匀,新数据热点集中

3. 一致性哈希 (Consistent Hash)

// 哈希环分片
long hashValue = hash(user_id) % (2^32);
int dbIndex = findNearestNode(hashValue);  // 顺时针查找最近节点

优点: 扩容时仅迁移1/N数据,节点变化影响最小

缺点: 实现复杂,需要虚拟节点避免数据倾斜

4. 时间范围算法 (Time-based)

// 按月分表
String tableName = "t_order_" + yyyyMM;
// t_order_202401, t_order_202402, t_order_202403...

优点: 历史数据归档方便,时间范围查询性能极佳

缺点: 当月数据热点集中,跨月查询需union多表

选择建议

  • 用户/订单维度: 取模算法 (数据均匀,查询性能稳定)
  • 时间维度归档: 时间范围算法 (日志表、流水表)
  • 动态扩容场景: 一致性哈希 (频繁扩容的系统)

2.3 分库分表数量规划

单表数据量建议

  • 500万: 开始考虑分表
  • 1000万: 强烈建议分表
  • 2000万: 必须分表,性能严重下降

分片数量计算

总数据量: 预估3年10亿订单数据
单表容量: 建议200万条 (保证性能)
分表数量: 10亿 / 200万 = 500张表

推荐方案: 4个库 × 256张表 = 1024张物理表
单表数据: 10亿 / 1024 ≈ 97.6万条 ✓

扩展空间: 支持扩展到8库、16库,无需修改表数量

库表数量最佳实践

分库数量: 2^n (2, 4, 8, 16, 32),便于取模和扩容
分表数量: 2^n (16, 32, 64, 128, 256),每库表数量适中

推荐配置:
- 小型系统: 2库 × 16表 = 32张表
- 中型系统: 4库 × 64表 = 256张表
- 大型系统: 8库 × 256表 = 2048张表
- 超大型系统: 16库 × 512表 = 8192张表

三、ShardingSphere分库分表实战

3.1 ShardingSphere-JDBC简介

ShardingSphere-JDBC是Apache顶级项目,轻量级Java框架,以jar包形式提供服务:

核心特性

  • 完全兼容JDBC: 无需修改业务代码
  • 支持任意ORM框架: MyBatis、MyBatis-Plus、JPA、Hibernate
  • 高性能: 本地计算,无额外网络开销
  • 分片策略丰富: 支持取模、范围、自定义算法
  • 读写分离: 集成主从复制,自动路由
  • 分布式事务: 集成Seata,支持XA、AT模式

3.2 ShardingSphere-JDBC配置

引入依赖

<!-- pom.xml -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.0.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.6</version>
</dependency>

完整配置 application.yml

spring:
  shardingsphere:
    # 数据源配置
    datasource:
      names: order-db-0,order-db-1,order-db-2,order-db-3

      # 订单库0
      order-db-0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.100:3306/order_db_0?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
        username: root
        password: password
        hikari:
          maximum-pool-size: 50
          minimum-idle: 10
          connection-timeout: 30000

      # 订单库1
      order-db-1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.101:3306/order_db_1?useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: password
        hikari:
          maximum-pool-size: 50
          minimum-idle: 10

      # 订单库2
      order-db-2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.102:3306/order_db_2?useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: password
        hikari:
          maximum-pool-size: 50
          minimum-idle: 10

      # 订单库3
      order-db-3:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.103:3306/order_db_3?useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: password
        hikari:
          maximum-pool-size: 50
          minimum-idle: 10

    # 分片规则配置
    rules:
      sharding:
        tables:
          # 订单表分片规则
          t_order:
            # 真实节点: 4个库 × 256张表 = 1024张物理表
            actual-data-nodes: order-db-$->{0..3}.t_order_$->{0..255}

            # 分库策略: 按user_id取模,路由到4个库
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: order-database-mod

            # 分表策略: 按user_id取模,路由到256张表
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: order-table-mod

            # 主键生成策略: 雪花算法
            key-generate-strategy:
              column: id
              key-generator-name: snowflake

          # 订单明细表分片规则 (与订单表相同分片键)
          t_order_item:
            actual-data-nodes: order-db-$->{0..3}.t_order_item_$->{0..255}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: order-database-mod
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: order-table-mod
            key-generate-strategy:
              column: id
              key-generator-name: snowflake

        # 分片算法定义
        sharding-algorithms:
          # 分库算法: user_id % 4
          order-database-mod:
            type: MOD
            props:
              sharding-count: 4

          # 分表算法: user_id % 256
          order-table-mod:
            type: MOD
            props:
              sharding-count: 256

        # 主键生成器定义
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1           # 机器ID (0-31)
              datacenter-id: 1       # 数据中心ID (0-31)

    # 属性配置
    props:
      sql-show: true                 # 打印SQL日志
      sql-simple: false              # 打印完整SQL (包含参数)

3.3 订单表Schema设计

订单主表

-- order_db_0 ~ order_db_3,每个库都需要创建256张表
-- t_order_0 ~ t_order_255

CREATE TABLE `t_order_0` (
  `id` BIGINT UNSIGNED NOT NULL COMMENT '订单ID (雪花算法生成)',
  `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
  `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID (分片键)',
  `seller_id` BIGINT UNSIGNED NOT NULL COMMENT '卖家ID',
  `total_amount` DECIMAL(19, 2) NOT NULL COMMENT '订单总金额',
  `pay_amount` DECIMAL(19, 2) NOT NULL DEFAULT 0.00 COMMENT '实付金额',
  `status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订单状态: 0-待支付,1-已支付,2-已发货,3-已完成,4-已取消',
  `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
  `delivery_time` DATETIME DEFAULT NULL COMMENT '发货时间',
  `finish_time` DATETIME DEFAULT NULL COMMENT '完成时间',
  `is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除,1-已删除',
  `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人ID',
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`, `created_at`),
  KEY `idx_seller_id` (`seller_id`, `created_at`),
  KEY `idx_status` (`status`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';

-- 批量创建256张表的脚本
DELIMITER $$
CREATE PROCEDURE create_order_tables()
BEGIN
  DECLARE i INT DEFAULT 0;
  WHILE i < 256 DO
    SET @sql = CONCAT('CREATE TABLE t_order_', i, ' LIKE t_order_0');
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
    SET i = i + 1;
  END WHILE;
END$$
DELIMITER ;

-- 执行创建
CALL create_order_tables();

订单明细表

CREATE TABLE `t_order_item_0` (
  `id` BIGINT UNSIGNED NOT NULL COMMENT '明细ID',
  `order_id` BIGINT UNSIGNED NOT NULL COMMENT '订单ID',
  `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID (分片键,与订单表保持一致)',
  `product_id` BIGINT UNSIGNED NOT NULL COMMENT '商品ID',
  `product_name` VARCHAR(128) NOT NULL COMMENT '商品名称 (冗余)',
  `product_image` VARCHAR(256) DEFAULT NULL COMMENT '商品图片',
  `sku_id` BIGINT UNSIGNED NOT NULL COMMENT 'SKU ID',
  `price` DECIMAL(19, 2) NOT NULL COMMENT '商品单价',
  `quantity` INT UNSIGNED NOT NULL COMMENT '购买数量',
  `total_amount` DECIMAL(19, 2) NOT NULL COMMENT '小计金额',
  `is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT 0,
  `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单明细表';

3.4 Java代码实现

Entity层

@Data
@TableName("t_order")
public class Order {
    @TableId(type = IdType.ASSIGN_ID)  // 使用ShardingSphere雪花算法
    private Long id;

    private String orderNo;
    private Long userId;       // 分片键
    private Long sellerId;
    private BigDecimal totalAmount;
    private BigDecimal payAmount;
    private Integer status;
    private LocalDateTime payTime;
    private LocalDateTime deliveryTime;
    private LocalDateTime finishTime;

    private Integer isDeleted;
    private Long createdBy;
    private LocalDateTime createdAt;
    private Long updatedBy;
    private LocalDateTime updatedAt;
}

@Data
@TableName("t_order_item")
public class OrderItem {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private Long orderId;
    private Long userId;       // 分片键,与订单表一致
    private Long productId;
    private String productName;
    private String productImage;
    private Long skuId;
    private BigDecimal price;
    private Integer quantity;
    private BigDecimal totalAmount;

    private Integer isDeleted;
    private Long createdBy;
    private LocalDateTime createdAt;
    private Long updatedBy;
    private LocalDateTime updatedAt;
}

Mapper层

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    // MyBatis-Plus自动生成CRUD方法
    // ShardingSphere自动路由到正确的分片
}

@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}

Repository层

public interface OrderRepository {
    void saveCustom(Order order);
    Order findById(Long orderId);
    Order findByOrderNo(String orderNo);
    List<Order> findByUserId(Long userId);
    PageResult<Order> pageByUserId(Long userId, Integer pageNum, Integer pageSize);
    void updateStatus(Long orderId, Integer status);
}

@Repository
public class OrderRepositoryImpl extends ServiceImpl<OrderMapper, Order>
    implements OrderRepository {

    @Override
    public void saveCustom(Order order) {
        super.save(order);
    }

    @Override
    public Order findById(Long orderId) {
        return super.lambdaQuery()
            .eq(Order::getId, orderId)
            .eq(Order::getIsDeleted, 0)
            .one();
    }

    @Override
    public Order findByOrderNo(String orderNo) {
        return super.lambdaQuery()
            .eq(Order::getOrderNo, orderNo)
            .eq(Order::getIsDeleted, 0)
            .one();
    }

    @Override
    public List<Order> findByUserId(Long userId) {
        // ShardingSphere根据user_id路由到单个分片
        return super.lambdaQuery()
            .eq(Order::getUserId, userId)
            .eq(Order::getIsDeleted, 0)
            .orderByDesc(Order::getCreatedAt)
            .list();
    }

    @Override
    public PageResult<Order> pageByUserId(Long userId, Integer pageNum, Integer pageSize) {
        Page<Order> page = new Page<>(pageNum, pageSize);
        Page<Order> result = super.lambdaQuery()
            .eq(Order::getUserId, userId)
            .eq(Order::getIsDeleted, 0)
            .orderByDesc(Order::getCreatedAt)
            .page(page);

        return new PageResult<>(result.getRecords(), result.getTotal());
    }

    @Override
    public void updateStatus(Long orderId, Integer status) {
        super.lambdaUpdate()
            .eq(Order::getId, orderId)
            .eq(Order::getIsDeleted, 0)
            .set(Order::getStatus, status)
            .set(Order::getUpdatedAt, LocalDateTime.now())
            .update();
    }
}

Service层

@Service
public class OrderServiceImpl implements OrderService {

    private static final TraceLogger log = TraceLogger.getLogger(OrderServiceImpl.class);

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private OrderItemRepository orderItemRepository;

    @Autowired
    private SnowflakeIdWorker idWorker;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public OrderDTO createOrder(OrderCreateRequest request) {
        log.info("Create order, userId={}, totalAmount={}", request.getUserId(), request.getTotalAmount());

        // 1. 生成订单号
        String orderNo = generateOrderNo();

        // 2. 创建订单 (ShardingSphere自动路由到user_id对应的分片)
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(request.getUserId());
        order.setSellerId(request.getSellerId());
        order.setTotalAmount(request.getTotalAmount());
        order.setPayAmount(request.getTotalAmount());
        order.setStatus(0);  // 待支付
        order.setCreatedBy(request.getUserId());
        orderRepository.saveCustom(order);

        log.info("Order created, orderId={}, orderNo={}", order.getId(), orderNo);

        // 3. 创建订单明细 (user_id与订单表相同,路由到同一分片)
        List<OrderItem> items = request.getItems().stream().map(item -> {
            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(order.getId());
            orderItem.setUserId(request.getUserId());  // 分片键
            orderItem.setProductId(item.getProductId());
            orderItem.setProductName(item.getProductName());
            orderItem.setSkuId(item.getSkuId());
            orderItem.setPrice(item.getPrice());
            orderItem.setQuantity(item.getQuantity());
            orderItem.setTotalAmount(item.getPrice().multiply(new BigDecimal(item.getQuantity())));
            orderItem.setCreatedBy(request.getUserId());
            return orderItem;
        }).collect(Collectors.toList());

        orderItemRepository.saveBatch(items);

        log.info("Order items created, orderId={}, itemCount={}", order.getId(), items.size());

        return convertToDTO(order);
    }

    @Override
    public OrderDTO getOrderById(Long orderId) {
        log.info("Get order by id, orderId={}", orderId);

        Order order = orderRepository.findById(orderId);
        if (order == null) {
            throw new BusinessException("订单不存在");
        }

        return convertToDTO(order);
    }

    @Override
    public PageResult<OrderDTO> pageOrdersByUserId(Long userId, Integer pageNum, Integer pageSize) {
        log.info("Page orders by userId, userId={}, pageNum={}, pageSize={}", userId, pageNum, pageSize);

        // ShardingSphere根据user_id路由到单个分片,无跨片查询
        PageResult<Order> pageResult = orderRepository.pageByUserId(userId, pageNum, pageSize);

        List<OrderDTO> orderDTOs = pageResult.getRecords().stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList());

        return new PageResult<>(orderDTOs, pageResult.getTotal());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void payOrder(Long orderId) {
        log.info("Pay order, orderId={}", orderId);

        // 更新订单状态
        orderRepository.updateStatus(orderId, 1);  // 已支付

        log.info("Order paid successfully, orderId={}", orderId);
    }

    private String generateOrderNo() {
        return "ORD" + System.currentTimeMillis() + RandomStringUtils.randomNumeric(6);
    }

    private OrderDTO convertToDTO(Order order) {
        OrderDTO dto = new OrderDTO();
        dto.setId(order.getId());
        dto.setOrderNo(order.getOrderNo());
        dto.setUserId(order.getUserId());
        dto.setTotalAmount(order.getTotalAmount());
        dto.setStatus(order.getStatus());
        dto.setCreatedAt(order.getCreatedAt());
        return dto;
    }
}

Controller层

@RestController
@RequestMapping("/api/order/v1/orders")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/create")
    public Response<OrderDTO> createOrder(@Valid @RequestBody OrderCreateRequest request) {
        OrderDTO order = orderService.createOrder(request);
        return Response.success(order);
    }

    @GetMapping("/{orderId}")
    public Response<OrderDTO> getOrderById(@PathVariable Long orderId) {
        OrderDTO order = orderService.getOrderById(orderId);
        return Response.success(order);
    }

    @GetMapping("/user/{userId}")
    public Response<PageResult<OrderDTO>> pageOrdersByUserId(
        @PathVariable Long userId,
        @RequestParam(defaultValue = "1") Integer pageNum,
        @RequestParam(defaultValue = "10") Integer pageSize
    ) {
        PageResult<OrderDTO> result = orderService.pageOrdersByUserId(userId, pageNum, pageSize);
        return Response.success(result);
    }

    @PutMapping("/{orderId}/pay")
    public Response<Void> payOrder(@PathVariable Long orderId) {
        orderService.payOrder(orderId);
        return Response.success();
    }
}

SQL日志 (ShardingSphere自动改写)

Logic SQL: SELECT * FROM t_order WHERE user_id = 1001 AND is_deleted = 0 ORDER BY created_at DESC

Actual SQL: order-db-1 ::: SELECT * FROM t_order_233 WHERE user_id = 1001 AND is_deleted = 0 ORDER BY created_at DESC

路由分析:
user_id = 1001
dbIndex = 1001 % 4 = 1order-db-1
tableIndex = 1001 % 256 = 233 → t_order_233

四、分布式主键生成方案

4.1 为什么需要分布式ID?

分库分表后,MySQL自增主键失效:

问题

  • 多库自增冲突: order_db_0生成id=1, order_db_1也生成id=1,冲突
  • 无法保证全局唯一: 订单号、用户ID需要全局唯一
  • 无法保证趋势递增: InnoDB B+树索引要求主键递增,否则频繁页分裂

4.2 分布式ID生成方案对比

方案1: UUID

String id = UUID.randomUUID().toString().replace("-", "");
// 示例: 3e11fa4771ca11e19e33c80aa9429562

优点: 本地生成,性能极高,全局唯一

缺点:

  • ❌ 字符串类型,占用空间大 (36字节)
  • ❌ 无序,InnoDB插入性能差
  • ❌ 不可读,无业务含义

方案2: 数据库自增ID+号段模式

-- 创建ID生成表
CREATE TABLE `id_generator` (
  `biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型',
  `max_id` BIGINT UNSIGNED NOT NULL COMMENT '当前最大ID',
  `step` INT UNSIGNED NOT NULL DEFAULT 1000 COMMENT '步长',
  `updated_at` DATETIME NOT NULL,
  PRIMARY KEY (`biz_type`)
) ENGINE=InnoDB;

-- 获取号段
UPDATE id_generator SET max_id = max_id + step WHERE biz_type = 'order';
SELECT max_id FROM id_generator WHERE biz_type = 'order';

优点: 趋势递增,简单易实现

缺点:

  • ❌ 依赖数据库,单点故障
  • ❌ 号段用完需要请求数据库,有延迟

方案3: Redis INCR

Long id = redisTemplate.opsForValue().increment("order_id_seq");

优点: 性能高 (10万QPS), 趋势递增

缺点:

  • ❌ 依赖Redis,需要持久化
  • ❌ Redis宕机会导致ID重复

方案4: Snowflake算法 (推荐) ⭐⭐⭐⭐⭐

snowflake-algorithm-structure.svg

Java实现

@Component
public class SnowflakeIdWorker {

    private final long epoch = 1577808000000L;  // 起始时间: 2020-01-01 00:00:00
    private final long datacenterIdBits = 5L;
    private final long workerIdBits = 5L;
    private final long sequenceBits = 12L;

    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);  // 31
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);          // 31
    private final long maxSequence = -1L ^ (-1L << sequenceBits);          // 4095

    private final long workerIdShift = sequenceBits;                        // 12
    private final long datacenterIdShift = sequenceBits + workerIdBits;   // 17
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 22

    private long datacenterId;
    private long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdWorker(@Value("${snowflake.datacenter-id:1}") long datacenterId,
                             @Value("${snowflake.worker-id:1}") long workerId) {
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("datacenter Id can't be greater than " + maxDatacenterId + " or less than 0");
        }
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("worker Id can't be greater than " + maxWorkerId + " or less than 0");
        }
        this.datacenterId = datacenterId;
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();

        // 时钟回拨检测
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate id");
        }

        // 同一毫秒内,序列号自增
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & maxSequence;
            // 序列号溢出,等待下一毫秒
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒,序列号重置
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        // 组装64位ID
        return ((timestamp - epoch) << timestampLeftShift)
            | (datacenterId << datacenterIdShift)
            | (workerId << workerIdShift)
            | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

配置

snowflake:
  datacenter-id: 1  # 数据中心ID (0-31)
  worker-id: 1      # 机器ID (0-31)

优点:

  • ✅ 本地生成,性能极高 (400万/秒)
  • ✅ 全局唯一
  • ✅ 趋势递增,InnoDB友好
  • ✅ 无依赖,高可用

缺点:

  • ❌ 依赖系统时钟,时钟回拨会导致ID重复
  • ❌ 最多支持1024台机器

生产环境推荐: Snowflake算法,性能和可靠性兼顾


五、跨分片查询与分页问题

5.1 跨分片查询类型

单分片查询 (推荐)

-- 带分片键user_id,路由到单个分片
SELECT * FROM t_order WHERE user_id = 1001;

执行计划: 只查询 order_db_1.t_order_233 (单分片)

广播查询 (所有分片)

-- 不带分片键,需要查询所有分片
SELECT * FROM t_order WHERE order_no = 'ORD20240115123456';

执行计划: 查询所有1024张表,性能差 ❌

优化方案: 为order_no建立全局路由表

CREATE TABLE `order_routing` (
  `order_no` VARCHAR(32) NOT NULL,
  `user_id` BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (`order_no`)
) ENGINE=InnoDB;

-- 查询流程
1. SELECT user_id FROM order_routing WHERE order_no = 'ORD20240115123456';
2. SELECT * FROM t_order WHERE user_id = {user_id} AND order_no = 'ORD20240115123456';

5.2 深分页问题与优化

deep-pagination-problem.svg

问题SQL

-- 查询第100万页,每页10条
SELECT * FROM t_order WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 1000000, 10;

性能问题:
1. MySQL需要扫描1000010条记录
2. 丢弃前10000003. 返回最后104. 耗时: 5+

ShardingSphere分页问题

-- 原始SQL
SELECT * FROM t_order WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 100, 10;

-- ShardingSphere改写后 (假设4个分片)
-- 每个分片执行:
SELECT * FROM t_order_xxx WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 0, 110;

-- 应用层归并
1. 4个分片各返回110条 (共440条)
2. 归并排序
3. 跳过前100条,返回10条

问题: offset越大,网络传输数据越多
LIMIT 1000000, 10 → 每个分片返回1000010条 → 4个分片共400万条数据传输!

优化方案1: ID范围查询 (推荐)

// 第1页
SELECT * FROM t_order WHERE user_id = 1001
ORDER BY id DESC LIMIT 10;
// 返回10条,记录最后一条 last_id = 9999990

// 第2页
SELECT * FROM t_order WHERE user_id = 1001 AND id < 9999990
ORDER BY id DESC LIMIT 10;

// 第N页
SELECT * FROM t_order WHERE user_id = 1001 AND id < {last_id}
ORDER BY id DESC LIMIT 10;

优点: 只扫描10条,性能稳定,耗时10ms
缺点: 不支持跳页,只能顺序翻页

优化方案2: 业务限制 (最简单)

int maxPage = 100;  // 最多查询100页
if (pageNum > maxPage) {
    throw new BusinessException("最多查询" + maxPage + "页,请使用搜索功能");
}

理由: 99%用户不会翻到第100页,Google搜索也只展示10

六、数据迁移方案

data-migration-flow.svg

6.1 停机迁移方案

适用场景: 非核心业务,可接受短时间停机

迁移步骤

# 步骤1: 停止应用服务 (凌晨2:00)
systemctl stop order-service

# 步骤2: 全量数据导出
mysqldump -h192.168.1.100 -uroot -p --single-transaction \
  --skip-lock-tables order_db t_order > /tmp/order_db.sql

# 步骤3: 按分片规则导入新库
python3 split_and_import.py --source=/tmp/order_db.sql \
  --target-dbs=order-db-0,order-db-1,order-db-2,order-db-3 \
  --sharding-key=user_id \
  --sharding-count=1024

# 步骤4: 数据校验
SELECT COUNT(*) FROM order_db.t_order;  -- 100000000
SELECT SUM(cnt) FROM (
  SELECT COUNT(*) AS cnt FROM order_db_0.t_order_0 UNION ALL
  SELECT COUNT(*) AS cnt FROM order_db_0.t_order_1 UNION ALL
  ...
  SELECT COUNT(*) AS cnt FROM order_db_3.t_order_255
) AS tmp;  -- 100000000 ✓

# 步骤5: 切换应用配置,启动服务 (凌晨6:00)
systemctl start order-service

优点: 实现简单,数据一致性有保障

缺点: 业务中断4小时,用户体验差

6.2 不停机迁移方案 (推荐)

阶段1: 准备阶段

# 1. 创建新分库分表结构
for db in order_db_0 order_db_1 order_db_2 order_db_3; do
  mysql -h192.168.1.10x -uroot -p -e "CREATE DATABASE ${db}"
  for ((i=0;i<256;i++)); do
    mysql -h192.168.1.10x -uroot -p ${db} < t_order_${i}.sql
  done
done

# 2. 全量数据迁移 (历史数据,不影响线上)
mysqldump -h192.168.1.100 -uroot -p --where="created_at < '2024-01-15 02:00:00'" \
  order_db t_order | python3 split_and_import.py

记录迁移截止时间: cutoff_time = '2024-01-15 02:00:00'

阶段2: 双写阶段

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    @Qualifier("oldDataSource")
    private DataSource oldDataSource;

    @Autowired
    @Qualifier("newDataSource")
    private DataSource newDataSource;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        try {
            // 1. 写旧库
            oldOrderRepository.save(order);

            // 2. 写新库 (ShardingSphere自动分片)
            newOrderRepository.save(order);
        } catch (Exception e) {
            // 新库异常时降级为单写
            if (isNewDbError(e)) {
                log.warn("New DB error, degrade to write old DB only");
                oldOrderRepository.save(order);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Order getOrderById(Long orderId) {
        // 读操作仍然走旧库
        return oldOrderRepository.findById(orderId);
    }
}

阶段3: 增量数据追平

-- 补充增量数据 (cutoff_time到双写开启期间)
SELECT * FROM order_db.t_order
WHERE created_at > '2024-01-15 02:00:00'
  AND created_at < '2024-01-15 10:00:00'  -- 双写开启时间
INTO OUTFILE '/tmp/incremental.sql';

-- 按分片规则导入新库
python3 split_and_import.py --source=/tmp/incremental.sql

阶段4: 灰度切换读操作

@Configuration
public class DataSourceConfig {

    @Value("${read.new-db.ratio:0}")
    private int newDbRatio;  // 新库读流量比例 (0-100)

    public Order getOrderById(Long orderId) {
        // 灰度切换: 5% → 10% → 50% → 100%
        if (ThreadLocalRandom.current().nextInt(100) < newDbRatio) {
            return newOrderRepository.findById(orderId);  // 读新库
        } else {
            return oldOrderRepository.findById(orderId);  // 读旧库
        }
    }
}

# 配置文件
read:
  new-db:
    ratio: 5  # 第1天: 5%流量读新库

阶段5: 停止双写,完成迁移

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 停止写旧库
        // oldOrderRepository.save(order);

        // 仅写新库
        newOrderRepository.save(order);
    }
}

// 保留旧库1-2周作为数据备份,确认无问题后下线

总耗时: 约3周,零业务中断 ✅


七、实战场景应用

7.1 电商订单系统分片方案

业务需求

  • 日订单量: 100万
  • 3年数据: 10.95亿条
  • 读写比例: 9:1 (90%读,10%写)
  • 高峰期QPS: 5000

分片方案

分片键: user_id (用户维度查询占95%)
分片算法: 取模算法 (数据分布均匀)
分片数量: 4库 × 256表 = 1024张物理表
单表数据: 10.95亿 / 1024 ≈ 107万条 ✓

分库:
- order_db_0: 192.168.1.100:3306
- order_db_1: 192.168.1.101:3306
- order_db_2: 192.168.1.102:3306
- order_db_3: 192.168.1.103:3306

分表: 每库256张表
- t_order_0 ~ t_order_255
- t_order_item_0 ~ t_order_item_255

性能数据

分片前 (单表1亿数据):
- 查询响应时间: 500ms
- 写入TPS: 1000
- 峰值QPS: 1500
- 系统状态: CPU 80%, 慢查询频发

分片后 (1024张表,单表100万):
- 查询响应时间: 50ms ✅
- 写入TPS: 5000 ✅
- 峰值QPS: 10000 ✅
- 系统状态: CPU 40%, 无慢查询

性能提升: 10倍 ✅

八、生产案例与故障排查

8.1 案例1: 分片键选择错误导致数据倾斜

故障现象

某电商平台按seller_id分片,发现order_db_0占用90%存储,其他库几乎为空

原因分析

-- 查询各分片数据量
SELECT 'order_db_0' AS db_name, COUNT(*) FROM order_db_0.t_order_0 UNION ALL
SELECT 'order_db_1', COUNT(*) FROM order_db_1.t_order_0;

结果:
order_db_0: 9000万条
order_db_1: 500万条
order_db_2: 300万条
order_db_3: 200万条

原因: Top卖家seller_id=1的订单占总量90%,全部路由到order_db_0
seller_id % 4 = 1 → order_db_0

解决方案

1. 改用user_id作为分片键 (用户分布均匀)
2. 对Top卖家特殊处理:
   - seller_id=1的订单按user_id+order_id联合分片
   - 或单独为Top卖家创建独立分片

8.2 案例2: 跨分片JOIN导致性能崩溃

故障现象

-- 查询用户订单及商品信息
SELECT o.*, p.product_name, p.price
FROM t_order o
JOIN t_product p ON o.product_id = p.id
WHERE o.user_id = 1001;

执行计划:
1. 查询t_order → order_db_1.t_order_233 (单分片)
2. 查询t_product → 需要查询product_db所有分片 (广播查询)
3. 应用层JOIN

问题: 商品表1024个分片全部查询,耗时10+

解决方案

// 方案1: 数据冗余,避免JOIN
@Data
@TableName("t_order")
public class Order {
    private Long productId;
    private String productName;  // 冗余商品名称
    private BigDecimal productPrice;  // 冗余商品价格
}

// 方案2: 应用层分步查询
List<Order> orders = orderRepository.findByUserId(userId);
List<Long> productIds = orders.stream().map(Order::getProductId).collect(Collectors.toList());
List<Product> products = productRepository.findByIds(productIds);  // 批量查询
// 应用层组装数据

九、常见问题与避坑指南

9.1 分片键可以修改吗?

不能修改

分片键一旦确定,数据已经按分片键路由到不同分片,修改分片键需要全量数据迁移

示例

-- 原分片键: user_id
user_id = 1001 → order_db_1.t_order_233

-- 修改分片键为order_id
order_id = 888 → order_db_0.t_order_120

问题: 数据已经在order_db_1.t_order_233,修改分片键无法自动迁移

解决方案: 分片键选择需要提前规划,无法修改则需要停机全量迁移

9.2 如何扩容分片?

扩容方案

原配置: 4库 × 256表 = 1024张表
扩容: 8库 × 256表 = 2048张表

步骤:
1. 创建新库: order_db_4 ~ order_db_7
2. 修改分片算法: user_id % 4 → user_id % 8
3. 数据迁移:
   - order_db_0 中 user_id % 8 in [4,5,6,7] 的数据迁移到新库
   - order_db_1 中 user_id % 8 in [4,5,6,7] 的数据迁移到新库
4. 修改应用配置,发布上线

迁移数据量: 50% (一半数据需要迁移)

避免频繁扩容: 一次性规划足够的分片数量 (如1024张表,支持10亿级数据)

9.3 分库分表后如何保证事务一致性?

单分片事务 (无问题)

@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
    // user_id相同,订单和订单明细在同一分片
    orderRepository.save(order);
    orderItemRepository.saveBatch(items);
}

跨分片事务 (需要分布式事务)

@GlobalTransactional  // Seata分布式事务
public void createOrder(Order order) {
    // 订单: user_id = 1001 → order_db_1
    orderRepository.save(order);

    // 库存: sku_id = 888 → inventory_db_0 (不同分片)
    inventoryService.deduct(skuId, quantity);
}

建议: 避免跨分片事务,业务层解耦


十、最佳实践与总结

10.1 分库分表最佳实践

1. 提前规划,宁可多不可少

  • 预估3年数据量,一次性规划足够的分片数量
  • 推荐: 1024张表 (4库×256表),避免频繁扩容

2. 分片键选择至关重要

  • 选择查询频率最高的维度作为分片键
  • 避免热点分片键 (Top卖家、大V用户)
  • 分片键一旦确定,无法修改

3. 避免跨分片查询

  • 单分片查询性能最佳
  • 跨分片JOIN改为应用层JOIN或数据冗余

4. 读写分离 + 分库分表

  • 主库写入,从库读取,提升吞吐量
  • ShardingSphere同时支持分片和读写分离

5. 监控告警

  • 监控各分片数据量,及时发现数据倾斜
  • 监控慢查询,优化跨分片查询

10.2 核心要点总结

分库分表策略

  • 垂直拆分: 按业务模块拆分,解耦业务
  • 水平拆分: 按数据行拆分,解决数据量问题
  • 混合拆分: 先垂直后水平,生产推荐 ⭐⭐⭐⭐⭐

分片算法

  • 取模算法: 最常用,数据分布均匀
  • 范围算法: 历史数据归档方便
  • 一致性哈希: 动态扩容场景
  • 时间范围: 日志表、流水表

分布式ID

  • Snowflake: 推荐,性能高,趋势递增
  • 数据库号段: 简单,依赖数据库
  • Redis INCR: 高性能,依赖Redis

跨分片查询

  • 单分片查询: 性能最佳,推荐
  • 广播查询: 性能差,避免使用
  • 优化方案: 数据冗余,应用层JOIN

深分页优化

  • ID范围查询: 性能稳定,推荐
  • 业务限制: 最多100页,超过用搜索
  • 延迟关联: 支持跳页,性能提升明显

数据迁移

  • 停机迁移: 简单,适合非核心业务
  • 不停机迁移: 双写策略,零业务中断,推荐 ⭐⭐⭐⭐⭐

通过深入掌握分库分表核心原理,结合ShardingSphere等成熟框架,能够构建支撑亿级数据的高性能数据库架构,助力业务快速增长。


参考资料