患者叫号队列系统恢复挂起令牌的序列重复问题求助
嘿,这个并发恢复令牌导致Sequence重复的问题我太熟了——本质就是竞态条件在搞鬼!当两个恢复请求几乎同时跑的时候,它们都会先读取当前Serving令牌的Sequence(比如你的例子里是4),然后都算出4+3=7作为目标位置,最后写入时就撞出重复值了。咱们来一步步解决这个问题:
核心问题拆解
问题出在你的TokenResumeProc里,查询当前Serving令牌Sequence、计算目标位置、更新这几步不是原子操作。多个请求同时执行时,会读到相同的初始值,进而生成重复的Sequence。
解决方案1:用事务+排他锁确保原子性
最直接的办法是把整个恢复逻辑包在事务里,并且锁定相关资源,让并发请求排队执行。以SQL Server为例,修改存储过程如下:
CREATE OR ALTER PROCEDURE TokenResumeProc @CouponNo VARCHAR(10) AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; -- 锁定整个令牌表,直到事务结束,彻底避免并发读取冲突 SELECT * FROM tblTokenQueue WITH (TABLOCKX, HOLDLOCK); DECLARE @TargetSequence INT; DECLARE @CurrentServingSeq INT; -- 获取当前正在服务的令牌Sequence SELECT @CurrentServingSeq = Sequence FROM tblTokenQueue WHERE Status = 'Serving'; IF @CurrentServingSeq IS NOT NULL BEGIN -- 计算理论目标位置:当前服务令牌后第3位 SET @TargetSequence = @CurrentServingSeq + 3; -- 检查剩余令牌数量,如果不足3个,直接放在队列最后 DECLARE @MaxSeq INT; SELECT @MaxSeq = MAX(Sequence) FROM tblTokenQueue; IF (@MaxSeq - @CurrentServingSeq) < 3 BEGIN SET @TargetSequence = @MaxSeq + 1; END ELSE BEGIN -- 把目标位置及之后的令牌Sequence后移一位,腾出位置 UPDATE tblTokenQueue SET Sequence = Sequence + 1 WHERE Sequence >= @TargetSequence; END END ELSE BEGIN -- 没有服务中的令牌,找已完成令牌的最大Sequence,放在其后 DECLARE @MaxCompletedSeq INT; SELECT @MaxCompletedSeq = MAX(Sequence) FROM tblTokenQueue WHERE Status = 'Completed'; SET @TargetSequence = ISNULL(@MaxCompletedSeq, 0) + 1; END -- 更新挂起令牌的状态和Sequence UPDATE tblTokenQueue SET Status = 'Pending', Sequence = @TargetSequence WHERE CouponNo = @CouponNo AND Status = 'Hold'; -- 假设挂起状态为'Hold',可根据实际调整 COMMIT TRANSACTION; END
这里的TABLOCKX会在事务期间锁定整个表,确保同一时间只有一个恢复请求能执行完整逻辑,从根源上杜绝竞态条件。
解决方案2:用行级锁优化性能(如果表数据量大)
如果令牌表数据量很大,全表锁会影响性能,可以改用行级锁锁定Serving状态的行,同时锁定最大Sequence的行,缩小锁范围:
CREATE OR ALTER PROCEDURE TokenResumeProc @CouponNo VARCHAR(10) AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; DECLARE @TargetSequence INT; DECLARE @CurrentServingSeq INT; DECLARE @MaxSeq INT; -- 锁定当前Serving的行,防止并发读取 SELECT @CurrentServingSeq = Sequence FROM tblTokenQueue WITH (UPDLOCK, HOLDLOCK) WHERE Status = 'Serving'; -- 锁定最大Sequence的行,防止并发修改 SELECT @MaxSeq = MAX(Sequence) FROM tblTokenQueue WITH (UPDLOCK, HOLDLOCK); IF @CurrentServingSeq IS NOT NULL BEGIN SET @TargetSequence = @CurrentServingSeq + 3; -- 剩余令牌不足3个,放在最后 IF (@MaxSeq - @CurrentServingSeq) < 3 BEGIN SET @TargetSequence = @MaxSeq + 1; END ELSE BEGIN UPDATE tblTokenQueue SET Sequence = Sequence + 1 WHERE Sequence >= @TargetSequence; END END ELSE BEGIN -- 无服务令牌,基于已完成令牌或初始位置设置 DECLARE @MaxCompletedSeq INT; SELECT @MaxCompletedSeq = MAX(Sequence) FROM tblTokenQueue WHERE Status = 'Completed'; SET @TargetSequence = ISNULL(@MaxCompletedSeq, 0) + 1; END UPDATE tblTokenQueue SET Status = 'Pending', Sequence = @TargetSequence WHERE CouponNo = @CouponNo AND Status = 'Hold'; COMMIT TRANSACTION; END
这个方案只锁定必要的行,比全表锁的性能更好,同时也能保证原子性。
测试建议
修改完存储过程后,可以做以下测试验证:
- 同时调用两次
TokenResumeProc恢复不同的挂起令牌,检查Sequence是否唯一 - 测试无Serving令牌的场景,恢复挂起令牌是否正确放在已完成队列之后
- 测试剩余令牌不足3个的场景,恢复的令牌是否放在队列最后
内容的提问来源于stack exchange,提问作者Surensiveaya
相关产品推荐
相关产品推荐

