订单系统分库分表方案设计与实现

一、分库分表核心认知

1.1 分库分表的必要性

MySQL 作为单机关系型数据库,不适合存储 TB 级海量数据 ,且单机并发能力有限;但电商交易类系统(订单、支付)必须使用 MySQL,因为其能提供金融级事务保证 ,而分布式事务方案目前仍不完善。分库分表是解决 MySQL海量数据存储高并发访问的最终方案,核心是将大表 / 大库拆分为多个小表 / 小库,降低单库单表的数据量和访问压力。

1.2 分库 vs 分表的区别与适用场景

维度 分表 分库
拆分对象 单数据库实例内的多张表 多个独立的 MySQL 数据库实例
解决问题 数据量过大导致的查询慢 并发请求量过高的问题
核心原理 减少单表数据量,优化 B + 树 分散并发请求到多个实例
适用场景 只读查询可缓存,事务内 CRUD 慢 单实例数据库连接池打满、QPS 过高

核心原则 :能不拆就不拆,拆分越分散,系统维护成本、故障概率越高,一般业务需同时做分库分表,根据预估并发量和数据量计算拆分数量。

1.3 分库分表的实现方式对比

分库分表在代码层的实现有 3 种方式,面试高频考点为优缺点及选型 ,核心推荐组件方式

实现方式 具体做法 优点 缺点 适用场景
纯手工方式 修改 DAO 层,定义多数据源,硬编码指定数据源 实现简单,无额外依赖 代码侵入性极强,维护成本高,扩展性差 超简单微服务(仅少量 SQL)、无合适组件的编程语言
组件方式 应用内集成 Sharding-JDBC 等组件,代理数据库请求并自动路由 代码侵入性低,性能 / 稳定性兼顾,支持读写分离 + 分库分表 需引入组件并做少量配置 绝大多数 Java 电商 / 分布式系统(推荐)
代理方式 应用与数据库间部署 Atlas/Sharding-Proxy 代理,代理伪装为单节点 MySQL 对应用完全透明,无需修改代码 增加调用链路,性能损失,代理节点易成瓶颈 / 故障点 多语言技术栈、无需应用改造的老旧系统

二、订单系统分库分表规划

电商订单系统 为实战案例,讲解分库分表的核心规划步骤,是 Java 开发面试项目实战高频考点。

2.1 数据量预估与拆分数量计算

核心前提

MySQL 单表数据量不宜超过 2000W(保证 B + 树高度,维持查询性能),单条订单数据约 1KB。

订单系统数据预估
  • 月订单量:2000W,年订单量:2.4 亿
  • 单订单平均商品数:10 个,年订单详情数:24 亿
拆分数量决策
  1. 仅按订单表计算:2.4 亿 / 2000W = 12 张,取 2 的幂为16 张
  2. 考虑订单详情表:24 亿 / 2000W = 120 张,取 2 的幂为128 张
  3. 实际最终决策:订单表 + 订单详情表均拆分为 32 张 (订单详情表单表数据量达 8000W)。
    • 决策原因:兼顾表关联性能 (订单表与详情表一一关联,表数量一致可避免跨表关联)和维护成本(128 张表维护成本过高)。

2.2 分片键选择

2.2.1 分片键定义

分片键(Sharding Key)是分库分表的拆分依据列 ,选择原则:与业务访问方式高度匹配 ,避免出现全表扫描 / 全分片查询(性能极差)。

2.2.2 订单系统分片键的问题与解决
问题 1:单一分片键的缺陷
  • 订单 ID:按用户 ID 查询「我的订单」时,无法定位分片,需全分片查询;
  • 用户 ID:按订单 ID 查询时,无法定位分片,需全分片查询。
解决方案:订单 ID 融合用户 ID

生成订单 ID 时,将用户 ID 的后两位拼接至全局唯一订单 ID 后 ,使订单 ID 同时包含用户 ID 特征 ,既支持按订单 ID 定位分片,也支持按用户 ID 定位分片。核心代码实现

复制代码
/**
 * 生成订单ID:全局唯一ID + 用户ID后两位
 * @param memberId 用户ID
 * @return 最终订单ID
 */
public Long generateOrderId(Long memberId) {
    // 从唯一ID服务获取全局唯一订单ID
    String leafOrderId = unqidFeignApi.getSegmentId(OrderConstant.LEAF_ORDER_ID_KEY);
    String strMemberId = memberId.toString();
    // 截取用户ID后两位,不足两位补0
    String orderIdTail = memberId < 10 ? "0" + strMemberId : strMemberId.substring(strMemberId.length() - 2);
    Log.debug("生成订单的orderId,组成元素为:{},{}", leafOrderId, orderIdTail);
    // 拼接并转换为Long
    return Long.valueOf(leafOrderId + orderIdTail);
}
问题 2:多维度查询的解决

订单系统除了用户 ID / 订单 ID 查询 ,还有商家按店铺 ID 查询、报表统计 等场景,分库分表后无法直接支持。解决方案数据异构

  1. 构建以店铺 ID 为分片键的只读订单库,专供商家查询;
  2. 将订单数据同步到 HDFS 等分布式存储,通过大数据技术生成报表;
  3. 核心思路:分库分表库负责交易写操作,异构库负责多维度读操作,读写分离。

2.3 分片算法选择

订单系统选用哈希分片(取模算法),是分布式系统中最常用的分片算法,面试需掌握其原理与实现。

核心原理

分片键(订单 ID / 用户 ID)的后两位 进行表数量(32)取模,计算得到具体的物理表后缀,定位分片。

核心代码片段
复制代码
// 截取订单号/客户ID的后两位 → 转int → 对表数量取模 → 定位物理表
.map(id -> id.substring(id.length()-2))
.distinct()
.map(Integer::new)
.map(idSuffix -> idSuffix % availableTargetNames.size())
分片算法对比
分片算法 原理 优点 缺点 适用场景
哈希取模 分片键哈希后对分片数取模 数据分布均匀,查询速度快 扩容时数据迁移量大 订单 / 用户等无规律数据
范围分片 按分片键范围拆分(如时间 / ID) 扩容简单,适合排序查询 数据易倾斜 日志 / 流水等按时间增长的数据
查表法 维护分片映射表,按表查询 灵活性高 映射表成瓶颈,维护成本高 定制化业务场景

三、订单系统分库分表具体实现

订单系统选用Sharding-JDBC 实现分库分表 + 读写分离,是 Java 开发面试技术选型 + 配置高频考点,以下为核心配置与代码实现。

3.1 核心配置

包含数据源配置、分库分表配置、读写分离配置,关键配置做注释说明:

复制代码
# 分库分表核心配置(ShardingSphere)
shardingsphere:
  # 数据源配置:主库+从库
  datasource:
    names: ds-master,ds-slave
    # 主库数据源
    ds-master:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.65.223:3306/tl_mall_order?serverTimezone=UTC&useSSL=false&useUnicode=true
      username: tlmall
      password: tlmall123
      initialSize: 5
      minIdle: 10
      maxActive: 30
      validationQuery: SELECT 1 FROM DUAL
    # 从库数据源
    ds-slave:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.65.137:3306/tl_mall_order?serverTimezone=UTC&useSSL=false&useUnicode=true
      username: tlmall
      password: tlmall123
      initialSize: 5
      minIdle: 10
      maxActive: 30
      validationQuery: SELECT 1 FROM DUAL
  # 分库分表配置
  sharding:
    default-data-source-name: ds-master # 默认数据源
    default-database-strategy: none # 暂不分库,仅分表
    tables:
      # 订单主表:oms_order_0~31
      oms_order:
        actual-data-nodes: ds_ms.oms_order_$->{0..31} # 物理表节点
        table-strategy:
          complex: # 复合分片键(id+member_id)
            sharding-columns: id,member_id # 分片键
            algorithm-class-name: com.tuling.tulingmall.ordercurr.sharding.OmsOrderShardingAlgorithm # 自定义分片算法
      # 订单详情表:oms_order_item_0~31
      oms_order_item:
        actual-data-nodes: ds_ms.oms_order_item_$->{0..31}
        table-strategy:
          complex:
            sharding-columns: order_id # 分片键(关联订单主表ID)
            algorithm-class-name: com.tuling.tulingmall.ordercurr.sharding.OmsOrderItemShardingAlgorithm # 自定义分片算法
    binding-tables: oms_order,oms_order_item # 绑定表(避免跨表关联)
    broadcast-tables: oms_order_operate_history,oms_company_address # 广播表(全部分片同步,数据量小)
  # 读写分离配置
  master-slave-rules:
    ds_ms: # 主从数据源逻辑名
      master-data-source-name: ds-master # 主库
      slave-data-source-names: [ds-slave] # 从库(支持多个)
      load-balance-algorithm-type: ROUND_ROBIN # 负载均衡算法:轮询
  # 显示SQL解析结果(调试用)
  props:
    sql:
      show: true

3.2 核心分片算法实现

自定义两个分片算法类:OmsOrderShardingAlgorithm(订单主表)、OmsOrderItemShardingAlgorithm(订单详情表),核心逻辑一致,以下为订单主表实现核心代码:

复制代码
/**
 * 订单主表自定义分片算法:复合分片键(id+member_id),取后两位取模32
 */
public class OmsOrderShardingAlgorithm implements ComplexKeysShardingAlgorithm<String> {
    // 分片键常量
    private static final String COLUMN_ORDER_SHARDING_KEY = "id";
    private static final String COLUMN_CUSTOMER_SHARDING_KEY = "member_id";

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<String> complexKeysShardingValue) {
        // 1. 获取分片键值:订单ID、用户ID
        Collection<String> orderIds = complexKeysShardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_ORDER_SHARDING_KEY, new ArrayList<>());
        Collection<String> memberIds = complexKeysShardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_CUSTOMER_SHARDING_KEY, new ArrayList<>());
        
        // 2. 合并分片键值,避免重复
        List<String> ids = new ArrayList<>(16);
        if (Objects.nonNull(orderIds)) {
            ids.addAll(orderIds);
        }
        if (Objects.nonNull(memberIds)) {
            ids.addAll(memberIds);
        }
        
        // 3. 核心分片逻辑:截取后两位 → 取模32 → 定位物理表
        return ids.stream()
                .filter(Objects::nonNull)
                .map(id -> id.substring(id.length() - 2)) // 截取后两位
                .distinct() // 去重
                .map(Integer::new) // 转int
                .map(idSuffix -> idSuffix % availableTargetNames.size()) // 对表数量取模
                .map(String::valueOf) // 转字符串
                // 匹配物理表名(如oms_order_0)
                .map(tableSuffix -> availableTargetNames.stream()
                        .filter(targetName -> targetName.endsWith(tableSuffix))
                        .findFirst()
                        .orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
}

3.3 重点概念

  1. 绑定表:指分片规则一致的关联表(如订单主表 + 订单详情表),Sharding-JDBC 会优化绑定表的关联查询,避免跨分片关联,提升性能;
  2. 广播表:指所有分片都同步的表,数据量小且极少修改(如订单操作历史、公司地址),无需分片,全部分片保存一份;
  3. 复合分片键:指多个列共同作为分片键(如订单表的 id+member_id),支持多维度查询的分片定位;
  4. 主从负载均衡:Sharding-JDBC 支持轮询(ROUND_ROBIN)、随机(RANDOM)等算法,实现从库的负载均衡。

五、流程图解

5.1 订单 ID 生成流程

5.2 订单系统分库分表 + 读写分离架构

5.3 哈希分片定位流程

相关推荐
倚肆1 小时前
WebSocket 完整教程:从安装到实战
java·websocket
亓才孓1 小时前
[SpringBoot]@SpringBootTest标签作用
java·spring boot·log4j
倚肆2 小时前
Spring WebSocket 的 MessageBrokerRegistry 与 StompEndpointRegistry 配置参数详解
java·websocket
弹简特2 小时前
【JavaEE09-后端部分】SpringMVC04-SpringMVC第三大核心-处理响应和@RequestMapping详解
java·spring boot·spring·java-ee·tomcat
漫霂2 小时前
Redis在Spring Boot中的应用
java·后端
浅念-2 小时前
C++ STL vector
java·开发语言·c++·经验分享·笔记·学习·算法
亓才孓2 小时前
[Mybatis]MyBatisSystemException(由于Connection的encoding引起的)
java·开发语言·mybatis
Never_Satisfied2 小时前
在c#中,如何在字符串的第x个字符位置插入字符
java·开发语言·c#
m0_531237172 小时前
C语言-while循环,continue/break,getchar()/putchar()
java·c语言·算法