【问题标题】: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

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

【问题讨论】:

    标签: c# sql-server asp.net-core


    【解决方案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 的情况下提交表单)。我没有使用过异步,但我尝试让应用程序代码处理所有数据库内部事务逻辑的经验远非一流。
    猜你喜欢
    • 2021-03-06
    • 2021-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多