(ASP.NET 核心中间件) 同一个client/user如何高效保证并行执行代码的线程安全?
(ASP.NET Core middleware) How to efficiently ensure thread safety for code being executed in parallel for the same client/user?
我有一个简单的 ASP.NET Core 2.2 Web API,它使用 Windows 身份验证并需要以下内容:
- 如果用户不在数据库中,则在第一次访问时将其插入
- 如果用户在过去 X 小时内未访问该应用程序,则增加该特定用户的访问计数
目前,我已经为此编写了一个快速而肮脏的解决方案:
/// <inheritdoc/>
public ValidationResult<bool> EnsureCurrentUserIsRegistered()
{
var ret = new ValidationResult<bool> { Payload = false };
string username = GetHttpContextUserName();
if (string.IsNullOrWhiteSpace(username))
return new ValidationResult<bool> { IsError = true, Message = "No logged in user" };
var user = AppUserRepository.All.FirstOrDefault(u => u.Username == username);
DateTime now = TimeService.GetCurrentUtcDateTime();
if (user != null)
{
user.IsEnabled = true;
// do not count if the last access was quite recent
if ((now - (user.LastAccessTime ?? new DateTime(2018, 1, 1))).TotalHours > 8)
user.AccessCount++;
user.LastAccessTime = now;
DataAccess.SaveChanges();
return ret;
}
// fetching A/D info to use in newly created record
var userInfoRes = ActiveDirectoryService.GetUserInfoByLogOn(username);
if (userInfoRes.IsError)
{
string msg = $"Could not find A/D info for user {username}";
Logger.LogError(msg);
}
Logger.LogInfo("Creating non-existent user {username}");
// user does not exist, creating it with minimum rights
var userInfo = userInfoRes.Payload;
var dbAppUser = new AppUser
{
Email = userInfo?.EmailAddress ?? "noemail@metrosystems.net",
FirstName = userInfo?.FirstName ?? "<no_first_name>",
LastName = userInfo?.LastName ?? "<no last name>",
IsEnabled = true,
Username = username,
UserPrincipalName = userInfo?.UserPrincipalName ?? "<no UPN>",
IsSuperUser = false,
LastAccessTime = TimeService.GetCurrentUtcDateTime(),
AccessCount = 1
};
AppUserRepository.Insert(dbAppUser);
DataAccess.SaveChanges();
ret.Payload = true;
return ret;
}
从中间件调用代码:
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
if (context.Request.Method == "OPTIONS")
{
await _next(context);
return;
}
ISecurityService securityService =
(ISecurityService) context.RequestServices.GetService(typeof(ISecurityService));
securityService.EnsureCurrentUserIsRegistered();
}
catch (Exception exc)
{
ILoggingService logger =
(ILoggingService)context.RequestServices.GetService(typeof(ILoggingService));
logger.LogError(exc, "Failed to ensure current user is registered");
}
await _next(context);
}
UI是一个SPA,可能会同时触发多个请求,由于上述逻辑不是线程安全的,所以有时会失败。
我在思考如何轻松实现一个线程安全机制:
- 使用session存储访问信息
- 定义一个 ConcurrentDictionary(用户名 => 访问信息)并将信息存储在那里
然而,这看起来相当复杂,我想知道是否有任何内置机制允许在用户级别的关键部分执行某些代码。
问题:如何有效保证并行执行代码的线程安全client/user?
关键是容错。除非您使用信号量(锁)对逻辑进行门控以一次只允许一个操作(这显然会影响性能),否则无法确保同一用户不会同时发生多个操作。
相反,您需要为此专注于计划,并制定应对策略。如果您正在修改特定的 table 行,您可以使用乐观并发来确保写入不会成功发生,除非该行处于与操作开始时相同的状态。这是通过存储并发令牌的列来处理的,该令牌在每次写入时都会更新。如果操作开始时的并发令牌与更新时数据库中的实际内容不匹配,则会抛出异常,您可以随后处理该异常。
如果多次写入不一定会产生不同的数据,您可以简单地捕获并忽略抛出的异常(尽管您仍然可能希望至少记录它们)。
总而言之,如何处理并发冲突在很大程度上取决于具体情况,但在所有情况下,都有一些方法可以优雅地恢复。这才是你应该关注的。
我有一个简单的 ASP.NET Core 2.2 Web API,它使用 Windows 身份验证并需要以下内容:
- 如果用户不在数据库中,则在第一次访问时将其插入
- 如果用户在过去 X 小时内未访问该应用程序,则增加该特定用户的访问计数
目前,我已经为此编写了一个快速而肮脏的解决方案:
/// <inheritdoc/>
public ValidationResult<bool> EnsureCurrentUserIsRegistered()
{
var ret = new ValidationResult<bool> { Payload = false };
string username = GetHttpContextUserName();
if (string.IsNullOrWhiteSpace(username))
return new ValidationResult<bool> { IsError = true, Message = "No logged in user" };
var user = AppUserRepository.All.FirstOrDefault(u => u.Username == username);
DateTime now = TimeService.GetCurrentUtcDateTime();
if (user != null)
{
user.IsEnabled = true;
// do not count if the last access was quite recent
if ((now - (user.LastAccessTime ?? new DateTime(2018, 1, 1))).TotalHours > 8)
user.AccessCount++;
user.LastAccessTime = now;
DataAccess.SaveChanges();
return ret;
}
// fetching A/D info to use in newly created record
var userInfoRes = ActiveDirectoryService.GetUserInfoByLogOn(username);
if (userInfoRes.IsError)
{
string msg = $"Could not find A/D info for user {username}";
Logger.LogError(msg);
}
Logger.LogInfo("Creating non-existent user {username}");
// user does not exist, creating it with minimum rights
var userInfo = userInfoRes.Payload;
var dbAppUser = new AppUser
{
Email = userInfo?.EmailAddress ?? "noemail@metrosystems.net",
FirstName = userInfo?.FirstName ?? "<no_first_name>",
LastName = userInfo?.LastName ?? "<no last name>",
IsEnabled = true,
Username = username,
UserPrincipalName = userInfo?.UserPrincipalName ?? "<no UPN>",
IsSuperUser = false,
LastAccessTime = TimeService.GetCurrentUtcDateTime(),
AccessCount = 1
};
AppUserRepository.Insert(dbAppUser);
DataAccess.SaveChanges();
ret.Payload = true;
return ret;
}
从中间件调用代码:
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
if (context.Request.Method == "OPTIONS")
{
await _next(context);
return;
}
ISecurityService securityService =
(ISecurityService) context.RequestServices.GetService(typeof(ISecurityService));
securityService.EnsureCurrentUserIsRegistered();
}
catch (Exception exc)
{
ILoggingService logger =
(ILoggingService)context.RequestServices.GetService(typeof(ILoggingService));
logger.LogError(exc, "Failed to ensure current user is registered");
}
await _next(context);
}
UI是一个SPA,可能会同时触发多个请求,由于上述逻辑不是线程安全的,所以有时会失败。
我在思考如何轻松实现一个线程安全机制:
- 使用session存储访问信息
- 定义一个 ConcurrentDictionary(用户名 => 访问信息)并将信息存储在那里
然而,这看起来相当复杂,我想知道是否有任何内置机制允许在用户级别的关键部分执行某些代码。
问题:如何有效保证并行执行代码的线程安全client/user?
关键是容错。除非您使用信号量(锁)对逻辑进行门控以一次只允许一个操作(这显然会影响性能),否则无法确保同一用户不会同时发生多个操作。
相反,您需要为此专注于计划,并制定应对策略。如果您正在修改特定的 table 行,您可以使用乐观并发来确保写入不会成功发生,除非该行处于与操作开始时相同的状态。这是通过存储并发令牌的列来处理的,该令牌在每次写入时都会更新。如果操作开始时的并发令牌与更新时数据库中的实际内容不匹配,则会抛出异常,您可以随后处理该异常。
如果多次写入不一定会产生不同的数据,您可以简单地捕获并忽略抛出的异常(尽管您仍然可能希望至少记录它们)。
总而言之,如何处理并发冲突在很大程度上取决于具体情况,但在所有情况下,都有一些方法可以优雅地恢复。这才是你应该关注的。