号源库存扣减不能用无条件UPDATE因并发导致超卖,须用带stock>0条件的原子更新并检查ROW_COUNT();索引需包含主键和(doctor_id,work_date,time_slot)联合唯一索引以精准行锁;挂号宜用带条件UPDATE+零重试的乐观控制而非悲观锁;号段越细并发越好,时间字段应避免模糊字符串而用TIME或时间戳。

号源库存扣减为什么不能用 UPDATE ... SET stock = stock - 1
因为并发请求下,多个事务读到同一初始 stock 值,各自减 1 后写回,导致超卖。比如号源剩 1 个,两个用户同时提交,最终变成 -1。
必须让扣减动作具备原子性 + 检查条件。正确做法是把「检查余量」和「扣减」合并成一条语句:
UPDATE schedule_slot SET stock = stock - 1 WHERE id = 123 AND stock > 0;
执行后检查 ROW_COUNT():
- 返回 1 → 扣减成功,可继续生成挂号单
- 返回 0 → 库存不足或已被抢光,拒绝该请求
MySQL 行锁在挂号场景下到底锁哪一行
只锁 WHERE 条件命中且能走索引的行。如果 id 是主键,UPDATE ... WHERE id = ? 就只锁那一行;但如果写成 WHERE doctor_id = ? AND date = ? 却没给这两个字段建联合索引,就会触发表锁或间隙锁,拖慢整个号段的并发能力。
挂号系统最关键的索引组合通常是:
-
PRIMARY KEY (id)(必须) -
UNIQUE KEY (doctor_id, work_date, time_slot)(确保号源唯一性,也支撑查询+扣减)
漏掉这个联合索引,SELECT ... FOR UPDATE 或带条件的 UPDATE 很可能锁住不该锁的范围,引发排队阻塞。
乐观锁 vs 悲观锁:挂号系统该选哪个
别硬套概念——挂号是短事务、高竞争、低冲突率(同一号段抢的人再多,成功者也就几个),用悲观锁(SELECT ... FOR UPDATE)反而容易卡住后续请求。
更稳妥的是「带条件 UPDATE + 重试」的乐观控制:
- 不提前加锁,直接执行带
stock > 0条件的UPDATE - 失败时立即返回「号源已约满」,不重试(避免雪崩)
- 若业务允许极短延迟,可加最多 1 次重试(比如等 50ms 后再查一次
stock)
注意:不要在应用层做「先 SELECT 再 UPDATE」,这中间存在竞态窗口,不管有没有锁都不可靠。
时间精度与号段设计怎么影响并发表现
号源粒度越细,并发压力越分散。把一天切成 30 分钟一个号段,比按半天切,锁冲突概率下降数倍。
但要注意 MySQL 的 DATETIME 默认只到秒级,如果医生同一时段开放多个号源(如上午号、下午号、特需号),仅靠 work_date + time_slot 字符串可能无法精确区分。建议:
- 用
TIME类型存开始时间(如08:00:00),配合duration字段(单位分钟) - 或直接用时间戳整数(如
start_time存 202405200800 表示 2024-05-20 08:00) - 避免用模糊字符串如
"上午"做查询条件,没法走索引
真正难的不是扣减逻辑,而是号段划分是否和医生排班、患者预约习惯、前端展示粒度对齐——错一层,后面所有并发优化都打折扣。










