多个生产者采用乐观并发处理方式插入唯一的“不可变”实体,有什么高效的方法?

9
假设有一个系统,其中有多个并发的生产者,每个生产者都试图持久化一些对象图,这些对象图包含以下唯一可识别名称的共同实体:
CREATE TABLE CommonEntityGroup(
    Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL
);
GO

CREATE UNIQUE INDEX IX_CommonEntityGroup_Name 
    ON CommonEntityGroup(Name)
GO


CREATE TABLE CommonEntity(
    Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL,
    CommonEntityGroupId INT NOT NULL,
    CONSTRAINT FK_CommonEntity_CommonEntityGroup FOREIGN KEY(CommonEntityGroupId) 
        REFERENCES CommonEntityGroup(Id)
);
GO

CREATE UNIQUE INDEX IX_CommonEntity_CommonEntityGroupId_Name 
    ON CommonEntity(CommonEntityGroupId, Name)
GO

例如,生产者A保存一些CommonEntityMeeting,而生产者B保存CommonEntitySet。他们中的任何一个都必须持久化与其特定项目相关的CommonEntity
基本上,关键点是:
- 有独立的生产者。 - 他们并行操作。 - 理论上(虽然这可能会改变,目前还不完全正确),它们将通过相同的Web服务(ASP.Net Web API)进行操作,只是具有各自的端点/“资源”。因此,理想的解决方案不应该依赖于此。 - 他们努力保留包含可能尚不存在的CommonEntity/CommonEntityGroup对象的不同对象图。 - CommonEntity/CommonEntityGroup在创建后就不可变且永远不会被修改或删除。 - 根据它们的某些属性(Name和相关的通用实体,如果有的话(例如,CommonEntityCommonEntity.Name+CommonEntityGroup.Name唯一)),CommonEntity/CommonEntityGroup是唯一的。 - 生产者不知道/不关心那些CommonEntities的ID - 他们通常只传递带有那些CommonEntitiesNames(唯一)和相关信息的DTO。因此,任何Common(Group)Entity都必须通过Name找到/创建。 - 生产者有一定的可能性同时尝试创建相同的CommonEntity/CommonEntityGroup。 - 尽管这样的CommonEntity/CommonEntityGroup对象已经存在于数据库中,但更有可能发生。
因此,在将Entity Framework(先数据库,虽然这可能并不重要)作为DAL和SQL Server作为存储时,如何有效可靠地确保所有这些生产者会同时成功持久化它们交叉的对象图?
考虑到UNIQUE INDEX已经确保不会有重复的CommonEntities(Name,GroupName对是唯一的),我可以看到以下解决方案:
- 确保在构建其他对象的图形之前找到/创建每个CommonEntity/CommonGroupEntity + SaveChanged()。 在这种情况下,当为相关实体调用SaveChanges时,由于其他制造商在刚刚创建相同的实体,所以不会出现任何索引违规。
public class CommonEntityGroupRepository // sort of
{
    public CommonEntityGroupRepository(EntitiesDbContext db) ...

    // CommonEntityRepository will use this class/method internally to create parent CommonEntityGroup.
    public CommonEntityGroup FindOrCreateAndSave(String groupName)
    {
        return
            this.TryFind(groupName) ?? // db.FirstOrDefault(...)
            this.CreateAndSave(groupName);
    }

    private CommonEntityGroup CreateAndSave(String groupName)
    {
        var group = this.Db.CommonEntityGroups.Create();
        group.Name = groupName;
        this.Db.CommonGroups.Add(group)

        try
        {
            this.Db.SaveChanges();
            return group;
        }
        catch (DbUpdateException dbExc)
        {
            // Check that it was Name Index violation (perhaps make indices IGNORE_DUP_KEY)
            return this.Find(groupName); // TryFind that throws exception.
        }
    }
}

采用此方法将对SaveChanges进行多次调用,并且每个CommonEntity都将拥有自己的一种Repository,尽管这似乎是最可靠的解决方案。 2.只需创建整个图并在索引违规时从头开始重建 有点丑陋和低效(使用10个CommonEntities可能需要重试10次),但简单而相对可靠。 3.只需创建整个图并在索引违规时替换重复项 不确定是否有一种简单可靠的方法来替换更为复杂的对象图中的重复项,虽然可以实现特定于情况和更通用的基于反射的解决方案。
仍然像前一个解决方案一样可能需要多次重试。 4.尝试将此逻辑移到数据库中(SP) 怀疑在存储过程内部处理更容易。它将是在数据库端实现的乐观或悲观方法。
虽然它可能提供更好的性能(在这种情况下不是问题),并将插入逻辑放入一个共同的位置。 5.在存储过程中使用SERIALIZABLE隔离级别 / TABLOCKX + SERIALIZABLE表提示 - 这应该绝对有效,但我更喜欢不要将表完全锁定超过实际必要的时间,因为实际竞争很少。正如标题中已经提到的那样,我想找到一些乐观并发方法。
我可能会尝试第一种解决方案,但也许有更好的选择或潜在的风险。

1
你可以使用插入或更新语句(也称为“upsert”)。据我所记,在SQL Server中,MERGE语句用于此目的。使用它,您可以在一个查询中插入或更新所有CommonEntities(尽管显然无法使用EF - 必须使用原始SQL)。 - Evk
@Evk 谢谢,我完全忘记了MERGE语句。或许你能把这作为一个答案添加进去? - Eugene Podskal
@SeanReeves 很有意思,假期后会看一下。只是我不确定它是否能处理并发问题,虽然我可能错了。如果我在这方面错了,那么也许你可以证明并将其作为答案添加? - Eugene Podskal
问题中没有展示共同实体和组在对象图的其余部分中如何使用。实际上,您的数据库中的其他表是否引用了 CommonEntity.idCommonEntity.Name?当您尝试保存对象图时,您是否知道 CommonEntityCommonEntityGroupNameID?换句话说,当您保存对象图时,您是否需要先通过其 Name 查找实体的 ID - Vladimir Baranov
@VladimirBaranov 我已经将这些信息添加到问题中 - 生产者不知道/不关心那些CommonEntities的ID - 他们通常只传递带有那些CommonEntities名称(唯一)和相关信息的DTO。因此,任何Common(Group)Entity都必须通过名称找到/创建。 - Eugene Podskal
显示剩余4条评论
4个回答

4

表值参数

一种选项是使用 表值参数 而不是单独调用数据库。

使用表值参数的示例过程:

create type dbo.CommonEntity_udt as table (
    CommonEntityGroupId int not null
  , Name      nvarchar(100) not null
  , primary key (CommonEntityGroupId,Name)
    );
go

create procedure dbo.CommonEntity_set (
    @CommonEntity dbo.CommonEntity_udt readonly
) as
begin;
  set nocount on;
  set xact_abort on;
  if exists (
    select 1 
      from @CommonEntity as s
        where not exists (
          select 1 
            from dbo.CommonEntity as t
            where s.Name = t.Name
              and s.CommonEntityGroupId = t.CommonEntityGroupId
            ))
    begin;
      insert dbo.CommonEntity (Name)
        select s.Name
          from @CommonEntity as s
          where not exists (
            select 1 
              from dbo.CommonEntity as t with (updlock, holdlock)
              where s.Name = t.Name
                and s.CommonEntityGroupId = t.CommonEntityGroupId
              );
    end;
end;
go

表值参数参考:


我不建议使用 merge,除非有充分的理由。这种情况只考虑插入,所以使用 merge 似乎有些过头了。

带有表值参数的 merge 版本示例:

create procedure dbo.CommonEntity_merge (
    @CommonEntity dbo.CommonEntity_udt readonly
) as
begin;
  set nocount on;
  set xact_abort on;
  if exists (
    select 1 
      from @CommonEntity as s
        where not exists (
          select 1 
            from dbo.CommonEntity as t
            where s.Name = t.Name
              and s.CommonEntityGroupId = t.CommonEntityGroupId
            ))
    begin;
      merge dbo.CommonEntity with (holdlock) as t
      using (select CommonEntityGroupId, Name from @CommonEntity) as s
      on (t.Name = s.Name
        and s.CommonEntityGroupId = t.CommonEntityGroupId)
      when not matched by target
        then insert (CommonEntityGroupId, Name) 
        values (s.CommonEntityGroupId, s.Name);
    end;
end;
go

MERGE参考资料:


ignore_dup_key 代码注释:

// 检查是否为名称索引违规(也许可以使索引忽略重复键)

ignore_dup_key会在后台使用serializable,可能会对非聚集索引产生昂贵的开销,即使索引是聚集的,也会根据重复项的数量产生重大的成本

可以在存储过程中使用 Sam Saffron的更新/插入模式 或这里展示的模式之一来处理此问题:不同错误处理技术的性能影响 - Aaron Bertrand



你能否将它结构化得更像一个答案,而不是证明我写问题技巧欠缺的证据吗?我的意思是通过移动<strike>来使其更易阅读。 - Eugene Podskal
按照要求移动了它。 - SqlZim
你确定这是乐观并发的方法吗?你正在使用 with (updlock, holdlock) 锁定表... 无论如何,你需要有重试逻辑,因为两个进程可以同时运行 exists 检查,然后尝试插入相同的 Name - Vladimir Baranov
如果表值参数中的所有名称都存在于表中,则不会锁定任何内容。not exists () [...] with (updlock, holdlock) 使用键范围锁定来防止两个并行进程插入相同的名称。merge版本使用with (holdlock),因为它已经在其默认行为中包含了updlock,而且它们再次使用的是键范围锁定,而不是独占的表锁。 - SqlZim
@SqlZim,我认为我对先运行if exists,然后在INSERT中再次运行相同的子查询感到困惑。我希望采取乐观的方法,即首先只需执行必要操作,而无需先进行检查,如果操作失败,则重试。在任何情况下都必须有一些重试逻辑。 - Vladimir Baranov
显示剩余3条评论

2
选择哪种方法,肯定取决于两种过程使用的功能类型和数据量。如果采用第一种方法,每次 SaveChanges() 调用都会放置一个事务,这可能会在大量记录的情况下稍微降低性能。如果需要插入/更新相当数量的记录,则肯定会采用基于存储过程的方法。通过此方法,您将完全控制数据库,并查询记录以检查其是否存在将非常容易(虽然这里可能需要进行一些微调)。我认为使用存储过程实现同样的操作不会有任何挑战。通过一些实现优化,例如将数据加载到临时表中(不是 SQL 临时表,而是可用于暂时存储数据的物理表),可以进一步增强该过程,以完全信息记录存储过程已处理的内容。

那么您主张不尝试实现某种乐观并发插入,而是坚持使用第5种方法(SERIALIZABLE隔离级别/TABLOCKX+SERIALIZABLE表提示在存储过程中)?并且要在存储过程内完成吗? - Eugene Podskal
你认为在SP中实现的第一种方法怎么样?例如:https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/,考虑到我已经添加了一个注释(http://stackoverflow.com/revisions/41336147/4),指出大多数唯一实体通常已经存在于数据库中? - Eugene Podskal
我仍然更喜欢使用基于存储过程的方法,虽然第一种使用存储过程的方法听起来不错,但我建议将所有逻辑放在一个地方。虽然大多数唯一实体通常都在数据库中,但我不建议从EF进行往返检查,因为同样的操作可以在数据库级别上完成。我想要表达的是,在有大量记录集的情况下,让数据库处理它以获得最大的效益。 - Mads...

2
根据您的最后一个关键点,另一个解决方案是将“创建”逻辑移动到一个中央应用程序服务器/服务(请参见更新2),该服务器/服务具有用户可以使用的队列来“添加”记录。
由于大多数记录已经存在,如果使用某种缓存,您应该能够使其非常高效。
现在,关于记录数量。请记住,EF并不是为支持“批量”操作而设计的,因此创建数千条记录会非常慢。
我使用了两个解决方案,可以帮助您快速处理大量记录 1)EntityFramework.BulkInsert 2)SqlBulkCopy 两者都非常容易使用
此外,希望您已经看过Entity Framework中最快的插入方式 更新
以下是我最近使用过两次的另一种解决方案
不要在用户执行“保存”时保存记录,而是安排它在 X 秒后发生。
如果同时有其他人尝试保存相同的记录,则只需“滑动”预定日期即可。

下面是一个样本代码,它尝试同时保存相同的记录 10 次,但实际保存只发生一次。

实际结果可以在此处查看:

enter image description here

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace ConsoleApplicationScheduler
{
    class Program
    {
        static void Main(string[] args)
        {
            ConcurrentSaveService service = new ConcurrentSaveService();
            int entity = 1;
            for (int i = 0; i < 10; i++)
            {
                //Save the same record 10 times(this could be conrurrent)
                service.BeginSave(entity);
            }

            Console.ReadLine();
        }
    }

    public class ConcurrentSaveService
    {
        private static readonly ConcurrentDictionary<int, DateTime> _trackedSubjectsDictionary = new ConcurrentDictionary<int, DateTime>();

        private readonly int _delayInSeconds;

        public ConcurrentSaveService()
        {
            _delayInSeconds = 5;
        }

        public async void BeginSave(int key)
        {
            Console.WriteLine("Started Saving");
            DateTime existingTaskDate;
            _trackedSubjectsDictionary.TryGetValue(key, out existingTaskDate);

            DateTime scheduledDate = DateTime.Now.AddSeconds(_delayInSeconds);
            _trackedSubjectsDictionary.AddOrUpdate(key, scheduledDate, (i, d) => scheduledDate);

            if (existingTaskDate > DateTime.Now)
                return;

            do
            {
                await Task.Delay(TimeSpan.FromSeconds(_delayInSeconds));

                DateTime loadedScheduledDate;
                _trackedSubjectsDictionary.TryGetValue(key, out loadedScheduledDate);
                if (loadedScheduledDate > DateTime.Now)
                    continue;

                if (loadedScheduledDate == DateTime.MinValue)
                    break;

                _trackedSubjectsDictionary.TryRemove(key, out loadedScheduledDate);

                if (loadedScheduledDate > DateTime.MinValue)
                {
                    //DoWork
                    Console.WriteLine("Update/Insert record:" + key);
                }

                break;
            } while (true);

            Console.WriteLine("Finished Saving");
        }
    }
}

更新2 由于您可以在WebAPI应用程序中控制“创建”过程,因此您应该能够避免重复使用类似以下伪代码的某种缓存。
using System.Collections.Concurrent;
using System.Web.Http;

namespace WebApplication2.Controllers
{
    public class ValuesController : ApiController
    {
        static object _lock = new object();
        static ConcurrentDictionary<string, object> cache = new ConcurrentDictionary<string, object>();
        public object Post(InputModel value)
        {
            var existing = cache[value.Name];
            if (existing != null)
                return new object();//Your saved record

            lock (_lock)
            {
                existing = cache[value.Name];
                if (existing != null)
                    return new object();//Your saved record

                object newRecord = new object();//Save your Object

                cache.AddOrUpdate(value.Name, newRecord, (s, o) => newRecord);

                return newRecord;
            }
        }
    }

    public class InputModel
    {
        public string Name;
    }
}

1
是的,关于中央服务为记录创建提供简化生产者、减少其对实际数据库服务器的依赖、以及以统一的方式缓解并发问题这一点说得很有道理。但我的问题不在于要保存的记录数量,而在于某些实体可能已经存在于数据库中,也可能尚未存在-唯一索引保证了重复的不存在,但它不能允许同时保存相同的实体。我希望有一种简单高效的方法来可靠地持久化包含此类实体的对象图。 - Eugene Podskal
@EugenePodskal 有10个用户修改同一条记录的意义是什么?会发生什么?最后一个胜出吗?如果您进行重试,您将如何保证这一点?我认为您应该重新考虑整个过程。并发异常应该是例外而不是常见情况。尽量避免问题而不是解决问题...也许告诉我们更多关于“业务”问题,看看我们是否能找到更好的设计。例如,也许您的用户只需创建新记录,“有效”的记录是最后一个。 - George Vovos
这可能是我的问题,我的问题表述不够清晰(已经修复)。基本上,CommonEntity/CommonEntityGroup一旦创建就是不可变的,之后也永远不会被修改或删除。生产者之间唯一的争议是,他们可能会尝试同时持久化一些CommonEntityLeftInfo和CommonEntityRightInfo,这些信息同时引用了同一个CommonEntity,但该实体可能不存在于数据库中。当生产者调用SaveChanges时,其中一个操作将失败,因为它试图插入重复的条目。即使在这种相当罕见的情况下,我也不希望它失败。 - Eugene Podskal
@EugenePodskal 然后,1)忽略我的更新答案,2)如果您只想避免重复,问题似乎更简单。当您尝试添加重复数据时,会得到特定的异常,对吧?(ConstraintViolation?)您不能直接忽略它吗? - George Vovos
让我们在聊天中继续这个讨论 - George Vovos
显示剩余3条评论

2

生产者不知道/不关心那些CommonEntities的ID - 他们通常只传递带有那些CommonEntities名称(唯一)和相关信息的DTO。因此,任何Common(Group)Entity都必须通过名称找到/创建。

我假设存储对象的表通过它们的ID而不是名称引用CommonEntity

我假设对象的表定义看起来像这样:

CREATE TABLE SomeObject(
    Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    ObjectName NVARCHAR(100) NOT NULL,
    CommonEntityId INT NOT NULL,
    CONSTRAINT FK_SomeObject_CommonEntity FOREIGN KEY(CommonEntityId) 
        REFERENCES CommonEntity(Id)
);

同时,高级的SaveSomeObject函数有CommonEntity.NameCommonEntityGroup.Name作为参数(不是ID)。这意味着在某个地方,函数必须查找实体的Name并找到其对应的ID
因此,具有参数(ObjectName, CommonEntityName, CommonEntityGroupName)的高级SaveSomeObject函数可以分为两个步骤实现:
CommonEntityID = GetCommonEntityID(CommonEntityName, CommonEntityGroupName);
SaveSomeObject(ObjectName, CommonEntityID);

GetCommonEntityID是一个辅助函数/存储过程,通过实体的Name查找其ID并在需要时创建实体(生成ID)。

在这里,我们将此步骤明确地提取到单独的专用函数中。只有这个函数需要处理并发问题。它可以使用乐观并发方法或悲观并发方法来实现。该函数的用户不关心它使用了什么魔法来返回有效的ID,但用户可以确信他可以安全地使用返回的ID来持久化对象的其余部分。


悲观并发方法

悲观并发方法很简单。确保只有一个GetCommonEntityID实例可以运行。我会使用sp_getapplock来实现它(而不是SERIALIZABLE事务隔离级别或表提示)。sp_getapplock本质上是一个互斥锁,一旦获得锁,我们就可以确信没有其他实例的此存储过程在并行运行。这使得逻辑简单-尝试读取ID,如果未找到,则INSERT新行。

CREATE PROCEDURE [dbo].[GetCommonEntityID]
    @ParamCommonEntityName NVARCHAR(100),
    @ParamCommonEntityGroupName NVARCHAR(100),
    @ParamCommonEntityID int OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRANSACTION;
    BEGIN TRY

        SET @ParamCommonEntityID = NULL;
        DECLARE @VarCommonEntityGroupID int = NULL;

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'GetCommonEntityID_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock

            SELECT @VarCommonEntityGroupID = ID
            FROM CommonEntityGroup
            WHERE Name = @ParamCommonEntityGroupName;

            IF @VarCommonEntityGroupID IS NULL
            BEGIN
                -- Such name doesn't exist, create it.
                INSERT INTO CommonEntityGroup (Name)
                VALUES (@ParamCommonEntityGroupName);

                SET @VarCommonEntityGroupID = SCOPE_IDENTITY();
            END;

            SELECT @ParamCommonEntityID = ID
            FROM CommonEntity
            WHERE
                Name = @ParamCommonEntityName
                AND CommonEntityGroupId = @VarCommonEntityGroupID
            ;

            IF @ParamCommonEntityID IS NULL
            BEGIN
                -- Such name doesn't exist, create it.
                INSERT INTO CommonEntity
                    (Name
                    ,CommonEntityGroupId)
                VALUES
                    (@ParamCommonEntityName
                    ,@VarCommonEntityGroupID);

                SET @ParamCommonEntityID = SCOPE_IDENTITY();
            END;

        END ELSE BEGIN
            -- TODO: process the error. Retry
        END;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
            -- TODO: process the error. Retry?
    END CATCH;

END

乐观并发控制策略

不要尝试进行锁定,采取乐观的态度查询ID。如果未找到,请尝试插入新值,并在唯一索引违规时重试。

CREATE PROCEDURE [dbo].[GetCommonEntityID]
    @ParamCommonEntityName NVARCHAR(100),
    @ParamCommonEntityGroupName NVARCHAR(100),
    @ParamCommonEntityID int OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    SET @ParamCommonEntityID = NULL;
    DECLARE @VarCommonEntityGroupID int = NULL;

    SELECT @VarCommonEntityGroupID = ID
    FROM CommonEntityGroup
    WHERE Name = @ParamCommonEntityGroupName;

    WHILE @VarCommonEntityGroupID IS NULL
    BEGIN
        -- Such name doesn't exist, create it.
        BEGIN TRANSACTION;
        BEGIN TRY

            INSERT INTO CommonEntityGroup (Name)
            VALUES (@ParamCommonEntityGroupName);

            SET @VarCommonEntityGroupID = SCOPE_IDENTITY();

            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            ROLLBACK TRANSACTION;
            -- TODO: Use ERROR_NUMBER() and ERROR_STATE() to check that
            -- error is indeed due to unique index violation and retry
        END CATCH;

        SELECT @VarCommonEntityGroupID = ID
        FROM CommonEntityGroup
        WHERE Name = @ParamCommonEntityGroupName;

    END;


    SELECT @ParamCommonEntityID = ID
    FROM CommonEntity
    WHERE
        Name = @ParamCommonEntityName
        AND CommonEntityGroupId = @VarCommonEntityGroupID
    ;

    WHILE @ParamCommonEntityID IS NULL
    BEGIN
        -- Such name doesn't exist, create it.
        BEGIN TRANSACTION;
        BEGIN TRY

            INSERT INTO CommonEntity
                (Name
                ,CommonEntityGroupId)
            VALUES
                (@ParamCommonEntityName
                ,@VarCommonEntityGroupID);

            SET @ParamCommonEntityID = SCOPE_IDENTITY();

            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            ROLLBACK TRANSACTION;
            -- TODO: Use ERROR_NUMBER() and ERROR_STATE() to check that
            -- error is indeed due to unique index violation and retry
        END CATCH;

        SELECT @ParamCommonEntityID = ID
        FROM CommonEntity
        WHERE
            Name = @ParamCommonEntityName
            AND CommonEntityGroupId = @VarCommonEntityGroupID
        ;

    END;

END

在这两种方法中,您都应该使用重试逻辑。如果您预计实体表中已经有名称并且重试的可能性很低(就像问题描述中的情况),则乐观的方法通常更好。如果您预计会有许多竞争进程尝试插入相同的名称,则悲观的方法通常更好。如果您串行插入,您可能会获得更好的效果。


感谢sp_getapplock,这是一个值得考虑的有趣方法。可能将GetCommonEntityIdGetCommonEntityGroupId存储过程分开,并在GetCommonEntityId中调用GetCommonEntityGroupId会更易读一些。 - Eugene Podskal
我喜欢使用 sp_getapplock,因为它不会像表提示一样锁定整个表格,所以如果系统的其他部分使用表格的其他无关部分,它们不会受到影响。而且互斥锁的逻辑对我来说很容易理解。 - Vladimir Baranov
我不确定为什么您认为表提示会锁定表。当存在索引时,with (updlock, holdlock)使用键范围锁,而不是表锁。了解更多请参考键范围锁定 - SqlZim
@SqlZim,是的,引擎通常只尝试锁定它需要的行,但它可以升级锁定到页面和表级别,这种升级是相当不可预测的。此外,随着创建或删除索引,这种行为可能会发生变化。是的,锁定提示允许实现正确的行为,但我个人更喜欢使用sp_getapplock,因为要查找的隐藏“陷阱”较少。 - Vladimir Baranov
@BogdanSahlean,你说得对,第一种方法串行化了对GetCommonEntityID的调用,这就是我称之为悲观的原因。第二种方法是乐观的,因为它试图在事先没有额外检查的情况下完成所需的操作,并且如果操作失败,还有重试逻辑。我相信它是线程安全的,因为有这个重试逻辑。 - Vladimir Baranov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接