← ClaudeAtlas

concurrency-selllisted

Use when deducting stock, quotas, tickets, 名额, or commission-related remaining amounts under concurrent requests; oversell, 超卖, 扣库存, 核销占名额. Do not use for ordinary CRUD that does not mutate a remaining counter.
1398281322-a11y/java-backend-guardrails · ★ 2 · AI & Automation · score 73
Install: claude install-skill 1398281322-a11y/java-backend-guardrails
# 并发超卖 / 扣库存 ## When to Invoke 扣库存、扣名额、余票、卡券可核销次数、分佣池剩余、秒杀预扣。 ## When NOT 不改「剩余数量」的读写;纯展示库存(可缓存,不在这里做扣减)。 ## 风险(面试考点) `stock = stock - 1` 无条件更新 → 超卖。读库存再写回 → 丢失更新。只加 Redis 锁但 DB 无条件更新 → 锁失效仍超卖。面试常把「分布式锁」当成超卖银弹,这是错的。 ## 适用场景 - 下单扣 SKU 库存 - 核销占用「可核销次数 / 场次名额」 - 分佣从可分配余额划出 ## 方案选型(轻量优先) | 优先级 | 方案 | 用在 | 代价 | |--------|------|------|------| | 1 | `UPDATE ... SET stock=stock-n WHERE stock>=n` | 中低并发,默认 | 一行热点时吞吐有上限 | | 2 | 唯一键防同一用户重复下单/核销 | 一人一单、一券一核 | 要有业务唯一键 | | 3 | Redis Lua 预扣 + MQ 异步落单 | 高 QPS 秒杀 | 缓存与 DB 要对账 | | 4 | 队列化(单 SKU 单消费者) | 热点 SKU | 延迟上升 | 默���:条件更新 + 唯一键。不要用 Redisson 锁当主防超卖。锁可以串行化,但库存正确性必须写在 `WHERE stock>=n` 或 Lua 原子扣减里。 需要乐观锁版本号时加载 `optimistic-lock`。不要把「锁」和「库存条件更新」混成一件事。 ## 默认方案 ```sql UPDATE sku_stock SET stock = stock - #{n}, updated_at = NOW() WHERE sku_id = #{skuId} AND stock >= #{n}; -- 影响行数 = 0 → 库存不足,不要再扣 ``` MyBatis-Plus: ```java int rows = stockMapper.update(null, Wrappers.<SkuStock>lambdaUpdate() .setSql("stock = stock - " + n) .eq(SkuStock::getSkuId, skuId) .ge(SkuStock::getStock, n)); if (rows == 0) { throw new BizException("库存不足"); } ``` 一人一单 / 一券一核: ```sql UNIQUE KEY uk_order_user_sku (user_id, sku_id, activity_id) UNIQUE KEY uk_verify_voucher (voucher_id) ``` 高 QPS 预扣(升级条件:单 SKU 条件更新扛不住): ```lua -- KEYS[1]=stock key, KEYS[2]=user bought key, ARGV[1]=n if redis.call('EXISTS', KEYS[2]) == 1 then return -1 end local left = tonumber(redis.call('GET', KEYS[1]) or '0') if left < tonumber(ARGV[1]) then return 0 end red