存储过程适合封装必须原子执行、强依赖数据库状态且跨表操作频繁的订单库存逻辑,如扣减库存+创建订单+更新销量;发券、推消息等异步操作应由应用层或消息队列处理。

存储过程适合封装哪些订单库存逻辑
不是所有订单步骤都该塞进存储过程。真正值得封装的是那些必须原子执行、强依赖数据库状态、且跨表操作频繁的环节,比如:扣减库存 + 创建订单主表记录 + 更新商品销量字段。发券、推消息、更新搜索索引这些异步动作,别放进去——它们失败不该回滚整个事务,也不该拖慢存储过程响应。
常见错误是把“查用户余额→扣积分→写积分流水→发通知”全塞进一个存储过程。结果通知服务暂时不可用,整笔订单卡住或失败。这类外围逻辑应由应用层或消息队列驱动。
-
SELECT ... FOR UPDATE必须在存储过程内完成,不能拆到应用层再传参数进来 - 库存校验(
WHERE stock >= num)和UPDATE必须在同一语句或同一事务块中,避免中间被其他事务修改 - 订单号生成若依赖数据库序列(如
LAST_INSERT_ID()),可放在存储过程里,但不推荐用 UUID 或雪花 ID —— 那些更适合应用层生成
怎么写一个安全的库存扣减+订单创建存储过程
核心原则:用 UPDATE ... WHERE 做原子校验,比先 SELECT 再 UPDATE 更可靠,也省去显式加锁开销。下面是一个生产可用的简化骨架:
DELIMITER $$
CREATE PROCEDURE create_order_with_stock_check(
IN p_sku_id BIGINT,
IN p_num INT,
IN p_user_id BIGINT,
OUT p_order_id BIGINT,
OUT p_result_code INT
)
BEGIN
DECLARE affected_rows INT DEFAULT 0;
DECLARE current_stock INT DEFAULT 0;
<pre class="brush:php;toolbar:false;">START TRANSACTION;
-- 原子扣减库存:只影响1行才算成功
UPDATE inventory
SET stock = stock - p_num, updated_at = NOW()
WHERE sku_id = p_sku_id AND stock >= p_num;
GET DIAGNOSTICS affected_rows = ROW_COUNT();
IF affected_rows = 0 THEN
SET p_result_code = -1; -- 库存不足
ROLLBACK;
LEAVE proc_label;
END IF;
-- 创建订单主表(假设 orders 表有 auto_increment id)
INSERT INTO orders (user_id, sku_id, quantity, status)
VALUES (p_user_id, p_sku_id, p_num, 'created');
SET p_order_id = LAST_INSERT_ID();
SET p_result_code = 0;
COMMIT;END$$ DELIMITER ;
注意:GET DIAGNOSTICS ... ROW_COUNT() 是关键,不能依赖 SELECT 查完再判断——那已经不是原子的了。另外,stock 字段必须是 UNSIGNED,否则 stock - p_num 可能变成负数而不报错。
为什么别在存储过程里做分布式锁或 Redis 操作
MySQL 存储过程无法调用外部服务(如 Redis),也不能发起 HTTP 请求。试图用 sys_eval() 或 UDF 扩展来调 Redis,不仅违反数据库职责边界,还会导致事务不可控、超时难管理、监控失真。
更现实的问题是:一旦你在存储过程里尝试模拟分布式锁(比如插入一张 lock_table),就等于把锁粒度从“单行”扩大到“全表”,极易引发死锁或阻塞。而真正的分布式锁(如 RedLock)必须由应用层协调,数据库只负责最终落地。
- 存储过程只管“数据落库”这一件事,锁由应用层用
Redis SETNX或Zookeeper控制 - 预占库存(
lock_stock)可以存在表里,但增减lock_stock的逻辑应在应用层完成,存储过程只做最终stock -= num - 如果业务要求“下单即锁库存”,那就用应用层先写
lock_stock,再调这个存储过程;不要试图在存储过程里再查一遍lock_stock并做判断——它会破坏事务简洁性
容易被忽略的事务与性能陷阱
存储过程本身不解决并发问题,它只是把 SQL 封装得更紧凑。真正决定是否超卖的,是事务隔离级别、索引设计和 SQL 写法。
最常踩的坑是:没给 sku_id 加唯一索引,导致 UPDATE ... WHERE sku_id = ? 走全表扫描,进而升级为表级锁。哪怕你写了 FOR UPDATE,没索引照样锁全表,QPS 直接掉 90%。
-
inventory.sku_id必须有UNIQUE INDEX或至少INDEX,否则WHERE sku_id = ?无法命中行锁 - 事务默认隔离级别用
REPEATABLE READ(MySQL 默认),别改成READ COMMITTED——后者在某些场景下会让SELECT ... FOR UPDATE锁不住幻读插入的行 - 存储过程里别做耗时计算(如循环生成订单明细)、别嵌套调用其他存储过程超过 3 层——栈深度和锁持有时间都会失控
复杂点在于:库存扣减只是订单链路的起点,后续支付、履约、退款都会反向影响它。存储过程再精巧,也无法替代对 lock_stock、version、updated_at 等字段的协同设计和应用层状态机管控。











