最近在使用ASP.NET Core的時候出現了一個奇怪的問題。在一個Controller上使用了一個ActionFilter之後經常出現EF報錯。 這個異常說Context在完成前一個操作的時候第二個操作依據開始。這個錯誤還不是每次都會出現,只有在併發強的時候出現,基本可以判斷跟多線程有關係。看一下 ...
最近在使用ASP.NET Core的時候出現了一個奇怪的問題。在一個Controller上使用了一個ActionFilter之後經常出現EF報錯。
InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
這個異常說Context在完成前一個操作的時候第二個操作依據開始。這個錯誤還不是每次都會出現,只有在併發強的時候出現,基本可以判斷跟多線程有關係。看一下代碼:
public static class ServiceCollectionExt
{
public static void AddAgileConfigDb(this IServiceCollection sc)
{
sc.AddScoped<ISqlContext, AgileConfigDbContext>();
}
}
[TypeFilter(typeof(BasicAuthenticationAttribute))]
[Route("api/[controller]")]
public class ConfigController : Controller
{
private readonly IConfigService _configService;
private readonly ILogger _logger;
public ConfigController(IConfigService configService, ILoggerFactory loggerFactory)
{
_configService = configService;
_logger = loggerFactory.CreateLogger<ConfigController>();
}
// GET: api/<controller>
[HttpGet("app/{appId}")]
public async Task<List<ConfigVM>> Get(string appId)
{
var configs = await _configService.GetByAppId(appId);
var vms = configs.Select(c => {
return new ConfigVM() {
Id = c.Id,
AppId = c.AppId,
Group = c.Group,
Key = c.Key,
Value = c.Value,
Status = c.Status
};
});
_logger.LogTrace($"get app {appId} configs .");
return vms.ToList();
}
}
代碼非常簡單,DbContext使用Scope生命周期;Controller里只有一個Action,裡面只有一個訪問資料庫的地方。怎麼會造成多線程訪問Context的錯誤的呢?於是把目光移到BasicAuthenticationAttribute這個Attribute。
public class BasicAuthenticationAttribute : ActionFilterAttribute
{
private readonly IAppService _appService;
public BasicAuthenticationAttribute(IAppService appService)
{
_appService = appService;
}
public async override void OnActionExecuting(ActionExecutingContext context)
{
if (!await Valid(context.HttpContext.Request))
{
context.HttpContext.Response.StatusCode = 403;
context.Result = new ContentResult();
}
}
public async Task<bool> Valid(HttpRequest httpRequest)
{
var appid = httpRequest.Headers["appid"];
if (string.IsNullOrEmpty(appid))
{
return false;
}
var app = await _appService.GetAsync(appid);
if (app == null)
{
return false;
}
if (string.IsNullOrEmpty(app.Secret))
{
//如果沒有設置secret則直接通過
return true;
}
var authorization = httpRequest.Headers["Authorization"];
if (string.IsNullOrEmpty(authorization))
{
return false;
}
if (!app.Enabled)
{
return false;
}
var sec = app.Secret;
var txt = $"{appid}:{sec}";
var data = Encoding.UTF8.GetBytes(txt);
var auth = "Basic " + Convert.ToBase64String(data);
return auth == authorization;
}
}
BasicAuthenticationAttribute的代碼也很簡單,Attribute註入了一個Service並且重寫了OnActionExecuting方法,在方法里對Http請求進行Basic認證。這裡也出現了一次數據查詢,但是已經都加上了await。咋一看好像沒什麼問題,一個Http請求進來的時候,首先會進入這個Filter對其進行Basic認證,如果失敗返回403碼,如果成功則進入真正的Action方法繼續執行。如果是這樣的邏輯,不可能出現兩次EF的操作同時執行。繼續查找問題,點開ActionFilterAttribute的元數據:
public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IAsyncResultFilter, IOrderedFilter, IResultFilter
{
protected ActionFilterAttribute();
//
public int Order { get; set; }
//
public virtual void OnActionExecuted(ActionExecutedContext context);
//
public virtual void OnActionExecuting(ActionExecutingContext context);
//
[DebuggerStepThrough]
public virtual Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next);
//
public virtual void OnResultExecuted(ResultExecutedContext context);
//
public virtual void OnResultExecuting(ResultExecutingContext context);
//
[DebuggerStepThrough]
public virtual Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
}
這玩意這麼看著跟以前有點不一樣啊,除了原來的4個方法,多了2個Async結尾的方法。到了這裡其實心裡已經有數了。這裡應該重寫OnResultExecutionAsync,因為我們的Action方法是個非同步方法。改一下BasicAuthenticationAttribute,重寫OnResultExecutionAsync方法:
public class BasicAuthenticationAttribute : ActionFilterAttribute
{
private readonly IAppService _appService;
public BasicAuthenticationAttribute(IAppService appService)
{
_appService = appService;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!await Valid(context.HttpContext.Request))
{
context.HttpContext.Response.StatusCode = 403;
context.Result = new ContentResult();
}
await base.OnActionExecutionAsync(context, next);
}
public async Task<bool> Valid(HttpRequest httpRequest)
{
var appid = httpRequest.Headers["appid"];
if (string.IsNullOrEmpty(appid))
{
return false;
}
var app = await _appService.GetAsync(appid);
if (app == null)
{
return false;
}
if (string.IsNullOrEmpty(app.Secret))
{
//如果沒有設置secret則直接通過
return true;
}
var authorization = httpRequest.Headers["Authorization"];
if (string.IsNullOrEmpty(authorization))
{
return false;
}
if (!app.Enabled)
{
return false;
}
var sec = app.Secret;
var txt = $"{appid}:{sec}";
var data = Encoding.UTF8.GetBytes(txt);
var auth = "Basic " + Convert.ToBase64String(data);
return auth == authorization;
}
}
修改完後經過併發測試,EF報錯的問題得到瞭解決。
再來解釋下這個問題是如何造成的:一開始BasicAuthenticationAttribute是framework版本的ASP.NET MVC遷移過來的,按照慣例重寫了OnActionExecuting。其中註入的service裡面的方法是非同步的,儘管標記了await,但是這並沒有什麼卵用,因為框架在調用OnActionExecuting的時候並不會在前面加上await來等待這個方法。於是一個重寫了OnActionExecuting的Filter配合一個非同步的Action執行的時候並不會如預設的一樣先等待OnActionExecuting執行完之後再執行action。如果OnActionExecuting里出現非同步方法,那這個非同步方法很可能跟Action里的非同步方法同時執行,這樣在高併發的時候就出現EF的Context被多線程操作的異常問題。這裡其實還是一個老生常談的問題,就是儘量不要在同步方法內調用非同步方法,這樣很容易出現多線程的問題,甚至出現死鎖。
ASP.NET Core已經全面擁抱非同步,與framework版本有了很大的差異還是需要多多註意。看來這個Core版本的ActionFilter還得仔細研究研究,於是上微軟官網查了查有這麼一段:
Implement either the synchronous or the async version of a filter interface, not both. The runtime checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's method(s). If both asynchronous and synchronous interfaces are implemented in one class, only the async method is called. When using abstract classes like ActionFilterAttribute, override only the synchronous methods or the asynchronous method for each filter type.
就是說對於filter interface要麼實現同步版本的方法,要麼實現非同步版本的方法,不要同時實現。運行時會首先看非同步版本的方法有沒有實現,如果實現則調用。如果沒有則調用同步版本。如果同步版本跟非同步版本的方法都同時實現了,則只會調用非同步版本的方法。當使用抽象類,比如ActionFilterAttribute,只需重寫同步方法或者非同步方法其中一個。