首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >门店预约高峰超卖怎么办?时段库存与并发锁的三种方案

门店预约高峰超卖怎么办?时段库存与并发锁的三种方案

原创
作者头像
数字化落地笔记
发布于 2026-10-28 09:40:10
发布于 2026-10-28 09:40:10
850
举报

门店预约高峰超卖怎么办?时段库存与并发锁的三种方案

先说结论:门店预约超卖,根因是"余量扣减"没有并发保护——两个请求同时读到"还有1个名额",都扣成功,就超卖了。解法是在数据库层把"判断余量"和"扣减"合成一条原子操作,配合唯一约束拦重复预约。本文从一个 12 工位养生馆的真实超卖事故讲起,对比三种并发方案的取舍,附可落地代码。

背景交代下环境:给连锁养生馆做预约系统改造,底座用的是某一站式SaaS门店系统,时段设置、预约人数限制、预约数据入库这些基础能力它都承载;但"余量扣减的并发一致性"这种跟业务强耦合的规则,通用能力覆盖不到,在业务层自己实现。事故就发生在我们自研的扣减逻辑上,与底座本身无关。

事故还原:高峰期一单超卖了9个

改造上线第二周的周六,预约高峰期,后台出现异常:某技师 14:00-15:00 时段容量 5 人,实际预约 9 单,前台还在喊"约不上"。查日志发现根因:扣减逻辑是"先 SELECT 余量,再 UPDATE 扣减",两个请求并发时都读到 booked=4,判断"4<5 还有名额",同时扣成 5,超卖。9 单就是这么叠出来的。

方案一:乐观锁(推荐,无锁表、吞吐高)

把"余量判断"和"扣减"合并成一条带条件的 UPDATE:

代码语言:sql
复制
UPDATE slot_capacity
SET booked = booked + 1, version = version + 1
WHERE id = ? AND booked < capacity AND version = ?;
-- 影响行数 = 1 扣减成功;= 0 说明已满或版本冲突,返回"该时段已满"
  • 优点:不需要锁表,高峰期吞吐不受影响;实现简单,一行 SQL;
  • 缺点:冲突时靠重试,极端并发下可能"抢不到"(但我们业务场景足够);
  • 适用:绝大多数预约场景,首选。

方案二:数据库行锁(SELECT ... FOR UPDATE)

显式锁住时段容量行,扣完再释放:

代码语言:sql
复制
BEGIN;
SELECT booked, capacity FROM slot_capacity WHERE id = ? FOR UPDATE;
-- 业务判断 booked < capacity
UPDATE slot_capacity SET booked = booked + 1 WHERE id = ?;
COMMIT;
  • 优点:判断与扣减之间绝对一致,无重试;
  • 缺点:锁行会阻塞其他预约请求,高峰期吞吐下降;锁持有期间断网/异常易卡事务;
  • 适用:超卖代价极高、且并发量可控的场景(如抢购类)。

方案三:Redis 原子扣减(适合前置校验)

用 Redis INCR 做前置容量校验,扣减成功后异步落库:

代码语言:python
复制
import redis
r = redis.Redis(host='...', decode_responses=True)
# 时段余量初始化为 capacity,预约时原子扣减
if r.decr('slot:1024:1400', 1) < 0:
    r.incr('slot:1024:1400', 1)   # 扣负了,回补并拒绝
    return '该时段已满'
# 扣减成功,异步写预约单
  • 优点:毫秒级,抗极高并发;库存热点在 Redis 单键上可压到每秒万级;
  • 缺点:多一层中间件;Redis 与数据库一致性要靠补偿(扣了 Redis 没落库要回补);
  • 适用:大促、秒杀式预约高峰。

三种方案怎么选(决策表)

  • 日常预约(并发不高):方案一乐观锁,简单可靠;
  • 超卖损失严重(医疗、稀缺技师):方案二行锁,宁可慢不可错;
  • 秒杀式高峰(团购引流、节假日):方案一+方案三,Redis 前置拦流量、数据库乐观锁兜底。

我们的最终落地:高峰期 Redis 前置校验 + 数据库乐观锁兜底 + 唯一约束防重复,三层叠起来,超卖率从 9.6% 降到 0,预约高峰期下单耗时稳定在 300ms 以内,线上再没出现过"约爆了"的客诉。

上线前的并发验证清单(5分钟扫完)

改完扣减逻辑,发布前按这个清单验证一遍,能拦住 90% 的线上超卖:

  • 用两个终端同时提交最后一个名额,验证只有一个成功(乐观锁生效);
  • 同一用户连点两次提交,验证只产生一条预约(唯一约束生效);
  • 把容量约满后第 N+1 个请求,验证返回"该时段已满";
  • 取消一单,验证余量 +1(事务内回补);
  • 若用了 Redis:模拟 Redis 扣成功但落库失败,验证对账任务能把余量补回。

踩坑清单(4条真实踩过)

  • 坑1:先查后写不防并发。SELECT 再 UPDATE,两个请求读同一个余量都通过。必须带条件原子扣减;
  • 坑2:只有乐观锁没有唯一约束。同一用户连点两次,锁拦了超卖但拦不了重复预约。用户+时段建唯一索引;
  • 坑3:取消回补不在事务里。订单取消了余量没回,出现"假满"。回补和取消同一事务;
  • 坑4:Redis 扣了库没落。Redis 扣成功、异步写库失败,余量蒸发。异步任务要补偿对账,定期把 Redis 余量和库对齐。

FAQ

Q1:乐观锁影响行数为0就是满了吗?

答:不一定,也可能是 version 版本冲突(被其他请求先扣了)。两种情况都视为"本次扣减失败",业务上统一返回"该时段已满"或让用户重试。

Q2:为什么取消也要防并发?

答:取消回补 booked-1 若不带条件,可能把余量扣成负数。回补 SQL 要带 booked > 0 条件。

Q3:需要为预约做 Redis 吗?

答:日常预约不需要,乐观锁够用;只有团购引流、节假日这种瞬时高峰才值得加 Redis 前置层。

Q4:超卖发现后怎么补救?

答:已超卖订单按"预约时间先后"保留、多出的联系顾客改约或退单,并立即上线并发保护。补救是止血,防超卖才是根治。

Q5:时段容量表和排班表为什么要分开?

答:容量表管"这个时段能接几个人",排班表管"谁在哪个时段上班",职责不同、变更频率也不同(技师请假只改排班,不碰容量)。混在一张表里,一次请假改错容量,就会引发"约到不存在的人"。

结语

门店预约超卖的根子,是余量扣减没有并发保护。把"判断+扣减"合成一条原子 SQL(乐观锁),用唯一约束拦重复,取消和爽约都及时回补——三层做到,超卖率归零、约不上投诉也归零。这套方案不挑行业,美业、汽修、口腔、健身、教培试听,凡是"时段+名额"的场景都能直接套用;上线前把验证清单过一遍,别等约爆了才想起来补。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 门店预约高峰超卖怎么办?时段库存与并发锁的三种方案
    • 事故还原:高峰期一单超卖了9个
    • 方案一:乐观锁(推荐,无锁表、吞吐高)
    • 方案二:数据库行锁(SELECT ... FOR UPDATE)
    • 方案三:Redis 原子扣减(适合前置校验)
    • 三种方案怎么选(决策表)
    • 上线前的并发验证清单(5分钟扫完)
    • 踩坑清单(4条真实踩过)
    • FAQ
    • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档