商品系统案例

wen java案例 1

本文目录导读:

商品系统案例

  1. 业务模型总览(架构思维)
  2. 数据库表设计(DDL 实战)
  3. 核心状态机(生命周期设计)
  4. 核心接口设计(API 定义)
  5. 核心难点:库存扣减的并发控制方案
  6. 实战细节补充(拓展视野)
  7. 总结自测清单

这是一个非常经典的后端工程案例,商品系统是电商、供应链、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 缓存 + 异步同步): 目的:应对高并发秒杀场景。

  1. 商品上架时,将库存预热到 Redis:SET sku_stock_{id} 100
  2. 请求扣减:使用 Lua 脚本 保证原子性。
    -- 扣减库存脚本
    local stock = redis.call('get', KEYS[1])
    if tonumber(stock) > 0 then
       redis.call('decr', KEYS[1])
       return 1
    end
    return 0
  3. 发送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;

实战细节补充(拓展视野)

  1. 多规格/组合属性(规格模板):前端往往通过 SKU 的属性组合(如颜色+尺寸)进行联动选择,数据库推荐使用 JSON 类型存储规格,查询时用 JSON_CONTAINS 或在内存中组装。
  2. 商品搜索:千万不要用 LIKE '%名字%' 去数据库搜,生产环境通常会使用 Elasticsearch 或者 OpenSearch,数据库落库后,通过 Canal 监听 Binlog 变更,同步到 ES,提供关键词搜索和筛选。
  3. 历史价格追踪:如果要求展示“价格曲线”,需要设计 product_price_history 表,在价格更新时插入一条记录。
  4. 软删除:物理删除会导致订单历史表外键失效,通常使用 deleted 字段(0/1)标记,所有查询默认过滤 WHERE deleted = 0

总结自测清单

如果去面试或被要求画架构图,你可以按这个逻辑讲:

  1. 讲模型:SPU + SKU 解决了多样化和库存管理的矛盾。
  2. 讲状态:状态机约束了商品在各个阶段的行为。
  3. 讲并发:通过“乐观锁(版本号)或 LUA脚本 Redis”双管齐下,保证不超卖。
  4. 讲扩展:通过 ES 做检索,通过消息队列做异步化削峰。

这套案例覆盖了从需求分析到高并发设计的全部要点,如果你有具体的业务场景(比如想做多商户平台、生鲜保质期管理、或者虚拟商品),可以继续告诉我,我为你做定制化补充。

抱歉,评论功能暂时关闭!