库存扣减必须用事务包裹并加行锁,核心是update语句带stock>=1条件且字段有索引,否则并发下会超卖;单仓场景可直接在goods表加int型stock字段并建索引,多仓则需独立inventory表。
库存扣减必须用事务包裹,否则数据不一致
直接在 navicat 里建表、写 sql 不等于完成了库存扣减模型——最常被忽略的是并发场景下超卖。比如两个请求同时读到 stock = 10,各自减 1 后都写回 9,实际应为 8。这问题不会在 navicat 的可视化界面里暴露,只会在上线后爆发。
Navicat 本身不执行业务逻辑,它只是帮你建表、调试 SQL、观察数据。真正防止超卖,得靠数据库层的事务 + 行锁 + 原子更新。所以建模第一步不是画 ER 图,而是确认你的扣减语句是否满足:
-
UPDATE goods SET stock = stock - 1 WHERE id = ? AND stock >= 1—— 必须带stock >= 1条件,且该字段有索引 - 整个操作必须包裹在
BEGIN TRANSACTION/COMMIT中(Navicat 的「查询」窗口支持手动执行) - 隔离级别建议用
READ COMMITTED(MySQL 默认),避免长事务拖慢性能
商品表和库存表要不要拆开?
新手常纠结“库存该单独建表还是合并在商品表里”。答案取决于你是否需要库存变更留痕、多仓库、批次管理等扩展需求。
如果只是单仓、无批次、无历史追溯,goods 表加一个 stock 字段最简单,Navicat 里右键表 →「设计表」→ 新增字段即可。但注意:
-
stock字段类型必须是INT或BIGINT,不能用DECIMAL或FLOAT(浮点数无法精确比对,会导致扣减异常) - 务必给
id加主键,给stock加索引(Navicat「索引」标签页里添加)——否则WHERE stock >= 1会全表扫描,锁住整张表 - 如果未来要支持分仓,现在就建
inventory表:字段至少含goods_id、warehouse_id、stock,并设联合主键(goods_id, warehouse_id)
Navicat 执行扣减 SQL 时容易忽略的锁行为
你在 Navicat 查询窗口里执行一条 UPDATE,看起来只是改了一行,但实际上 MySQL 可能锁住更多行——尤其当 WHERE 条件没走索引,或用了范围查询。
验证是否锁表/锁行的方法很简单:打开两个 Navicat 查询窗口,窗口 A 执行:
START TRANSACTION; UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
先别 COMMIT;窗口 B 立即执行同样语句查同一 id,会卡住直到 A 提交或回滚。这是正常行锁。但如果 B 卡在查 id = 456(不同 ID),说明锁升级了——大概率是 stock 字段没索引,导致 MySQL 用不上主键索引,退化成间隙锁或表锁。
- 用 Navicat「工具」→「服务器监控」→「当前会话」看
STATE是否为updating或Locked - 执行
EXPLAIN SELECT * FROM goods WHERE id = 123 AND stock >= 1,确认key列显示用了哪个索引 - 避免在 WHERE 中混用函数,比如
WHERE ABS(stock) >= 1,会让索引失效
测试超卖必须模拟真实并发,不能手点两次
Navicat 自带的「查询」窗口是串行的,点十次按钮仍是顺序执行,完全测不出问题。想验证模型是否扛得住并发,得换方式:
- 用命令行工具如
mysql -e "UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock >= 1;"配合 shell 循环,起多个进程 - 更靠谱的是写个 Python 脚本,用
threading或asyncio并发调用,每轮扣减前查一次SELECT stock FROM goods WHERE id = 1记录初始值,结束后再查最终值对比 - Navicat 的「数据传输」或「备份」功能完全不适用于压力测试——它们是单线程批量操作,和业务场景无关
真正难的不是画出三张表的关系线,而是让每一行 UPDATE 在 1000 QPS 下依然返回正确结果。模型设计完,立刻用并发脚本打一遍,比任何 ER 图都管用。











