电商订单系统设计与实现

一、订单系统核心业务设计

1.1 系统整体架构

订单系统分为当前订单服务(tulingmall-order-curr)历史订单服务(tulingmall-order-history),采用微服务拆分 + 分库分表存储,核心是保证订单数据全流程一致性。

1.2 核心数据表设计

电商订单系统必备 4 张核心表,笔记中做了业务简化,修复笔记错误 :原笔记中 "订单表和订单详情表虽然记录数上是一对一的关系" 为笔误,实际是一对多(一个订单对应多个商品条目)。

标准表名 笔记简化后表名 核心作用 简化点说明
订单主表 oms_order 存储订单基础信息、订单状态 合并支付 / 优惠表字段到主表
订单商品表 oms_order_item 存储订单中商品明细 无简化
订单支付表 合并至 oms_order 存储支付 / 退款信息 用主表status+ 支付字段替代
订单优惠表 取消 存储优惠券 / 满减等优惠信息 业务中取消促销优惠,故删除

订单状态枚举(oms_order.status)0-待付款、1-待发货、2-已发货、3-已完成、4-已关闭、5-无效订单

1.3 核心业务流程

用户下单核心分为订单确认、订单提交、支付、履约四大步骤,本质是标准的 CRUD,但需保证分布式场景下的操作原子性。

1.4 事务使用原则

  1. 单机场景:使用数据库本地事务,保证订单主表 / 商品表插入的原子性(要么都成功,要么都失败);
  2. 微服务场景:本地事务无法满足,需使用分布式事务(如 Seata、RocketMQ 事务消息),解决跨服务(订单 + 商品库存)的数据一致性;
  3. 核心要求:任何订单状态更新操作,必须保证事务原子性,避免数据脏写。

二、订单系统数据一致性核心问题

订单系统的核心痛点是重复请求导致的数据错误 ,需通过幂等性设计 解决,核心包含重复下单问题ABA 问题 两类,是面试高频率考点

2.1 订单重复下单问题

问题成因

用户重复点击、网络重传、RPC 框架 / 网关自动重试,导致多次发送创建订单请求,最终生成多条相同订单。注意:前端按钮置灰无法从根本解决,仅为辅助手段。

解决思路:基于数据库主键唯一约束实现幂等性

核心是预先生成全局唯一订单号,将其作为订单表主键,利用 MySQL 主键唯一约束,保证多次插入仅一次成功。

实现步骤
核心代码实现
复制代码
// 生成订单号接口
@ApiOperation("获取orderId,可避免重复下单")
@GetMapping("/generateOrderId")
public CommonResult generateOrderId(@RequestHeader("memberId") Long memberId) {
    Long orderId = portalOrderService.generateOrderId(memberId);
    return CommonResult.success(orderId);
}

// 生成订单号核心逻辑:唯一ID+用户ID后两位
public Long generateOrderId(Long memberId) {
    // 从唯一ID服务获取分段ID
    String leafOrderId = unqidFeignApi.getSegmentId(OrderConstant.LEAF_ORDER_ID_KEY);
    String strMemberId = memberId.toString();
    // 拼接用户ID后两位,为分库分表做准备
    String orderIdTail = memberId < 10 ? "0" + strMemberId : strMemberId.substring(strMemberId.length() - 2);
    return Long.valueOf(leafOrderId + orderIdTail);
}

// 插入订单时捕获主键冲突异常
try {
    orderMapper.insert(order);
} catch (DuplicateKeyException | SQLIntegrityConstraintViolationException e) {
    // 重复下单,直接返回成功
    return CommonResult.success();
}
核心考点
  • 幂等性的定义:操作任意多次执行,与一次执行的影响相同
  • 幂等性实现方案分类:更新本身幂等 (如update set count=10)、外部存储做冲突检测(如数据库主键 / 唯一索引);
  • 为什么不能用 "查询后插入" 解决重复下单?并发场景下,查询和插入非原子操作,会出现幻读,依然会重复创建

2.2 订单 ABA 问题

问题定义

并发更新订单时,因网络延迟 / 重试,导致订单数据被反复修改,最终出现错误数据的情况(与 CAS 的 ABA 问题原理一致)。

典型场景
复制代码
1. 订单支付后,提交物流单号666,更新成功,但响应网络丢失;
2. 调用方重试,重新提交666,此时订单已被人工修改为888;
3. 重试请求执行后,物流单号被改回666,数据出错。
解决思路:基于版本号(version)的乐观锁实现

为订单主表增加version字段,通过版本号校验 + 原子更新保证更新的幂等性,避免 ABA 问题。

实现原则
  1. 查询订单时,将version一起返回给前端;
  2. 更新订单时,携带version仅当数据库版本号与传入版本号一致时才更新
  3. 更新成功后,版本号自增 1 ,且校验 + 更新 + 版本自增必须在同一个事务中,保证原子性。
核心 SQL
复制代码
-- 带版本号的更新,原子操作
UPDATE oms_order 
SET tracking_number = 666, version = version + 1 
WHERE id = ? AND version = ?;
-- 判断更新结果:返回行数>0则更新成功,否则版本号不一致,需重新查询
实现原理
核心考点
  • 订单系统 ABA 问题的产生原因?与并发编程中 CAS 的 ABA 问题有何异同?
  • 版本号乐观锁的实现要点?为什么必须保证 "校验 - 更新 - 自增" 的原子性?
  • 乐观锁和悲观锁在订单系统中的适用场景?(乐观锁:高并发读、低冲突更新;悲观锁:高冲突更新如库存扣减)

2.3 幂等性设计总结

场景 解决方案 核心原理 适用范围
订单创建 预生成订单号 + 主键唯一约束 数据库主键唯一约束,避免重复插入 写操作 - 新增数据
订单更新 版本号乐观锁 版本号校验,保证更新的原子性 写操作 - 更新数据
天然幂等操作 直接执行 操作本身幂等(如更新固定值) 无并发冲突的更新操作

三、数据库架构优化 - 读写分离

订单系统属于用户关联型系统 ,缓存命中率低,大量查询穿透到 MySQL,需通过读写分离 提升数据库并发能力,是面试中间件 / 数据库优化高频考点

3.1 读写分离核心原理

将 MySQL 分为主库(Master)从库(Slave) ,主库负责写操作 (订单创建 / 更新),从库负责读操作 (订单查询),主库通过主从复制将数据同步到从库,多个从库分担查询压力。

3.2 读写分离的适用场景

  • 读写比例严重失衡(互联网系统通常 9:1 及以上);
  • 读操作并发量高,写操作相对低频;
  • 订单系统、用户中心、购物车等用户关联型系统(缓存命中率低)。

3.3 读写分离的核心问题 - 主从数据不一致

问题成因

主从复制是异步同步 ,主库更新后,数据同步到从库存在毫秒级延迟,若更新后立即从从库查询,会读取到旧数据(如支付成功后立即查询订单,仍显示待付款)。

解决方案
  1. 业务层面规避(推荐) :支付成功后跳转到支付完成中间页,不自动返回订单页,让用户手动查询,规避延迟窗口;
  2. 技术层面解决
    • 关键业务(更新后立即查询):将读请求路由到主库
    • 事务内查询:同一个数据库事务中的读操作,默认路由到主库;
  3. 不推荐:强制主从同步(同步复制会严重降低主库写性能)。

3.4 读写分离的实现方式

  1. 纯手工方式:修改 DAO 层,定义多数据源,手动指定读写数据源(代码侵入性高,适合简单微服务);
  2. 组件方式(推荐):使用 Sharding-JDBC、MyCat 等组件,代理数据源,自动路由读写请求(代码侵入性低,性能优);
  3. 代理方式:部署 Atlas/Sharding-Proxy 代理,应用程序对数据库无感知(调用链路长,有性能损耗,易出现代理瓶颈)。

四、数据库架构优化 - 分库分表

当订单数据量达到亿级 ,单库单表无法支撑,需通过分库分表 拆分数据,解决海量数据存储高并发写 问题,是面试高级考点(分布式架构设计)。

4.1 分库分表核心原则

能小拆就不小拆,能少拆就不多拆:数据拆分越分散,并发 / 维护成本越高,系统故障概率越大,仅当单库单表达到性能瓶颈时才使用。

4.2 分库 vs 分表的区别

维度 分表(单库多表) 分库(多库多实例)
解决问题 单表数据量过大导致的查询慢 单库并发量过高导致的性能瓶颈
数据存储 同一数据库实例,多张表 不同数据库实例,不同表
实现难度 低(仅需拆分表,无需考虑跨库) 高(需解决跨库事务 / 查询)
适用场景 读多写少,数据量超 2000W 高并发写,单库 TPS 达到瓶颈
核心结论 :订单系统需分库 + 分表结合,既解决数据量问题,又解决并发问题。

4.3 订单系统分库分表规划

4.3.1 数据量预估与分片数确定

笔记中预估:月订单 2000W → 年订单 2.4 亿;单订单平均 10 个商品 → 年订单商品 24 亿。MySQL 单表性能阈值 :单表数据量不超过 2000W(保证 B + 树高度,查询性能)。最终分片数 :订单表 + 订单商品表均拆分为32 张表(2 的幂次,便于哈希取模,修复笔记中 "128 张表" 的过度设计问题)。

4.3.2 分片键选择(核心难点)

分片键(Sharding Key)是分库分表的依据,选择原则与业务查询方式强绑定,避免跨分片查询。

订单系统的分片键痛点
  • 仅选订单 ID:无法支持 "我的订单"(按用户 ID 查询),需跨分片扫描;
  • 仅选用户 ID:无法支持 "按订单 ID 查询",需跨分片扫描。
解决方案:订单 ID 拼接用户 ID 后两位

生成订单 ID 时,将用户 ID 后两位 拼接在全局唯一 ID 后,使订单 ID 既包含全局唯一性,又关联用户 ID,实现按订单 ID / 用户 ID 均能定位分片

4.4 分片算法选择

订单系统使用哈希分片算法 (取模法),核心是对订单 ID / 用户 ID 的后两位取模 32,定位到具体的物理表,保证数据均匀分布。

复制代码
// 核心分片逻辑:截取后两位→转int→取模32→定位物理表
ids.stream()
    .map(id -> id.substring(id.length() - 2)) // 截取后两位
    .distinct()
    .map(Integer::new)
    .map(idSuffix -> idSuffix % 32) // 对32取模
    .map(String::valueOf)
    .map(tableSuffix -> availableTargetNames.stream()
        .filter(targetName -> targetName.endsWith(tableSuffix))
        .findFirst().orElse(null))
    .collect(Collectors.toList());

4.5 分库分表的实现(基于 Sharding-JDBC)

订单系统采用Sharding-JDBC 组件方式 实现分库分表 + 读写分离,核心配置实现要点如下:

4.5.1 核心配置(application.yml)
复制代码
spring:
  shardingsphere:
    datasource:
      names: ds-master # 主库数据源
      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
        username: tlmall
        password: tlmall123
    rules:
      sharding:
        tables:
          oms_order: # 订单主表分片配置
            actual-data-nodes: ds-master.oms_order_$->{0..31} # 32张物理表
            table-strategy:
              complex: # 复合分片键
                sharding-columns: id,member_id # 分片键:订单ID+用户ID
                sharding-algorithm-name: oms_order_table_alg
          oms_order_item: # 订单商品表分片配置
            actual-data-nodes: ds-master.oms_order_item_$->{0..31}
            table-strategy:
              complex:
                sharding-columns: order_id # 分片键:订单ID
                sharding-algorithm-name: oms_order_item_table_alg
        sharding-algorithms: # 自定义分片算法
          oms_order_table_alg:
            type: CLASS_BASED
            props:
              algorithmClassName: com.tuling.tulingmall.ordercurr.sharding.OmsOrderShardingAlgorithm
          oms_order_item_table_alg:
            type: CLASS_BASED
            props:
              algorithmClassName: com.tuling.tulingmall.ordercurr.sharding.OmsOrderItemShardingAlgorithm
        binding-tables: [oms_order,oms_order_item] # 绑定表,避免跨表笛卡尔积
        broadcast-tables: [oms_order_setting,oms_order_return_reason] # 广播表,所有分片都有
    props:
      sql-show: true # 打印分片SQL,便于调试
4.5.2 核心实现要点
  1. 复合分片键 :订单主表使用id+member_id作为复合分片键,支持按订单 ID / 用户 ID 双维度查询;
  2. 绑定表 :将oms_orderoms_order_item设为绑定表,保证联表查询时不跨分片,提升性能;
  3. 广播表:将配置类表(如订单设置、退货原因)设为广播表,所有分片均存储一份,避免跨分片查询;
  4. 自定义分片算法 :实现ComplexKeysShardingAlgorithm接口,重写分片逻辑,支持多字段分片。

4.6 分库分表的问题与解决方案

核心问题 产生原因 解决方案
跨分片查询 按非分片键查询(如店铺 ID) 1. 构建专用只读库(按店铺 ID 分片);2. 数据同步到 HDFS/ClickHouse 做大数据分析
跨分片事务 分库后,本地事务无法覆盖多库 使用分布式事务(Seata AT 模式)
分页查询复杂 跨分片分页需合并结果集 1. 尽量避免跨分片分页;2. 使用分片键分页,再合并
数据扩容困难 哈希取模分片,扩容需重新分片 提前规划分片数(2 的幂次);使用一致性哈希
相关推荐
csdn2015_1 小时前
mybatisplus自动生成id
java·mybatis
sheji34161 小时前
【开题答辩全过程】以 基于Java的网上书店销售系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
石去皿2 小时前
数据结构与算法面试核心考点精要
java·算法·面试
学Linux的语莫2 小时前
skills的使用
java·数据库·python
大模型玩家七七2 小时前
关系记忆不是越完整越好:chunk size 的隐性代价
java·前端·数据库·人工智能·深度学习·算法·oracle
一 乐2 小时前
林业资源管理|基于java + vue林业资源管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·林业资源管理系统
lipiaoshuigood2 小时前
微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
java·spring cloud·微服务
Geoking.2 小时前
什么是乐观锁?原理、实现方式与实战详解
java·开发语言
小光学长3 小时前
基于ssm的校园约自习网站23i21xj4(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·数据库·spring