ASP.NET CORE和EF-锁定每个用户的创建/更新/删除请求

时间:2019-07-28 08:33:14

标签: c# asp.net-mvc entity-framework asp.net-core locking

我有一个用Asp.Net Core和Entity Framework&SQL Server编写的支付系统。 我必须处理许多安全情况,这些情况需要防止用户同时执行2个或更多操作。例如:

每次付款交易

  1. 检查用户余额,并在信用额度不足时阻止。
  2. 执行付款并减少用户余额。

现在,如果用户触发2个或更多创建付款的请求,则其中一些请求将超过可用的信用验证。 由于我有许多与此场景相似的场景,因此我想到了可以解决所有这些问题的通用解决方案。我考虑过要添加一个将检查每个请求以及以下内容的中间件:

  1. 如果该请求是GET请求,它将通过-这将启用并发GET请求。
  2. 如果该请求是POST / PUT / DELETE请求,它将检查此特定用户是否已经存在POST / PUT / DELETE(假设该用户已登录)。如果存在,错误请求的响应将返回给客户端。

为了以正确的方式执行此操作并支持多于1台服务器,我知道我需要在数据库级别执行此操作。我知道我可以锁定Oracle中的特定行,并考虑在每个UPDATE / CREAT / DELETE的开头锁定用户行,并在最后释放它。 EF的最佳方法是什么?有更好的解决方案吗?

我使用的是UnitOfWork模式,每个请求都有自己的作用域。

最好, 塔尔

2 个答案:

答案 0 :(得分:0)

我反对使用行锁作为请求同步机制:

  • 尽管Oracle因不升级行锁而闻名,但仍有事务级和其他优化可能决定升级,这可能导致可伸缩性降低和死锁。

  • 如果两个用户想互相转账,则有可能会陷入僵局。 (如果您现在没有这样的功能,将来可能会拥有,所以最好创建一个不会轻易失效的架构)。

现在,仅由于来自同一用户的另一个请求恰好花费较长时间而返回“错误请求”的系统当然是故障安全系统,但是其可靠性(无故障运行指标)受到影响。我希望世界上任何付款系统都能够保证故障安全和可靠。

  

有更好的解决方案吗?

基于CQRSshared-nothing方法的架构:

  • ASP.NET服务器(“网络层”):
    • 像现在一样直接执行读取(GET)操作
    • 将写入(POST / PUT / DELETE)操作提交到队列中,并立即返回HTTP 200。
  • 应用层:以无共享方式获取和执行写请求的(微)服务集群:
    • 随时,整个系统中(在所有进程和机器上)最多由一个线程处理来自任何特定用户的请求
    • 无共享方法确保您不必同时处理来自同一用户的请求。

实施无共享内容

什么都不共享的体系结构可以通过分区(AKA分片)来实现。例如:

  • 您有N个处理线程(在某些进程中)在M台机器的群集上运行
  • 为每台计算机分配一个独特的角色,以在这N个线程中运行特定范围的线程
  • 通过计算:thread_index = HASH(User) % N,或者如果用户ID是整数:thread_index = USER_ID % N,总是将用户的每个请求分派到同一特定线程。
  • 已调度的请求如何传递到处理线程取决于选择的队列。例如,Web层可以将请求提交到N个不同的主题,或者可以将请求直接推送到分布式参与者(请参见Akka.Net),或者您可以仅将数据库表用作队列,并使每个线程获取属于它的请求。

此外,您需要一个协调器来确保M台计算机中的每台都已启动并正在运行。如果一台机器出现故障,协调器会旋转另一台具有相同角色的机器。例如,如果您对服务进行了docker化,则可以将KubernetesStatefulSet结合使用。

答案 1 :(得分:0)

最近在同样的想法中跌跌撞撞。由于某些原因,我的一些用户能够两次发布公式,这导致数据重复。

即使这是一个老问题,我希望它能对某人有所帮助。

就像您提到的那样,一种方法是使用数据库来锁定属性,但是像您一样,我找不到可靠的实现。我还假设您有一个整体应用程序,因为@ felix-b提到了一个很好的解决方案。

我采用了使通常可以并发运行的线程顺序运行的方式。此解决方案可能会遇到一些缺点,但我找不到任何缺点。请让我知道您的想法。

所以我用包含UserId和SemaphoreSlim的字典来解决了这个问题。

然后,我仅用ActionFilter标记控制器,并限制每个用户执行控制器方法。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AtomicOperationPerUserAttribute : ActionFilterAttribute
{
    private readonly ILogger<AtomicOperationPerUserAttribute> _logger;
        private readonly IConcurrencyService _concurrencyService;

        public AtomicOperationPerUserAttribute(ILogger<AtomicOperationPerUserAttribute> logger, IConcurrencyService concurrencyService)
        {
            _logger = logger;
            _concurrencyService = concurrencyService;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            int userId = YourWayToGetTheUserId; //mine was with context.HttpContext.AppSpecificExtensionMethod()

            _logger.LogInformation($"User {userId} claims sempaphore with RequestId {context.HttpContext.TraceIdentifier}");

            var semaphore = _concurrencyService.SemaphorePerUser(userId);

            semaphore.Wait();
        }

        public override void OnActionExecuted(ActionExecutedContext context)
        {
            int userId = YourWayToGetTheUserId; //mine was with context.HttpContext.AppSpecificExtensionMethod()

            var semaphore = _concurrencyService.SemaphorePerUser(userId);

            _logger.LogInformation($"User {userId} releases sempaphore with RequestId {context.HttpContext.TraceIdentifier}");
            
            semaphore.Release();
        }
    }

“ ConcurrentService”是在Startup.cs中注册的Singleton。

public interface IConcurrencyService
{
    SemaphoreSlim SemaphorePerUser(int userId);
}

public class ConcurrencyService : IConcurrencyService
{
    public static ConcurrentDictionary<int, SemaphoreSlim> Semaphors = new ConcurrentDictionary<int, SemaphoreSlim>();

    public SemaphoreSlim SemaphorePerUser(int userId)
    {
        return Semaphors.GetOrAdd(userId, new SemaphoreSlim(1, 1));
    }
}

由于在我的情况下,我需要在ActionFilter中使用依赖项,以[ServiceFilter(typeof(AtomicOperationPerUserAttribute))]标记控制器动作

我因此在Startup.cs

中注册了“服务”
services.AddScoped<AtomicOperationPerUserAttribute>();
services.AddSingleton<IConcurrencyService, ConcurrencyService>();