ASP.. NET Core IStringLocalizerFactory并发



. NET Core 3.1项目我有一个自定义的IStringLocalizerFactory,通过实体框架与数据库工作:

public class EFStringLocalizerFactory : IStringLocalizerFactory
{
private readonly LocalizationContext _context;
private readonly IMemoryCache _memoryCache;
private static readonly ConcurrentDictionary<string, IStringLocalizer> InternalLocalizersHolder = new ConcurrentDictionary<string, IStringLocalizer>();
public EFStringLocalizerFactory(LocalizationContext context, IMemoryCache memoryCache)
{
_context = context;
_memoryCache = memoryCache;
}
public IStringLocalizer Create(Type resourceSource)
{
return CreateStringLocalizer(_context, _memoryCache, resourceSource.FullName);
}
public IStringLocalizer Create(string baseName, string location)
{
return CreateStringLocalizer(_context, _memoryCache, baseName);
}
internal static IStringLocalizer CreateStringLocalizer(LocalizationContext context, IMemoryCache memoryCache, string resourceSection)
{
return InternalLocalizersHolder.GetOrAdd(resourceSection, s => new EFStringLocalizer(context, memoryCache, s));
}
}

EFStringLocalizer类是这样的:

public class EFStringLocalizer : IStringLocalizer
{
private readonly LocalizationContext _context;
private readonly IMemoryCache _translationsCache;
private readonly string _resourceSection;

public EFStringLocalizer(LocalizationContext context, IMemoryCache memoryCache, string resourceSection)
{
_context = context;
_translationsCache = memoryCache;
_resourceSection = resourceSection;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == name);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return EFStringLocalizerFactory.CreateStringLocalizer(_context, _translationsCache, _resourceSection);
}
//TODO fix parameter usage?
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
try
{
return _translationsCache.GetOrCreate($"LocalizerGetAllStrings-{CultureInfo.CurrentCulture.Name}-{_resourceSection}", entry =>
{
var keysWithTranslations = _context.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name && r.Section == _resourceSection)
.Select(r => new LocalizedString(r.Key, r.Value)).ToList();
return keysWithTranslations;
});
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private string GetString(string name)
{
return GetAllStrings(false).FirstOrDefault(r => r.Name == name)?.Value;
}
}

资源/文化类只是存储在数据库中的poco。我有以下代码来注册我的依赖在Startup.cs类:

public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<LocalizationContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultLocalizationConnection")));//still concurrency error!
...
services.AddSingleton<IStringLocalizerFactory, EFStringLocalizerFactory>();
...
}

问题是每当应用程序回收/重新启动时,我有一定的机会得到与EF并发相关的异常。更困难的是,我无法可靠地重现这个问题。下面是堆栈跟踪:

2021-01-28 15:00:07.2356|ERROR|Microsoft.EntityFrameworkCore.Query|System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()|An exception occurred while iterating over the results of a query for context type 'DataAccess.LocalizationContext'.
System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
2021-01-28 15:00:07.2615|ERROR|Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware|System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at Localization.EFStringLocalizer.<GetAllStrings>b__9_0(ICacheEntry entry) in EFStringLocalizer.cs:line 58
at Microsoft.Extensions.Caching.Memory.CacheExtensions.GetOrCreate[TItem](IMemoryCache cache, Object key, Func`2 factory)
at Localization.EFStringLocalizer.GetAllStrings(Boolean includeAncestorCultures) in EFStringLocalizer.cs:line 56
at Localization.EFStringLocalizer.GetString(String name) in EFStringLocalizer.cs:line 80
at Localization.EFStringLocalizer.get_Item(String name) in EFStringLocalizer.cs:line 30
at Microsoft.AspNetCore.Mvc.Localization.HtmlLocalizer.get_Item(String name)
at Microsoft.AspNetCore.Mvc.Localization.HtmlLocalizer`1.get_Item(String name)
at AspNetCore.Views_Home_IndexNew.ExecuteAsync() in WebInterfaceViewsHomeIndexNew.cshtml:line 15
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)
at Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatcher|8_0(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task`1 matcherTask)
at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
at WebInterface.Startup.<>c.<<AddSecurityMiddlewares>b__12_4>d.MoveNext() in Startup.cs:line 284
--- End of stack trace from previous location where exception was thrown ---
at NWebsec.AspNetCore.Middleware.Middleware.CspMiddleware.Invoke(HttpContext context)
at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)|An unhandled exception has occurred while executing the request.

我认为用瞬态作用域注册LocalizationContext不会有帮助,因为EFStringLocalizerFactory无论如何都是单例注册的。在IStringLocalizerFactory中,除了引入全局锁或其他低效的技术之外,是否有更好/适当的方法来处理并发?

我相信将LocalizationContext注册为瞬态作用域不会有帮助,因为EFStringLocalizerFactory无论如何都被注册为单例。

正确的。

除了引入全局锁或其他低效的技术外,是否有更好/适当的方法来处理IStringLocalizerFactory内的并发性?

据我所知没有。

EF Core DbContexts一次只支持一个操作,我认为错误信息是清楚的。另一个因素是,内存缓存实现不执行任何类型的锁定,因此用于创建缓存条目的lambda表达式可以由想要从缓存中读取的几个消费者并发执行。

显式锁定是IMO的一种方式,有两个选项:

  • 绕过GetOrCreate方法,这意味着你可以保证EF Core查询只运行一次,但没有两个消费者能够并发地从缓存中读取;或
  • 围绕EF Core查询,这意味着你可以潜在地覆盖现有的缓存条目,但消费者可以并发地从缓存中读取。

我个人会选择选项2,并使用SemaphoreSlim实例。

最新更新