c# - 防止同时选择相同的下一个计算值

标签 c# sql sql-server-2008-r2

我的数据库中有一个名为 SerialNumber 的表,如下所示:

[Id] [bigint] IDENTITY(1,1) NOT NULL,
[WorkOrderId] [int] NOT NULL,
[SerialValue] [bigint] NOT NULL,
[SerialStatusId] [int] NOT NULL

在我的应用程序中,我始终需要获取给定工单的 MIN SerialValue,其中 SerialStatusId 为 1 (工单 ID)。
即,如果客户端选择了某个序列号,则应立即更改为 SerialStatusId =2,以便下一个客户端不会选择相同的值。
该应用程序安装在多个站点上,可以同时访问数据库。
我尝试了几种方法来解决这个问题,但没有成功:

update SerialNumber
set SerialStatusId = 2
output inserted.SerialValue
where WorkOrderId = @wid AND 
SerialValue = (select MIN(SerialValue) FROM SerialNumber where SerialStatusId = 1)

并且

declare @val bigint;
set @val = (select MIN(SerialNumber.SerialValue) from SerialNumber where SerialStatusId = 1
and WorkOrderId = @wid);

select SerialNumber.SerialValue from SerialNumber where SerialValue = @val;
update SerialNumber set SerialStatusId = 2 where SerialValue = @val;

我测试不选择相同值的方法是:

    public void Test_GetNext()
    {
        Task t1 = Task.Factory.StartNew(() => { getSerial(); });
        Task t2 = Task.Factory.StartNew(() => { getSerial(); });
        Task t3 = Task.Factory.StartNew(() => { getSerial(); });
        Task t4 = Task.Factory.StartNew(() => { getSerial(); });
        Task t5 = Task.Factory.StartNew(() => { getSerial(); });

        Task.WaitAll(t1, t2, t3, t4, t5);


        var duplicateKeys = serials.GroupBy(x => x)
                    .Where(group => group.Count() > 1)
                    .Select(group => group.Key).ToList();
    }

    private List<long> serials = new List<long>();
    private void getSerial()
    {
        object locker = new object();
        SerialNumberRepository repo = new SerialNumberRepository();
        while (serials.Count < 200)
        {
            lock (locker)
            {
                serials.Add(repo.GetNextSerialWithStatusNone(workOrderId));
            }
        }
    }

到目前为止,我所做的所有测试都从包含 200 个序列的表中给出了大约 70 个重复值。 如何改进 SQL 语句使其不会返回重复值?
更新
尝试实现 @Chris 建议的内容,但仍然得到重复的数量:

SET XACT_ABORT, NOCOUNT ON
DECLARE @starttrancount int;
BEGIN TRY
SELECT @starttrancount = @@TRANCOUNT
IF @starttrancount = 0 BEGIN TRANSACTION
update SerialNumber
set SerialStatusId = 2
output deleted.SerialValue
where WorkOrderId = 35 AND
SerialValue = (select MIN(SerialValue) FROM SerialNumber where SerialStatusId = 1)
IF @starttrancount = 0
COMMIT TRANSACTION
ELSE
EXEC sp_releaseapplock 'get_code';
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND @starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR (50000,-1,-1, 'mysp_CreateCustomer')
END CATCH

最佳答案

您收到重复项的原因是因为有 race condition读取前一个值和将新值更新到 SerialNumber 之间的时间。如果两个并发连接(例如两个不同的线程)同时读取相同的值,它们都将派生出相同的新 SerialNumber。第二个连接也会再次冗余地更新状态。

更新的解决方案 - 前一个解决方案导致死锁

要使用当前设计解决此问题,在读取 的下一个值时,您需要预先锁定表中的行以防止其他并行读取器。

CREATE PROC dbo.GetNextSerialLocked(@wid INT)
AS 
BEGIN
    BEGIN TRAN
        DECLARE @NextAvailableSerial BIGINT;
        SELECT @NextAvailableSerial = MIN(SerialValue) 
           FROM SerialNumber WITH (UPDLOCK, HOLDLOCK) where SerialStatusId = 1;

        UPDATE SerialNumber
        SET SerialStatusId = 2
        OUTPUT inserted.SerialValue
        WHERE WorkOrderId = @wid AND SerialValue = @NextAvailableSerial;
    COMMIT TRAN
END
GO

由于获取 + 更新现在分为多个语句,因此我们确实需要一个事务来控制 HOLDLOCK 的生命周期。

您还可以将事务隔离级别增加到SERIALIZABLE - 虽然这不会阻止并行读取,但它会导致第二个写入器在尝试写入表时陷入死锁,鉴于现在读取的值已更改。

您还可以考虑替代设计以避免以这种方式获取数据,例如如果序列号应该增加,请考虑使用IDENTITY

另请注意,您的 C# 代码锁定不起作用,因为每个 Task 将有一个独立的 locker 对象,并且永远不会争用同一锁。

    object locker = new object(); // <-- Needs to be shared between Tasks
    SerialNumberRepository repo = new SerialNumberRepository();
    while (serials.Count < 200)
    {
        lock (locker) <-- as is, no effect.

此外,添加连续剧时,您需要在 serials 集合周围设置内存屏障。最简单的可能只是将其更改为并发集合:

private ConcurrentBag<long> serials = new ConcurrentBag<long>();

如果您想序列化来自 C# 的访问,则需要在所有任务/线程中使用共享的静态对象实例(显然,从代码序列化需要对序列号的所有访问都来自来自同一过程)。在数据库中进行序列化更具可扩展性,因为您还可以为不同的 WorkOrderId

提供多个并发读取器

关于c# - 防止同时选择相同的下一个计算值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30597395/

相关文章:

php - MySQL 语法错误 SQL 错误 [1064] [42000] : You have an error in your SQL syntax

mysql - 由于数据类型, '' 中的未知列 'field list'

sql - 如何在同一查询的另一个字段中使用计算字段

sql - 行为不当的身份

c# - .Net Core MSTest 项目中未加载 app.config

c# - GC.Collect 循环?

c# - SQL查询结果到C#变量

c# - Sitecore 7 搜索所有内容

java - 有没有办法在 jOOQ 中检查查询绑定(bind)值?

transactions - 有没有办法检查事务授予的锁