ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

3天搞定照烧鸡肉饭订单系统,图解原理避坑指南

3天搞定照烧鸡肉饭订单系统,图解原理避坑指南 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 飙升,你才能真正理解什么是“乐观锁”,什么是“事务边界”。 技术没有银弹,但有底线。这个底线就是:你的代码在并发下是否依然正确? 还有什么不懂的?评论区留言挨个回。
返回列表