分表后的分布式访问是核心挑战。核心思路是:将对业务透明的数据路由和聚合逻辑,下沉到专门的数据访问层。有三大主流方案,本文将通过一个具体的电商订单分表例子来详解

假设我们有一个订单表,按订单ID哈希分成了4张表:

  • orders_0(order_id % 4 = 0)

  • orders_1(order_id % 4 = 1)

  • orders_2(order_id % 4 = 2)

  • orders_3(order_id % 4 = 3)

方案一:代理中间件模式(推荐,对应用透明)

架构图

应用程序 → 代理中间件 → 分表0/1/2/3
     (无感知)      (路由+聚合)

1. ShardingSphere-Proxy(推荐)

像一个“数据库翻译官”,应用把它当成一个MySQL用。

-- 应用看到的(逻辑表)
CREATE TABLE orders (
    order_id BIGINT,
    user_id INT,
    amount DECIMAL
);

-- ShardingSphere自动路由到物理表
-- 应用查询(完全无感知)
SELECT * FROM orders WHERE order_id = 123;
-- Proxy自动转换为:
SELECT * FROM orders_3 WHERE order_id = 123; -- 123 % 4 = 3

-- 跨分片查询也自动处理
SELECT user_id, SUM(amount) FROM orders GROUP BY user_id;
-- Proxy查询所有分表,在内存中聚合结果

部署示例

# proxy的配置片段
rules:
- !SHARDING
  tables:
    orders:
      actualDataNodes: ds_${0..3}.orders_${0..3}
      tableStrategy:
        standard:
          shardingColumn: order_id
          shardingAlgorithmName: order_hash
  shardingAlgorithms:
    order_hash:
      type: HASH_MOD
      props:
        sharding-count: 4

优点:应用零改造,SQL兼容性好。

缺点:代理层可能成为性能瓶颈,需要高可用部署。

2. MyCat / ProxySQL

老牌中间件,配置类似:

<!-- MyCat的schema配置 -->
<schema name="shop" checkSQLschema="false">
    <table name="orders" dataNode="dn0,dn1,dn2,dn3" rule="order_hash"/>
</schema>
<dataNode name="dn0" dataHost="host1" database="db0" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0">
    <heartbeat>select user()</heartbeat>
    <writeHost host="master" url="192.168.0.1:3306"/>
</dataHost>

方案二:客户端SDK模式(高性能,侵入应用)

架构图

应用程序 → JDBC驱动(增强版) → 分表0/1/2/3
     (需少量配置)    (集成在驱动中)

ShardingSphere-JDBC(最流行)

在应用内完成路由,无代理层网络开销。

// Spring Boot配置
spring:
  shardingsphere:
    datasource:
      names: ds0, ds1
      ds0: ...
      ds1: ...
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: ds$->{0..1}.orders_$->{0..1}
            key-generate-strategy: # 分布式ID生成
              column: order_id
              key-generator-name: snowflake
            table-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: order_hash
        sharding-algorithms:
          order_hash:
            type: HASH_MOD
            props:
              sharding-count: 4

// 业务代码完全不变
@Repository
public class OrderDao {
    public Order getById(Long orderId) {
        // 框架自动路由到正确的分表
        return jdbcTemplate.queryForObject(
            "SELECT * FROM orders WHERE order_id = ?", 
            orderId // 框架根据这个值计算分表
        );
    }
}

复杂查询示例

// 1. 范围查询(触发全表扫描)
List<Order> orders = orderDao.findByCreateTimeBetween(
    startTime, endTime); 
// 框架会查询所有分表:orders_0,1,2,3,然后合并结果

// 2. 跨分片事务
@ShardingTransactionType(TransactionType.XA) // 启用分布式事务
@Transactional
public void transferOrder(Long fromOrderId, Long toOrderId) {
    // 可能涉及两个不同分表的更新
    updateOrderStatus(fromOrderId, "CANCELLED");
    updateOrderStatus(toOrderId, "CREATED");
}

优点:性能最好,无代理层网络跳转。

缺点:侵入应用,升级需要重启应用,对连接池有影响。


方案三:微服务+数据分治模式(现代架构)

架构图

订单服务 → 订单表0
用户服务 → 订单表1
支付服务 → 订单表2
(按业务域垂直分库+水平分表)

按业务域分治

# 不同服务管理不同分表
order-service:        # 管理尾号0,1的订单
  datasource:
    - orders_0
    - orders_1
    
user-order-service:   # 管理用户维度的订单(尾号2)
  datasource:
    - orders_2
    
archive-service:      # 管理归档订单(尾号3)
  datasource:
    - orders_3

API聚合层

// 在API网关或BFF层聚合
@GetMapping("/orders/{userId}")
public List<Order> getUserOrders(@PathVariable Long userId) {
    // 并行查询不同服务
    CompletableFuture<List<Order>> future1 = 
        orderService.getOrders(userId);
    CompletableFuture<List<Order>> future2 = 
        userOrderService.getOrders(userId);
    
    // 合并结果
    return CompletableFuture.allOf(future1, future2)
        .thenApply(v -> {
            List<Order> result = new ArrayList<>();
            result.addAll(future1.join());
            result.addAll(future2.join());
            return result;
        }).join();
}

优点:彻底解耦,按业务伸缩。

缺点:架构复杂,需要服务治理,数据一致性挑战大。


必须解决的三大核心问题

问题1:分布式ID生成

分表后,数据库自增ID不再适用。

// 方案1:Snowflake算法(推荐)
public class SnowflakeIdGenerator {
    // 生成64位ID:时间戳(41位)+机器ID(10位)+序列号(12位)
    // 示例:1541815603606036480(全局唯一、趋势递增)
}

// 方案2:数据库号段模式
CREATE TABLE id_generator (
    biz_tag VARCHAR(128) PRIMARY KEY,
    max_id BIGINT NOT NULL,
    step INT NOT NULL
);
-- 每次获取一批ID:UPDATE ... max_id = max_id + step

// 方案3:Redis原子操作
Long id = redis.incr("order_id_counter");
// 结合业务前缀:ORDER_20240101_00000001

问题2:跨分片查询

方案A:应用层聚合(推荐)
public List<Order> searchOrders(OrderQuery query) {
    List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
    
    // 并行查询所有分片
    for (int i = 0; i < 4; i++) {
        String table = "orders_" + i;
        futures.add(CompletableFuture.supplyAsync(() -> 
            queryShard(table, query)
        ));
    }
    
    // 合并结果
    return futures.stream()
        .flatMap(future -> future.join().stream())
        .sorted(Comparator.comparing(Order::getCreateTime).reversed())
        .limit(query.getPageSize())
        .collect(Collectors.toList());
}
方案B:建立全局二级索引
-- 针对非分片键的查询,建立单独的索引表
CREATE TABLE order_user_index (
    user_id BIGINT,
    order_id BIGINT,  -- 分片键
    shard_id TINYINT,  -- 分片编号
    PRIMARY KEY(user_id, order_id)
);

-- 查询流程:
-- 1. 查询索引表:SELECT shard_id, order_id FROM order_user_index WHERE user_id=?
-- 2. 到对应分表查询详情
方案C:异步聚合到宽表/ES
// 使用CDC(Debezium/Canal)同步到ES
订单表分片 → Binlog → CDC采集 → Kafka → ES索引

// 查询直接走ES
List<Order> orders = elasticsearchTemplate.search(
    QueryBuilders.boolQuery()
        .must(termQuery("user_id", userId))
        .must(rangeQuery("amount").gte(100)),
    Order.class
);

问题3:分布式事务

场景:下单扣库存
-- 订单在分表1,库存表在另一个库

解决方案

// 方案1:Seata AT模式(推荐)
@GlobalTransactional
public void placeOrder(OrderDTO order) {
    // 1. 扣减库存(库存服务)
    inventoryService.reduce(order.getSkuId(), order.getQuantity());
    
    // 2. 创建订单(订单服务,可能跨分片)
    orderService.create(order);  // 可能需要操作多个分表
    
    // 3. 扣减余额(账户服务)
    accountService.deduct(order.getUserId(), order.getAmount());
    
    // Seata保证这三个操作的原子性
}

// 方案2:基于消息的最终一致
public void placeOrder(OrderDTO order) {
    // 1. 预扣库存(本地事务)
    boolean success = inventoryService.tryReduce(order);
    
    if (success) {
        // 2. 发送创建订单消息
        rocketMQTemplate.send("order_topic", order);
        // 3. 消费者异步创建订单(可能重试)
    }
}

选型决策指南

根据团队规模选择

小型团队 (3-5人):
  - 推荐: ShardingSphere-Proxy
  - 理由: 运维简单,应用无需改造

中型团队 (5-20人):
  - 推荐: ShardingSphere-JDBC + 配置中心
  - 理由: 性能更好,灵活性高

大型团队 (20+人):
  - 推荐: 微服务分治 + 专门的DAL层
  - 理由: 按业务域自治,便于多团队协作

根据业务阶段选择

初创期 (数据量 < 1000万):
  - 建议: 先不分表,做好监控
  - 优化: 索引、缓存、读写分离

成长期 (1000万 ~ 1亿):
  - 建议: 垂直分表 + 简单水平分表
  - 方案: ShardingSphere-JDBC,2-4个分片

成熟期 (> 1亿):
  - 建议: 完整的分布式数据架构
  - 方案: 多级分片 + 全局索引 + 分布式事务

根据查询模式选择

-- 场景1:主键/分片键查询占80%以上
-- 方案:客户端SDK模式(ShardingSphere-JDBC)

-- 场景2:复杂查询多,多维度查询
-- 方案:代理模式 + ES/CK搜索引擎

-- 场景3:实时性要求不高,偏分析
-- 方案:分库分表 + 数据同步到数仓

实施步骤(推荐路径)

步骤1:评估与准备

-- 1. 分析现有表
SELECT 
    table_name,
    table_rows,
    data_length/1024/1024 as data_mb,
    index_length/1024/1024 as index_mb
FROM information_schema.tables 
WHERE table_schema = 'your_db';

-- 2. 分析查询模式
-- 使用慢查询日志或性能模式
SELECT * FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text LIKE '%orders%'
ORDER BY sum_timer_wait DESC;

步骤2:渐进式实施

// 阶段1:双写过渡
public void createOrder(Order order) {
    // 同时写入老表和新分表
    jdbcTemplate.update("INSERT INTO orders VALUES (?,?,?)", ...);
    jdbcTemplate.update("INSERT INTO orders_" + shardKey + " VALUES (?,?,?)", ...);
    
    // 验证数据一致性
    assert oldData.equals(newData);
}

// 阶段2:读切流(逐步迁移读流量)
// 阶段3:写切流(最终完全切到分表)

步骤3:监控与优化

监控指标:
  - 分片均衡度: 各分表数据量差异 < 20%
  - 查询响应时间: P95 < 100ms
  - 跨分片查询比例: < 5%
  - 分布式事务成功率: > 99.9%

告警规则:
  - 单个分片数据量超过阈值
  - 跨分片查询耗时突增
  - 分布式事务失败率升高

总结:分表只是手段,不是目的。在分表前,先考虑是否可以:优化查询、增加索引、读写分离、使用缓存。只有当这些手段都用尽,且数据量确实达到瓶颈时,才需要分表。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐