如何在更新和插入并发时避免重复行
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 中在一秒钟内发出并发请求时避免重复记录