我有一个用Asp.Net Core和Entity Framework&SQL Server编写的支付系统。 我必须处理许多安全情况,这些情况需要防止用户同时执行2个或更多操作。例如:
每次付款交易
现在,如果用户触发2个或更多创建付款的请求,则其中一些请求将超过可用的信用验证。 由于我有许多与此场景相似的场景,因此我想到了可以解决所有这些问题的通用解决方案。我考虑过要添加一个将检查每个请求以及以下内容的中间件:
为了以正确的方式执行此操作并支持多于1台服务器,我知道我需要在数据库级别执行此操作。我知道我可以锁定Oracle中的特定行,并考虑在每个UPDATE / CREAT / DELETE的开头锁定用户行,并在最后释放它。 EF的最佳方法是什么?有更好的解决方案吗?
我使用的是UnitOfWork模式,每个请求都有自己的作用域。
最好, 塔尔
答案 0 :(得分:0)
我反对使用行锁作为请求同步机制:
尽管Oracle因不升级行锁而闻名,但仍有事务级和其他优化可能决定升级,这可能导致可伸缩性降低和死锁。
如果两个用户想互相转账,则有可能会陷入僵局。 (如果您现在没有这样的功能,将来可能会拥有,所以最好创建一个不会轻易失效的架构)。
现在,仅由于来自同一用户的另一个请求恰好花费较长时间而返回“错误请求”的系统当然是故障安全系统,但是其可靠性(无故障运行指标)受到影响。我希望世界上任何付款系统都能够保证故障安全和可靠。
有更好的解决方案吗?
基于CQRS和shared-nothing方法的架构:
什么都不共享的体系结构可以通过分区(AKA分片)来实现。例如:
thread_index = HASH(User) % N
,或者如果用户ID是整数:thread_index = USER_ID % N
,总是将用户的每个请求分派到同一特定线程。此外,您需要一个协调器来确保M台计算机中的每台都已启动并正在运行。如果一台机器出现故障,协调器会旋转另一台具有相同角色的机器。例如,如果您对服务进行了docker化,则可以将Kubernetes与StatefulSet结合使用。
答案 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>();