You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

患者叫号队列系统恢复挂起令牌的序列重复问题求助

嘿,这个并发恢复令牌导致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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:09:50