【问题标题】:Allow access to operation only one user at the time一次只允许一个用户访问操作
【发布时间】:2019-11-19 03:31:05
【问题描述】:

我有与 web api 通信的 ASP.NET Core 应用程序。 有一个业务操作涉及几个步骤(在一个页面上做某事,转到下一页和下一页)。这种多步骤操作是在一个元素的上下文中完成的。

假设你有一个一些业务对象的列表,你的任务是从这个列表中接受object 3。接受是我们的多步操作,如果我目前接受对象 3,其他人应该不能进入接受对象 3 的操作。当我完成操作时,它应该被解锁。 希望问题是可以理解的。

我们不想要非常耗时的解决方案,最简单的想法是创建一个指示用户何时开始操作的数据库表,它保存对象的 id 和用户的 id 并在例如 5 分钟后自动删除自己,如果其他人想要访问操作,我们检查它是否被阻止此对象。但它有点hacky而且不是很干净(如果用户去喝咖啡并在10分钟后继续操作怎么办?)

我正在寻找一种更好的方法来实现这种行为并欣赏任何想法

【问题讨论】:

    标签: c# asp.net-core identityserver4 asp.net-core-webapi


    【解决方案1】:

    您正在寻找信号量。 lock 关键字是最基本的信号量,但您也可以使用Semaphore/SemaphoreSlim,它提供了诸如速率限制之类的功能,而lock 将逐个操作一个操作。但是,您的目标是一次只针对特定资源选择一个操作,,这使得SemaphoreSlim 成为更好的选择,特别是ConcurrentDictionary<string, SemaphoreSlim>

    您需要一个具有单例生命周期的类(应用程序整个生命周期的一个实例)。在那里,您将添加一个 ivar:

    private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
    

    然后,您将在要门控的操作周围添加以下代码:

    var semaphore = _semaphores.GetOrAdd("object3", _ => new SemaphoreSlim(1, 1));
    await semaphore.WaitAsync();
    
    // do something
    
    semaphore.Release();
    

    "object3" 显然只是一个占位符。您将希望使用任何有意义的东西(ID 等) - 唯一标识您要控制的特定资源的东西。如果该特定资源上存在现有操作,则这将仅保留对该特定资源的操作。不同的资源将获得自己的信号量,从而获得自己的门。

    【讨论】:

    • 内存解决方案似乎不会容错或以任何方式横向扩展。如果此操作对业务至关重要,那么基于数据库的机制可能是硬性要求。
    • 首先,此处提供的代码不一定要在应用程序中出现。它可以在单独的控制台应用程序、服务等中。其次,OP 没有指定它必须是持久锁。它只需要防止同时对同一个“对象”进行两次操作,如果应用程序出现故障,则不会对任何对象进行任何操作。数据库方法可以让您在应用重新启动时保持持久性,但它会成倍增加并发问题。
    • 您需要使用所有可能的对象预先填充数据库,以便您可以进行乐观更新,然后制定重试策略以在更新失败时重新尝试。在大多数情况下,插入是不可行的。
    • 你的评论让我有点困惑,但也许我弄错了。我假设这种强制执行必须在 Web 服务器上进行,该服务器可能位于服务器场中,或者受到应用程序池回收的影响。在这两种情况下,锁定原语可能同时存在于两个或多个应用程序域中,因此无法正常工作。
    • 还是一头雾水。不知道您所说的“外部过程”是什么意思。正如我所读到的那样,该架构是一个 .NET 核心应用程序与单个 Web API 通信的几个实例——非常简单,非常常见。我不知道控制台程序或附加进程在那个环境中的位置。也许我们只需要同意不同意。
    【解决方案2】:

    如果我要实现这种行为,我也会使用数据库,但方式有点不同。我将制作一个 objects 表(object 3 是它的行之一),为UserId 添加一列,布尔值OnProcess(以标记对象是否正在进行中)和StartProcess 的时间戳。

    为了让用户能够输入操作,运行如下查询:

    UPDATE Objects SET UserId = <CurrentUser>, StartProcess = <NOW>, OnProcess = true
    OUTPUT Object.Id
    WHERE Object.Id == 3 AND 
        (
            OnProcess == false
            OR ( OnProcess == true AND UserId == <CurrentUser> )
            OR ( StartProcess <is more than 15 minutes ago>)
        )
    

    免责声明:上面的查询不是可执行查询,但应该足够清楚以了解它的作用。

    使用上面的查询,Object.Id 将在以下情况下返回:

    • 对象未被其他用户处理
    • CurrentUser 本身正在处理对象,同时重置StartProcess(某种滑动行为)。这样,如果CurrentUserAFK 给定时间(但不超过阈值时间)并回来,他/她可以舒适地继续操作
    • 在过去 15 分钟内未处理对象。这实际上是我在前面提到的阈值。至于多长时间(在我的示例中为 15 分钟),这完全取决于您。

    如果为用户返回Object.Id,则他/她可以进入操作。

    【讨论】:

    • 如果我理解正确,解决方案涉及创建对象表,用户可以在其上开始多步操作?情况是我确实已经有了这样的表,但它包含大约 0.5M 条记录,所以我不确定向它添加一列是否会是个好主意
    • 您的表目前有多少列?我认为再添加 3 列不会有问题,除非您的表中已经有数百个 select * 在您的代码中
    • 好吧,它有 16 列,也许你是对的,但创建一个专门用于此目的的小型新表不是更好的主意吗?
    • 我的回答中的查询假设它不必担心行的可用性。如果您创建一个新的较小的表,那么您必须确保在任何用户可以开始之前 row 存在。根据您的整个应用程序,它可能是微不足道的,也可能是难以实现的。我想只有你可以决定哪个更好,创建小表或添加 3 列。
    猜你喜欢
    • 1970-01-01
    • 2017-02-26
    • 2014-09-05
    • 2017-03-20
    • 2014-02-11
    • 1970-01-01
    • 2015-03-12
    • 1970-01-01
    • 2013-05-18
    相关资源
    最近更新 更多