3天搞定照烧鸡肉饭订单系统,图解原理避坑指南
面试被问原理答不上来?别慌。很多后端新人背八股文很溜,一让写个带库存扣减的下单接口就卡壳,尤其是这种看似简单的“照烧鸡肉饭”业务场景,底层涉及并发、状态机和数据一致性,光靠背是学不会的。今天不玩虚的,直接带你从零搭建一个高可用的订单服务,用图解原理的方式拆解核心逻辑。
项目目标与场景拆解
我们要做的不是一个简单的CRUD,而是一个具备生产级特性的订单模块。场景很简单:用户点击“立即购买”,后端接收请求,扣减库存,创建订单,返回订单号。但魔鬼在细节里。
假设照烧鸡肉饭每日限量100份。如果两个用户同时点击,传统写法 if stock 0 然后 stock = stock - 1 会发生什么?两个线程都读到 stock=1,都判断通过,最后 stock=-1,超卖了。这是经典的竞态条件。
我们的目标:防超卖:确保库存不出现负数。
幂等性:用户网络抖动重复点击,不会创建两个订单。
状态清晰:订单从“待支付”到“已取消”或“已完成”的状态流转要严谨。很多教程直接上Redis,但对于初学者,先搞懂数据库层面的悲观锁和乐观锁,再引入缓存,理解才深刻。我们先用最基础的MySQL + Java Spring Boot来夯实地基。
目录结构与依赖配置
一个清晰的工程结构是代码可维护性的基础。以下是本项目核心目录,遵循分层架构原则:
src/main/java/com/food/order/
├── config/
│ └── DataSourceConfig.java # 数据源配置,开启事务
├── controller/
│ └── OrderController.java # REST接口入口
├── service/
│ ├── impl/
│ │ └── OrderServiceImpl.java # 核心业务逻辑
│ └── OrderService.java # 接口定义
├── entity/
│ ├── Order.java # 订单实体
│ └── Product.java # 商品实体(照烧鸡肉饭)
├── mapper/
│ ├── OrderMapper.java # 订单数据访问
│ └── ProductMapper.java # 商品数据访问
└── exception/└── GlobalExceptionHandler.java # 全局异常处理pom.xml 中除了基础的 Spring Boot Web 和 MyBatis Plus,务必加上 mysql-connector-j 和 lombok。不要手动写 Getter/Setter,Lombok 能让代码更聚焦于业务逻辑本身。
核心代码实现与图解原理
这是本文的重头戏。我们将重点讲解如何用 乐观锁 解决超卖问题,并通过代码图解状态流转。
1. 数据库表结构设计
先建表。注意 version 字段,这是乐观锁的关键。
CREATE TABLE t_product (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50) NOT NULL COMMENT '商品名,如:照烧鸡肉饭',stock INT NOT NULL DEFAULT 0 COMMENT '库存',version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE t_order (id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id BIGINT NOT NULL,product_id BIGINT NOT NULL,status TINYINT NOT NULL COMMENT '0:待支付, 1:已支付, 2:已取消',create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2. 商品Mapper:原子性扣减库存
很多新手喜欢先查后改:select stock; if(stock0) update stock = stock-1。这在单线程下没问题,多线程下必死无疑。
正确的做法是将判断和更新合并为一条 SQL。MyBatis Plus 支持这种方式,或者写原生 XML。这里我们用原生 SQL 更直观。
@Mapper
public interface ProductMapper extends BaseMapperProduct {/*** 乐观锁扣减库存* @param id 商品ID* @param expectedVersion 预期的当前版本号* @return 影响行数*/@Update(UPDATE t_product SET stock = stock - 1, version = version + 1 +WHERE id = #{id} AND stock 0 AND version = #{expectedVersion})int decreaseStock(@Param(id) Long id, @Param(expectedVersion) int expectedVersion);
}图解原理:
想象 version 是一个计数器。线程A读到 version=0。
线程B读到 version=0。
线程A执行更新:WHERE version=0,成功,version变为1。
线程B执行更新:WHERE version=0,但数据库里已经是1了,条件不匹配,更新行数为0。
业务层检测到行数为0,抛出“库存不足”或“操作冲突”异常,重试或直接失败。这种机制避免了行级锁带来的性能损耗,适合高并发读多写少场景。
3. 订单Service:事务与状态机
核心业务逻辑在 Service 层。这里要特别注意事务边界。
@Service
public class OrderServiceImpl implements OrderService {@Autowiredprivate ProductMapper productMapper;@Autowiredprivate OrderMapper orderMapper;@Override@Transactional(rollbackFor = Exception.class)public Long createOrder(Long userId, Long productId) {// 1. 查询商品,获取当前版本号Product product = productMapper.selectById(productId);if (product == null) {throw new RuntimeException(商品不存在);}// 2. 尝试扣减库存int rows = productMapper.decreaseStock(productId, product.getVersion());if (rows == 0) {// 库存不足或并发冲突// 这里可以选择重试,但为了简单,直接抛出异常throw new BusinessException(抢购失败,库存不足或网络拥堵,请刷新重试);}// 3. 创建订单Order order = new Order();order.setUserId(userId);order.setProductId(productId);order.setStatus(0); // 待支付orderMapper.insert(order);return order.getId();}
}避坑指南:事务回滚:@Transactional 默认只回滚 RuntimeException。如果抛出的是 Checked Exception,事务不会回滚,导致库存扣了但订单没创建。务必加上 rollbackFor = Exception.class。
幂等性缺失:上面的代码如果用户快速双击,会创建两个订单。生产环境必须加分布式锁或唯一索引约束(如 user_id + product_id + status 的唯一索引,仅限待支付状态)。运行与测试
代码写完了,怎么验证?不能只靠眼睛看。启动服务:确保 MySQL 连接配置正确,启动 Spring Boot 应用。
接口测试:使用 Postman 或 Swagger。URL: POST /api/orders
Body: {userId: 1001, productId: 1}并发测试:这是检验“图解原理”是否落地的关键。使用 JMeter 或简单的 Python 脚本,发起 50 个并发请求,库存设为 10。
预期结果:10 个请求成功,40 个请求返回“抢购失败”。
数据库检查:SELECT stock, version FROM t_product WHERE id=1; 库存应为 0,version 应增加 10。如果库存变成了 -40,说明你的 SQL 没写对,或者事务没生效。此时不要慌,检查 UPDATE 语句是否包含了 stock 0 的条件。
优化扩展
基础版跑通了,但离生产还有距离。以下是三个进阶方向:
1. 引入 Redis 缓存库存
当 QPS 达到几千时,MySQL 的行锁会成为瓶颈。方案:将库存预热到 Redis。
流程:请求先到 Redis 扣减,成功再异步写 MySQL。
风险:Redis 和 MySQL 数据不一致。需要监听 Binlog 或使用 Canal 进行最终一致性补偿。2. 状态机模式
目前的 setStatus(0) 是硬编码。如果状态多了(待支付、已支付、已发货、已完成、已退款),if-else 会爆炸。方案:使用 Spring Statemachine 或自定义状态机类。
好处:状态转换规则集中管理,非法转换(如从“已取消”直接到“已发货”)会被自动拦截。3. 异步化非核心逻辑
创建订单后,发短信、积分、日志记录等操作不要同步执行。方案:发送 MQ 消息(RabbitMQ/Kafka)。
价值:降低接口响应时间,解耦业务逻辑。小结
从照烧鸡肉饭的订单系统入手,我们不仅仅是在写一个买饭的功能,更是在实践分布式系统中的核心难题:并发控制、数据一致性和系统扩展性。
记住,面试被问原理答不上来,往往是因为只背了结论,没跑过代码。当你亲手调试过 version 字段在并发下的变化,当你在 JMeter 压力下看到数据库的 CPU 飙升,你才能真正理解什么是“乐观锁”,什么是“事务边界”。
技术没有银弹,但有底线。这个底线就是:你的代码在并发下是否依然正确?
还有什么不懂的?评论区留言挨个回。
企业数字化 ERP 产品动态
相关推荐
图解天津落户流程:版本升级API全变后的避坑指南 图解天津落户流程:版本升级API全变后的避坑指南 刚拿到天津户口指标的朋友,是不是瞬间感觉“版本升级后 API 全变了”?昨天还查得通的旧政策,今天一提交系统直接报错;原本以为简单的学历落户,现在却卡在社保月份的计算逻辑里。这种割裂感,就像… · 2026/9/21 23:53:03
5个实操细节,一文搞懂公司运营管理方案代码逻辑 5个实操细节,一文搞懂公司运营管理方案代码逻辑 复制来的代码跑不通,报错信息像天书,你盯着屏幕发呆,心里直骂街:这谁写的烂代码?别急,这种痛苦我太懂了。很多中小施工企业的负责人,拿到一份《公司运营管理方案》的电子档,里面夹杂着Python脚… · 2026/9/21 23:53:03
3天搞定迷宫式油封源码解析,告别配置卡壳 3天搞定迷宫式油封源码解析,告别配置卡壳 配置环境就卡半天,这大概是很多转行做机械密封或者流体仿真工程师的噩梦。你以为迷宫式油封只是画个图、算个尺寸?错。当你打开那些复杂的 CFD… · 2026/9/21 23:52:18
供配电系统毕业设计全流程:从负荷计算到设备选型的工程逻辑 简介:一份面向电气工程专业毕业设计或课程设计的完整文档,内容针对某机械厂供配电系统从负荷测算、无功补偿到主变压器选型与主接线方案设计展开,同时覆盖短路电流计算和高低压电气设备选择校验,适合需要参考毕业设计框架与计算过… · 2026/9/23 14:31:01
UHFReader09 C# DEMO 实战:串口/TCP 盘存与避坑指南 简介:这是一份面向C#开发者与RFID入门者的UHF RFID阅读器演示工程,围绕UHFReader09设备型号展开,帮助读者理解如何用C#与超高频阅读器通信、控制参数并处理标签数据,可应用于仓储管理、物流追踪、资产盘点等长距离识别场景。压缩包… · 2026/9/23 14:30:54
柳青丈夫面试必问避坑指南 3天搞定环境配置 柳青丈夫面试必问避坑指南 3天搞定环境配置 配置环境就卡半天,这大概是每个程序员入行时最痛的记忆。你盯着黑底白字的终端窗口,报错信息滚得飞快,脑子里全是“我到底哪步错了”。更扎心的是,当你终于跑通Hello… · 2026/9/23 14:30:54
Prisma API 详解:基于数据模型自动生成的 GraphQL 接口与 Playground 探索指南 后端数据库GraphQL 【免费下载链接】prisma1 💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated] 项目地址: https://gitcode.com/gh_mirrors/pr/prisma1 点击查看 免费下载 本篇指南聚焦 Prisma&… · 2026/9/23 14:30:48
Nginx UI 集成 Casdoor:OAuth 2.0 统一身份认证接入指南 Nginx UI 集成 Casdoor:OAuth 2.0 统一身份认证接入指南 【免费下载链接】nginx-ui Yet another WebUI for Nginx 项目地址: https://gitcode.com/gh_mirrors/ngi/nginx-ui
本篇技术指南围绕 Nginx UI 的 Casdoor 认证提供方配置展开,完整讲解 En… · 2026/9/23 14:30:47
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29