本文目录导读:

这是一个非常经典的后端工程案例,商品系统是电商、供应链、ERP系统的核心模块,为了让你有一个从0到1的全局视角,下面从业务建模、数据库设计、核心接口、状态机设计、库存并发控制五个维度,为你拆解一个标准的商品系统。
业务模型总览(架构思维)
商品系统通常采用 SPU(标准产品单元) + SKU(库存量单位) 的两级模型,这是最核心的概念。
- SPU:指的是商品(横向维度),华为Mate 60 Pro”。
- SKU:指的是具体的售卖规格(纵向维度),华为Mate 60 Pro - 雅丹黑 - 12GB+512GB”。
- 类目:用于属性继承(例如手机 > 屏幕尺寸)。
- 品牌:独立的品牌表关联。
数据库表设计(DDL 实战)
这是一个精简但完整的核心表结构设计(MySQL 8.0+):
-- 1. 品牌表(简单示例)
CREATE TABLE `brand` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL COMMENT '品牌名称',
`logo_url` varchar(255) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT '1' COMMENT '1启用 0禁用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='品牌表';
-- 2. SPU 表(商品主表)
CREATE TABLE `spu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL COMMENT '商品标题',
`sub_title` varchar(255) DEFAULT NULL COMMENT '副标题',
`category_id` bigint NOT NULL COMMENT '三级类目ID',
`brand_id` bigint DEFAULT NULL,
`main_image` varchar(512) DEFAULT NULL COMMENT '主图URL',
`detail_html` longtext COMMENT '商品详情(富文本)',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '商品状态: 0草稿,1上架,2下架',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB COMMENT='SPU商品表';
-- 3. SKU 表(销售规格/库存表)
CREATE TABLE `sku` (
`id` bigint NOT NULL AUTO_INCREMENT,
`spu_id` bigint NOT NULL, varchar(128) NOT NULL COMMENT '规格描述,如:黑色/256G',
-- 价格与库存字段(注意用Decimal避免精度丢失)
`price` decimal(10,2) NOT NULL COMMENT '售价',
`origin_price` decimal(10,2) DEFAULT NULL COMMENT '划线价',
`stock` int NOT NULL DEFAULT '0' COMMENT '库存数量',
`locked_stock` int NOT NULL DEFAULT '0' COMMENT '锁定库存(防止超卖)',
-- 规格属性 JSON格式存储,便于扩展,如 [{"k":"颜色","v":"黑色"}]
`specs` json DEFAULT NULL,
`image` varchar(512) DEFAULT NULL COMMENT '规格图片',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '1启用 0禁用',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号(防并发)',
PRIMARY KEY (`id`),
KEY `idx_spu_id` (`spu_id`)
) ENGINE=InnoDB COMMENT='SKU商品规格表';
设计要点:
locked_stock用于处理下单预占库存(冻结),version用于处理高并发下的乐观锁。
核心状态机(生命周期设计)
商品状态流转是商品系统的核心业务逻辑,设计状态机时需要遵循单向且不逆流(除非特殊异常)。
graph LR
A[草稿/待审核] -->|提交审核| B[审核中]
B -->|审核通过| C[待上架]
B -->|审核驳回| A
C -->|上架操作| D[已上架]
D -->|定时下架/手动下架| E[已下架]
E -->|重新上架| D
D -->|删除| F[逻辑删除]
E -->|删除| F
- 注意:通常绝不允许“上架中”直接修改关键属性(价格、规格),如果要修改,必须走“下架 -> 编辑 -> 审核 -> 上架”流程。
核心接口设计(API 定义)
通常情况下,商品服务内部接口(开放给商家端/admin)和管理端接口会分开设计。
商品创建/编辑接口(写操作)
POST /api/v1/products/spu
{
"name": "iPhone 15 Pro",
"category_id": 123,
"brand_id": 1,
"main_image": "https://xxx.com/1.jpg",
// 接收 SKU 列表
"sku_list": [
{
"title": "原色钛金属/256G",
"price": 7999.00,
"stock": 100,
"specs": [{"k": "颜色", "v": "原色"}, {"k": "内存", "v": "256G"}]
},
{
"title": "原色钛金属/512G",
"price": 9999.00,
"stock": 50,
"specs": [{"k": "颜色", "v": "原色"}, {"k": "内存", "v": "512G"}]
}
]
}
商品查询接口(读操作)
- BFF(面向客户端):
GET /api/v1/products/{spuId}-> 返回聚合数据(SPU信息 + 所有启用的SKU + 品牌名 + 类目路径)。 - 管理后台:
GET /api/v1/admin/products?status=1&page=1&size=10-> 返回分页列表。
库存扣减接口(重中之重 - 防止超卖)
这是面试中考察并发安全的核心问题,下面给出3种方案。
核心难点:库存扣减的并发控制方案
为了避免超卖(Oversell),禁止使用“查库存 -> 判断 -> 减库存”的简单流程(存在竞态条件)。
方案 A(推荐-乐观锁): 利用数据库的行级锁或乐观锁直接更新。
-- 核心 SQL:一步完成扣减,且避免超卖,利用 version 字段
UPDATE sku
SET stock = stock - 1,
version = version + 1
WHERE id = #{skuId}
AND stock - 1 >= 0
AND version = #{oldVersion};
-- 受影响行数 = 1 代表成功,= 0 代表库存不足或版本冲突
方案 B(Redis 缓存 + 异步同步): 目的:应对高并发秒杀场景。
- 商品上架时,将库存预热到 Redis:
SET sku_stock_{id} 100。 - 请求扣减:使用 Lua 脚本 保证原子性。
-- 扣减库存脚本 local stock = redis.call('get', KEYS[1]) if tonumber(stock) > 0 then redis.call('decr', KEYS[1]) return 1 end return 0 - 发送MQ消息(如 RocketMQ/Kafka)异步将扣减记录持久化到 MySQL,并增加
sold字段。
方案 C(数据库悲观锁):
在事务中先 SELECT ... FOR UPDATE 锁行,再更新,这种方式适合并发量不高但绝对不允许错误的场景(如ERP系统)。
START TRANSACTION; SELECT * FROM sku WHERE id = 1 FOR UPDATE; -- 业务判断 if stock > 0 UPDATE sku SET stock = stock - 1 WHERE id = 1; COMMIT;
实战细节补充(拓展视野)
- 多规格/组合属性(规格模板):前端往往通过 SKU 的属性组合(如颜色+尺寸)进行联动选择,数据库推荐使用 JSON 类型存储规格,查询时用
JSON_CONTAINS或在内存中组装。 - 商品搜索:千万不要用
LIKE '%名字%'去数据库搜,生产环境通常会使用 Elasticsearch 或者 OpenSearch,数据库落库后,通过 Canal 监听 Binlog 变更,同步到 ES,提供关键词搜索和筛选。 - 历史价格追踪:如果要求展示“价格曲线”,需要设计
product_price_history表,在价格更新时插入一条记录。 - 软删除:物理删除会导致订单历史表外键失效,通常使用
deleted字段(0/1)标记,所有查询默认过滤WHERE deleted = 0。
总结自测清单
如果去面试或被要求画架构图,你可以按这个逻辑讲:
- 讲模型:SPU + SKU 解决了多样化和库存管理的矛盾。
- 讲状态:状态机约束了商品在各个阶段的行为。
- 讲并发:通过“乐观锁(版本号)或 LUA脚本 Redis”双管齐下,保证不超卖。
- 讲扩展:通过 ES 做检索,通过消息队列做异步化削峰。
这套案例覆盖了从需求分析到高并发设计的全部要点,如果你有具体的业务场景(比如想做多商户平台、生鲜保质期管理、或者虚拟商品),可以继续告诉我,我为你做定制化补充。