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

SQL Server并发API调用数据更新异常的解决方案咨询

解决SQL Server API重复调用导致重复新增的问题

你有一个用于SQL Server表增改的API,逻辑规则为:当请求负载中的ID>0时,根据该ID更新对应行;当ID=0时,新增一行并返回新生成的ID。对应的SQL代码片段如下(已修正原代码中UPDATE语句的语法错误):

BEGIN TRANSACTION; 

SET @FETCHEDID = (SELECT ID FROM dbo.SAMPLETABLE WHERE ID = @ID)
    
IF @FETCHEDID > 0
BEGIN
    UPDATE dbo.SAMPLETABLE
    SET [AMOUNT] = @AMOUNT,
        [DATECHANGED] = @DATEADDED,
        [LASTCHANGEDBYID] = @ADDEDBYID
    WHERE [ID] = @ID;
END
ELSE
BEGIN
    INSERT INTO dbo.SAMPLETABLE ([AMOUNT], [DATEADDED],[LASTCHANGEDBYID], [DATECHANGED])
    VALUES (@AMOUNT, @DATEADDED, @ADDEDBYID, @DATEADDED);
END

COMMIT TRANSACTION;

当UI连续快速发起两次ID=0的重复调用时,会出现数据库新增两行的问题——第二个调用在第一个调用事务提交前读取到过期数据,导致重复执行新增逻辑。以下是几种后端层面的解决方案:


方案1:基于业务唯一标识的数据库锁定

核心思路是通过业务层面的唯一字段(比如用户ID、订单编号等,需根据你的实际业务场景确定),结合事务锁阻止并发新增。当第一个请求执行时,锁定该唯一标识对应的行范围,第二个请求会等待第一个事务完成后再判断是否需要执行更新。

修改后的SQL示例(假设业务唯一键为USERID):

BEGIN TRANSACTION;

DECLARE @EXISTS BIT = 0;
-- 用UPDLOCK+HOLDLOCK锁定对应业务标识的范围,防止并发新增
SELECT @EXISTS = 1 
FROM dbo.SAMPLETABLE WITH (UPDLOCK, HOLDLOCK)
WHERE USERID = @USERID;

IF @ID > 0
BEGIN
    UPDATE dbo.SAMPLETABLE
    SET [AMOUNT] = @AMOUNT,
        [DATECHANGED] = @DATEADDED,
        [LASTCHANGEDBYID] = @ADDEDBYID
    WHERE [ID] = @ID;
END
ELSE
BEGIN
    IF @EXISTS = 1
    BEGIN
        -- 已存在对应业务行,执行更新
        UPDATE dbo.SAMPLETABLE
        SET [AMOUNT] = @AMOUNT,
            [DATECHANGED] = @DATEADDED,
            [LASTCHANGEDBYID] = @ADDEDBYID
        WHERE USERID = @USERID;
        -- 返回已存在的ID
        SELECT ID FROM dbo.SAMPLETABLE WHERE USERID = @USERID;
    END
    ELSE
    BEGIN
        -- 无对应行,执行新增
        INSERT INTO dbo.SAMPLETABLE ([AMOUNT], [DATEADDED],[LASTCHANGEDBYID], [DATECHANGED], USERID)
        VALUES (@AMOUNT, @DATEADDED, @ADDEDBYID, @DATEADDED, @USERID);
        -- 返回新生成的ID
        SELECT SCOPE_IDENTITY() AS ID;
    END
END

COMMIT TRANSACTION;

说明

UPDLOCK+HOLDLOCK会在查询时添加更新锁,并将锁保持到事务结束,确保并发请求必须等待第一个事务提交后才能进行判断,从根源上避免重复新增。


方案2:使用MERGE语句统一增改逻辑

MERGE语句可以合并新增和更新的逻辑,结合锁定提示同样能解决并发问题,代码更简洁。

示例SQL:

BEGIN TRANSACTION;

DECLARE @NEWID INT;

MERGE dbo.SAMPLETABLE WITH (HOLDLOCK) AS T
USING (
    SELECT 
        CASE WHEN @ID > 0 THEN @ID ELSE NULL END AS ID,
        @AMOUNT AS AMOUNT,
        @DATEADDED AS DATEADDED,
        @ADDEDBYID AS ADDEDBYID
) AS S
ON (T.ID = S.ID OR (S.ID IS NULL AND T.USERID = @USERID)) -- 新增时用业务唯一键匹配
WHEN MATCHED THEN
    UPDATE SET 
        T.AMOUNT = S.AMOUNT,
        T.DATECHANGED = S.DATEADDED,
        T.LASTCHANGEDBYID = S.ADDEDBYID
WHEN NOT MATCHED THEN
    INSERT ([AMOUNT], [DATEADDED],[LASTCHANGEDBYID], [DATECHANGED], USERID)
    VALUES (S.AMOUNT, S.DATEADDED, S.ADDEDBYID, S.DATEADDED, @USERID)
OUTPUT INSERTED.ID INTO @NEWID;

SELECT @NEWID AS ID;

COMMIT TRANSACTION;

说明

HOLDLOCK会锁定目标表的相关范围,确保MERGE操作的原子性,并发请求会等待当前事务完成后再执行,避免重复插入。


方案3:API层添加分布式锁

如果数据库层面的锁定不适合你的业务场景,可以在API层面对同一个业务标识(比如用户ID+请求的业务唯一标识)添加分布式锁(例如基于Redis的SETNX命令)。只有获取到锁的请求才能执行数据库操作,其他请求等待锁释放后再处理,确保第二个调用能读取到第一个调用新增的行,进而执行更新逻辑。


内容的提问来源于stack exchange,提问作者Madhur Maurya

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 06:05:26