如何在更新和插入并发时避免重复行

Posted

技术标签:

【中文标题】如何在更新和插入并发时避免重复行【英文标题】:How to avoid duplicate rows while do update and insert concurrency 【发布时间】:2021-12-25 11:34:58 【问题描述】:

我有这个代码:

public class MyService : IMyService

    public EarningService(IDbConnection db)
    
        Db = db;
    

    public async Task SomeMethodWithTransacion()
    
        Db.Open();
        var tran = Db.BeginTransaction();

        try
        
            string sqlInsert1 = "insert into someTable1";
            await Db.ExecuteAsync(sqlInsert1, new param1);
    
            string sqlInsert2 = "insert into someTable2";
            await Db.ExecuteAsync(sqlInsert2, new param2)
    
            var field1 = GetSomeField1(param);
       
            string updateQuery1 = @"UPDATE SOMETABLE1 SET someField = 'someValue' WHERE Id = @Id";
            Db.Execute(updateQuery1, new  Id );
    
            string sqlInsert3 = "insert into someTable3";
            await Db.ExecuteAsync(sqlInsert3, new param3);
    
            var field2 = GetField2(param);
            var field3 = GetField3(param);
            var field4 = GetField4(param);
        
            string sqlInsert4 = "insert into someTable4";
            await Db.ExecuteAsync(sqlInsert4, new param4);

            var sqlSP = "[AddAssets]";
            await Db.ExecuteAsync(sqlSP, new  @owner= @owner, @rvalue = value , tran,
                   commandType: CommandType.StoredProcedure);

            tran.Commit();
            return;
        
        catch (Exception e)
        
            tran.Rollback();
            return 500;
        
   


//startup
public void ConfigureServices(IServiceCollection services)

    string dbConnectionString = this.Configuration.GetConnectionString("Default");

    // Inject IDbConnection, with implementation from SqlConnection class.
    services.AddTransient<IDbConnection>((sp) => new SqlConnection(dbConnectionString));  
      

但是我在执行下面的代码时遇到了问题:

var sqlSP = "[AddAssets]";
                await Db.ExecuteAsync(sqlSP, new  @owner= @owner, @rvalue = value , tran,
                       commandType: CommandType.StoredProcedure);

存储过程:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[AddAssets] 
    @owner uniqueidentifier, 
    @rvalue nvarchar(MAX)
AS
BEGIN
    DECLARE @currentBal decimal = (SELECT TOP (1) [Balance] 
                                   FROM [dbo].[Assets] 
                                   WHERE [Owner] = @owner AND [Status]=1)

    IF @currentBal >= 0
    BEGIN
        DECLARE @assetTable TABLE
                            (
                                [Id] [int] NOT NULL,
                                [Owner] [uniqueidentifier] NULL,
                                [Balance] [decimal](18, 2) NULL,
                                [Status] [smallint] NULL
                            );

    INSERT INTO @assetTable 
        SELECT TOP 1 
            [Id], [Owner], [Balance], [Status] 
        FROM [dbo].[Assets] WITH (UPDLOCK) 
        WHERE [Owner] = @owner AND [Status] = 1;

    INSERT INTO [dbo].[Assets] ([Owner], [Balance], [Status])
    VALUES (@owner, @rvalue, 1)

    UPDATE [dbo].[Assets] 
    SET [Status] = 2  
    WHERE [Id] = (SELECT TOP 1 [Id] FROM @assetTable 
                  WHERE [Owner] = @owner);
END

我确实使用 jmeter 对该方法进行了负载测试,但得到了以下预期结果:

Id Owner Balance Status RV
2 3B09C822-E613-454B-A6AA-53FA68363F74 102.00 1 0x000000000007186D
12 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 1 0x000000000007186F
13 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 1 0x0000000000071871
14 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 1 0x0000000000071873
15 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 1 0x0000000000071875
16 3B09C822-E613-454B-A6AA-53FA68363F74 61.00 1 0x0000000000071877
17 3B09C822-E613-454B-A6AA-53FA68363F74 86.00 1 0x0000000000071879
18 3B09C822-E613-454B-A6AA-53FA68363F74 88.00 1 0x000000000007187B
19 3B09C822-E613-454B-A6AA-53FA68363F74 77.00 1 0x000000000007187D
20 3B09C822-E613-454B-A6AA-53FA68363F74 51.00 1 0x000000000007187F

当我预期结果是只有 1 行的状态 = 1 对于 1 个所有者:

Id Owner Balance Status RV
2 3B09C822-E613-454B-A6AA-53FA68363F74 102.00 1 0x000000000007186D
12 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 2 0x000000000007186F
13 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 2 0x0000000000071871
14 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 2 0x0000000000071873
15 3B09C822-E613-454B-A6AA-53FA68363F74 103.00 2 0x0000000000071875
16 3B09C822-E613-454B-A6AA-53FA68363F74 61.00 2 0x0000000000071877
17 3B09C822-E613-454B-A6AA-53FA68363F74 86.00 2 0x0000000000071879
18 3B09C822-E613-454B-A6AA-53FA68363F74 88.00 2 0x000000000007187B
19 3B09C822-E613-454B-A6AA-53FA68363F74 77.00 2 0x000000000007187D
20 3B09C822-E613-454B-A6AA-53FA68363F74 51.00 2 0x000000000007187F

请有人帮我解决这个问题。

【问题讨论】:

【参考方案1】:

您的代码很复杂,我尝试在另一个更简单的代码中编写相同的逻辑,并且我添加了 WITH (rowlock,UpdLock) 代码来读取更新的值并锁定行直到事务结束:

试试这个:

ALTER PROCEDURE [AddAssets] 
(
    @owner      uniqueidentifier, 
    @rvalue     nvarchar(MAX)
)
AS
BEGIN
    declare @valid_balance_id as int;
    
    -- select last added
    set @valid_balance_id = (
        SELECT      TOP (1) 
                    [Id] 
        FROM        [Assets] WITH (rowlock,UpdLock) 
        WHERE       [Owner] = @owner  
                AND 
                    [Status] = 1 
        ORDER BY    [Id] DESC
    );

    -- if no value exit
    if @valid_balance_id is null 
    BEGIN
        return -1;
    END

    -- if selected id has balance >= 0
    set @valid_balance_id = (
        SELECT      TOP (1) 
                    [Id] 
        FROM        [Assets] WITH (rowlock,UpdLock) 
        WHERE       [Balance] >= 0 
                AND 
                    [Id] = @valid_balance_id 
    );

    -- if row has no balance or negative balance then exit
    if @valid_balance_id is null 
    BEGIN
        return -1;
    END

    -- set status = 2 for the valid value
    update  TOP (1)
            [Assets] 
    SET     [Status] = 2 
    WHERE   [Id] = @valid_balance_id;

    -- insert new value
    INSERT INTO [Assets]
    (
        [Owner], [Balance], [Status]
    )
    VALUES
    (
        @owner, @rvalue, 1
    );
END

【讨论】:

这是一个糟糕的答案,因为它没有解释你在做什么不同或为什么它起作用。如果它有效,那很好,但你不能从这个答案中真正学到任何东西。 已更新,@user1666620 谢谢 till the end of the transaction -> 直到 what 交易结束?您不应该添加交易吗?存储过程不是事务。 c#代码中的transaction调用storeprocedure 嗯,我仍然会在过程中这样做,因为 (a) C# 代码可能会更改,并且 (b) 有人可能会调用过程不是从 C#。对我来说,这有点像 only 在网页上而不是在数据库中验证电子邮件格式,假设没有人会尝试从网页以外的任何地方插入数据(或设法在禁用 javascript 的情况下提交表单)。我没有使用过异步,但我尝试让应用程序代码处理所有数据库内部事务逻辑的经验远非一流。

以上是关于如何在更新和插入并发时避免重复行的主要内容,如果未能解决你的问题,请参考以下文章

从 tableView 上的核心数据填充数据时如何避免行重复

如何在 Laravel 中在一秒钟内发出并发请求时避免重复记录

哪种 SQL 模式可以更快地避免插入重复行?

Kafka Bigquery中的重复行

如果插入语句给出重复键异常(在表中找到行 id=1)如何更新 JDBC(Postgresql)中的语句

一行列表视图正在更新,但也插入了重复行