C# 锁定中间件或服务以防止竞争条件
我们有一个基于IdentityServer4的集中式身份验证服务器,为我们所有的各种应用程序提供电源。当用户第一次访问应用程序时,一段中间件,C# 锁定中间件或服务以防止竞争条件,c#,asp.net-core,.net-core,concurrency,race-condition,C#,Asp.net Core,.net Core,Concurrency,Race Condition,我们有一个基于IdentityServer4的集中式身份验证服务器,为我们所有的各种应用程序提供电源。当用户第一次访问应用程序时,一段中间件,UserProvisioningMiddleware确定当前用户是否已在当前应用程序中设置(有效地将他们的一些JWT声明插入到特定于应用程序的数据库中)。在极少数情况下,竞争条件可能会发生,即同时发出多个请求,中间件会针对每个请求执行,并且它会多次尝试为用户提供资源。这会导致内部服务器异常,因为JWTsub被用作主键 一个简单的解决方法是在将用户保存到数据
UserProvisioningMiddleware
确定当前用户是否已在当前应用程序中设置(有效地将他们的一些JWT声明插入到特定于应用程序的数据库中)。在极少数情况下,竞争条件可能会发生,即同时发出多个请求,中间件会针对每个请求执行,并且它会多次尝试为用户提供资源。这会导致内部服务器异常,因为JWTsub
被用作主键
一个简单的解决方法是在将用户保存到数据库之前进行最后一次检查,并将该调用包装为try-catch以静默地放弃重复的主键错误,但这是次优的。第二种可能的解决方案是维护当前正在进行资源调配的所有用户的静态哈希集
,并在我的IsUserProvisioned
方法中使用计时器创建任务
,该方法等待用户出列并进行资源调配,但这仍然可能会导致一些潜在的死锁
以下是my user provisioning service的实现:
公共类UserProvisioningService:IUserProvisioningService
{
专用只读IClaimsUserService(U claimsUserService);
私有只读ICurrentUserService\u currentUserService;
私有只读IJobSchedulingContext\u context;
公共用户配置服务(
ICLAIMUSERSERVICE索赔服务,
ICurrentUserService当前用户服务,
IJobSchedulingContext(上下文)
{
_索赔服务=索赔服务;
_currentUserService=currentUserService;
_上下文=上下文;
}
///
公共任务IsProvisionedAsync()
{
var userId=\u currentUserService.userId;
if(userId==null)
抛出新的NotAuthorizedException();
return\u context.Users
.NotCacheable()
.AnyAsync(u=>u.Id==userId);
}
///
公共任务供应同步()
{
var userId=\u currentUserService.userId;
if(userId==null)
抛出新的NotAuthorizedException();
var claimsUser=_claimsUser.GetClaimsUser();
if(claimsUser==null)
抛出新的InvalidOperationException(“无法设置用户”);
var user=新应用程序用户(
userId.Value,
索赔人GivenName,
claimsUser.FamilyName,
索赔人(电子邮件);
_context.Users.Add(用户);
返回_context.SaveChangesAsync();
}
}
以及中间件
公共类UserProvisioningMiddleware
{
private readonly RequestDelegate\u next;
public UserProvisioningMiddleware(RequestDelegate下一步)
{
_下一个=下一个;
}
[正确使用]
公共异步任务调用(
HttpContext上下文,
IUserProvisioningService(服务供应服务)
{
if(context.User?.Identity?.IsAuthenticated==true)
如果(!await provisioningService.IsProvisionedAsync())
等待provisioningService.ProvisionionSync();
等待下一步(上下文);
}
}
在不牺牲性能的情况下,我还有什么其他选择可以防止这种竞争状况在将来再次发生?- 最好的方法是将其用作原子函数。 一种方法是创建一个类似TryProvisionAsync()的方法,该方法执行一个存储过程,该存储过程执行所有选择/插入业务并返回适当的结果
- 另一个解决方案是使用Redis分布式锁,这有助于避免竞争条件
- 最后一个解决方案是在服务中保持应用程序锁,但如果您想听听我的意见,这将不是一个好方法,尤其是在服务的负载平衡和分发场景之后。因为服务的不同节点在自己的内存中持有自己的锁,这将是条件失败的单点
public interface IUnitOfWork {
public async Task<bool> TryProvisionOrBeOstrich();
}
public class ProvissioningUnitOfWork : IUnitOfWork {
private static readonly object _lock= new object();
private readonly IServiceScopeFactory _serviceScopeFactory;
公共接口IUnitOfWork{
公共异步任务TryProvisionOrBeOstrich();
}
公共类ProvisioningUnitofWork:IUnitOfWork{
私有静态只读对象_lock=new object();
专用readonly IServiceScopeFactory\u serviceScopeFactory;
//注入您的所有服务
//注入服务提供者以创建一些作用域并获得临时服务
public async Task<book> TryProvisionOrBeOstrich() {
//get your username from context
using (var scope = _serviceScopeFactory.CreateScope())
{
var _claimsUserService = scope.GetService<IClaimsUserService>();
var _currentUserService = scope.GetService<ICurrentUserService>();
var _context = scope.GetService< IJobSchedulingContext context>();
var provisioningService = scope.ServiceProvider.GetService<UserProvisioningService>();
lock(_lock)
{
if (!await provisioningService.IsProvisionedAsync())
await provisioningService.ProvisionAsync();
}
}
}
}
public异步任务TryProvisionOrBeOstrich(){
//从上下文获取您的用户名
使用(var scope=\u serviceScopeFactory.CreateScope())
{
var_claimsUserService=scope.GetService();
var_currentUserService=scope.GetService();
var_context=scope.GetService();
var provisioningService=scope.ServiceProvider.GetService();
锁
{
如果(!await provisioningService.IsProvisionedAsync())
等待provisioningService.ProvisionionSync();
}
}
}
}
- 现在,您可以将UnitOfWork添加为SingletonService,并将其注入控制器,使用其方法而不会出现任何问题