一、订单系统核心业务设计
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 事务使用原则
- 单机场景:使用数据库本地事务,保证订单主表 / 商品表插入的原子性(要么都成功,要么都失败);
- 微服务场景:本地事务无法满足,需使用分布式事务(如 Seata、RocketMQ 事务消息),解决跨服务(订单 + 商品库存)的数据一致性;
- 核心要求:任何订单状态更新操作,必须保证事务原子性,避免数据脏写。
二、订单系统数据一致性核心问题
订单系统的核心痛点是重复请求导致的数据错误 ,需通过幂等性设计 解决,核心包含重复下单问题 和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 问题。
实现原则
- 查询订单时,将
version一起返回给前端; - 更新订单时,携带
version,仅当数据库版本号与传入版本号一致时才更新; - 更新成功后,版本号自增 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 读写分离的核心问题 - 主从数据不一致
问题成因
主从复制是异步同步 ,主库更新后,数据同步到从库存在毫秒级延迟,若更新后立即从从库查询,会读取到旧数据(如支付成功后立即查询订单,仍显示待付款)。
解决方案
- 业务层面规避(推荐) :支付成功后跳转到支付完成中间页,不自动返回订单页,让用户手动查询,规避延迟窗口;
- 技术层面解决 :
- 关键业务(更新后立即查询):将读请求路由到主库;
- 事务内查询:同一个数据库事务中的读操作,默认路由到主库;
- 不推荐:强制主从同步(同步复制会严重降低主库写性能)。
3.4 读写分离的实现方式
- 纯手工方式:修改 DAO 层,定义多数据源,手动指定读写数据源(代码侵入性高,适合简单微服务);
- 组件方式(推荐):使用 Sharding-JDBC、MyCat 等组件,代理数据源,自动路由读写请求(代码侵入性低,性能优);
- 代理方式:部署 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 核心实现要点
- 复合分片键 :订单主表使用
id+member_id作为复合分片键,支持按订单 ID / 用户 ID 双维度查询; - 绑定表 :将
oms_order和oms_order_item设为绑定表,保证联表查询时不跨分片,提升性能; - 广播表:将配置类表(如订单设置、退货原因)设为广播表,所有分片均存储一份,避免跨分片查询;
- 自定义分片算法 :实现
ComplexKeysShardingAlgorithm接口,重写分片逻辑,支持多字段分片。
4.6 分库分表的问题与解决方案
| 核心问题 | 产生原因 | 解决方案 |
|---|---|---|
| 跨分片查询 | 按非分片键查询(如店铺 ID) | 1. 构建专用只读库(按店铺 ID 分片);2. 数据同步到 HDFS/ClickHouse 做大数据分析 |
| 跨分片事务 | 分库后,本地事务无法覆盖多库 | 使用分布式事务(Seata AT 模式) |
| 分页查询复杂 | 跨分片分页需合并结果集 | 1. 尽量避免跨分片分页;2. 使用分片键分页,再合并 |
| 数据扩容困难 | 哈希取模分片,扩容需重新分片 | 提前规划分片数(2 的幂次);使用一致性哈希 |