分库分表之后怎么进行 Join 操作?

#中间件#面试八股

分库分表之后怎么进行 Join 操作?

结论:分片后跨库 JOIN 不能交给数据库,只能前置解决:字段冗余(宽表)、绑定表(Binding Table,同键同规则可本地 JOIN)、广播表(Broadcast Table,小表全量复制)、应用层内存组装,以及把复杂查询下沉到搜索引擎或数仓。原则是「能冗余不 JOIN,能同片不跨片」。

一、方案对比

方案做法适用代价
字段冗余订单表冗余 user_name、shop_name维表字段少、变更少一致性维护
绑定表订单与明细同分片键同规则主子表要求分片键完全一致
广播表字典表在每个库各存一份小表、读多写少写放大、同步延迟
应用层组装先查主表,再按 ID 批量 IN 查从表通用多一次往返、代码量
搜索引擎宽表同步到 ES复杂条件检索同步链路、存储成本
数仓T+1 或准实时同步报表分析实时性差

二、绑定表与广播表

绑定表(同分片键 user_id,同规则 %4)        广播表(每库一份)
  ds0: t_order_0 + t_order_item_0             ds0: t_dict_city
  ds1: t_order_1 + t_order_item_1             ds1: t_dict_city
  ds2: t_order_2 + t_order_item_2             ds2: t_dict_city
  ds3: t_order_3 + t_order_item_3             ds3: t_dict_city
   └── 同库同后缀,可下推为本地 JOIN            └── 全量复制,避免跨库

绑定表的三个条件缺一不可:分片键相同、分片算法相同、分片数量相同。少一个,中间件仍然会扇出到全部分片。

三、应用层组装

// 先查订单,再按 userId 批量查用户,避免 N+1
List<Order> orders = orderMapper.selectByUser(userId, page);
Set<Long> userIds = orders.stream()
        .map(Order::getUserId)
        .collect(Collectors.toSet());

Map<Long, User> userMap = userMapper.selectByIds(userIds).stream()
        .collect(Collectors.toMap(User::getId, u -> u));

List<OrderVO> vos = orders.stream().map(o -> {
    OrderVO vo = OrderVO.of(o);
    User u = userMap.get(o.getUserId());
    vo.setUserName(u == null ? "未知用户" : u.getNickname());
    return vo;
}).toList();

批量 IN 有两条纪律:单批不超过 500 个 ID,避免 SQL 文本过长与解析开销;跨库查用户表时,若用户表也分片,这批 ID 会命中多个分片,仍需并行查询后合并。

四、ShardingSphere 配置

rules:
  sharding:
    binding-tables:
      - t_order,t_order_item
    broadcast-tables:
      - t_dict_city
      - t_dict_channel
    tables:
      t_order:
        actual-data-nodes: ds$->{0..3}.t_order_$->{0..3}
        database-strategy:
          standard:
            sharding-column: user_id
            sharding-algorithm-name: db_mod
      t_order_item:
        actual-data-nodes: ds$->{0..3}.t_order_item_$->{0..3}
        database-strategy:
          standard:
            sharding-column: user_id
            sharding-algorithm-name: db_mod

配置生效后,SELECT * FROM t_order o JOIN t_order_item i ON o.id = i.order_id WHERE o.user_id = 42 会被改写成单库单表的本地 JOIN。

五、常见追问

Q1:为什么互联网公司普遍禁止多表 JOIN? 不只是分片问题。JOIN 让优化器的选择空间变大、执行计划不稳定,大表关联容易拖垮实例;同时也会让分库分表、缓存、读写分离难以实施。

Q2:广播表更新怎么办? 由中间件在所有分片执行同一条 DML,本质是分布式写。可靠做法是「写主库 + binlog 同步」或「MQ 广播 + 消费端幂等」,不要依赖中间件的多播事务。

Q3:应用层组装的数据一致性如何保证? 组装只用于展示,不参与判断;涉及金额、库存的决策必须回到主表。允许最终一致,并给冗余字段加版本号或更新时间戳做校验。

小结

  • 分片后 JOIN 的关键是把「跨库」变成「同片」或「应用内」。
  • 绑定表解决主子表,广播表解决字典表,冗余字段解决维表。
  • 复杂检索交给搜索引擎,别在分片数据库上硬扛。
笔记加载中…