ASP.NET Core IStringLocalizerFactory并发

3
在ASP.NET Core 3.1项目中,我有一个自定义的,它通过实体框架与数据库交互:
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 WebInterface\Views\Home\IndexNew.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并发性的方法?
1个回答

2
我认为将LocalizationContext注册为短暂范围不会有帮助,因为EFStringLocalizerFactory已经作为单例注册了。
正确。
除了引入全局锁或其他低效技术之外,还有没有更好/更适当的处理IStringLocalizerFactory中并发的方法?
据我所知,没有。
我认为错误消息很清楚,EF Core DbContexts仅支持一次操作。另一个因素是内存缓存实现没有进行任何形式的锁定,因此用于创建缓存条目的lambda表达式可以被多个消费者并发地执行,这些消费者希望从缓存中读取。
明确地锁定是我认为可行的方法,有两种选择:
1. 在GetOrCreate方法周围,这意味着您可以保证仅运行一次EF Core查询,但是没有2个消费者能够同时从缓存中读取; 或
2. 在EF Core查询周围,这意味着您可以潜在地覆盖现有的缓存条目,但是消费者可以同时从缓存中读取。
我个人会选择选项2并使用SemaphoreSlim实例。

接受这个答案后,有一个几乎相同的包装,它在上下文操作中有一个锁定,我最终使用SemaphoreSlim对所有请求进行了限制。参考链接:https://github.com/damienbod/AspNetCoreLocalization/blob/f7e2c97da50efce3524813e1948e8cef4494c4d6/src/Localization.SqlLocalizer/DbStringLocalizer/SqlStringLocalizerFactory.cs#L146 - HardLuck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接