.Net Core 3.1中DelegatingHandler复用DbContext引发多线程冲突
解决方案:DbContext并发操作错误处理
问题根源
默认HttpClient在DI中是Singleton生命周期,你的自定义LtiUserApiAuthenticationHttpClientHandler会被绑定到这个Singleton实例上,因此Handler本身也会被当作Singleton对待。如果直接在Handler的构造函数中注入IUserService,该服务会被Handler长期持有,其生命周期被“提升”为Singleton——哪怕你的DbContext是Transient,只要IUserService变成Singleton,它内部的DbContext实例就会被多个并发请求共享,而EF Core的DbContext本身不是线程安全的,最终引发多线程操作同一个实例的错误。
正确解决步骤
1. 修改自定义Handler,通过IServiceScopeFactory创建作用域
在Handler中不要直接注入业务服务,而是注入IServiceScopeFactory,在SendAsync方法内部创建独立作用域,获取当前请求的Scoped服务实例:
public class LtiUserApiAuthenticationHttpClientHandler : DelegatingHandler { private readonly IServiceScopeFactory _scopeFactory; public LtiUserApiAuthenticationHttpClientHandler(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 创建独立作用域,保证每次请求获取到的服务是隔离的 using var scope = _scopeFactory.CreateScope(); var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); // 执行数据库查询 var targetUser = await userService.Get(...); // 这里添加你的请求认证逻辑,比如设置Header等 request.Headers.Add("Authorization", $"Bearer {targetUser.Token}"); return await base.SendAsync(request, cancellationToken); } }
2. 调整DI注册方式
- 将Handler注册为Transient
- 使用
IHttpClientFactory注册你的HttpClient,绑定Handler:
// Startup.cs 或 Program.cs(.NET 6+) services.AddTransient<LtiUserApiAuthenticationHttpClientHandler>(); services.AddHttpClient<ILtiUserApiClient, LtiUserApiClient>() .AddHttpMessageHandler<LtiUserApiAuthenticationHttpClientHandler>();
3. 确保业务服务和DbContext的生命周期配置正确
- 将
IUserService及其实现注册为Scoped(每个请求一个实例) - DbContext保持Transient或Scoped均可(推荐Scoped,更符合请求生命周期):
services.AddScoped<IUserService, UserService>(); services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default")), ServiceLifetime.Scoped); // 显式指定Scoped,默认也是Scoped
4. 排查异步代码中的阻塞操作
确认IUserService.Get方法内所有EF Core操作都使用异步API并正确await,禁止使用.Result、.Wait()等阻塞调用——这类操作会破坏线程上下文,导致DbContext被意外共享。
验证
重新用ApacheBench做并发测试(20请求、并发2),此时每个请求的数据库查询都会在独立的作用域内使用专属的DbContext实例,不会出现多线程冲突问题。
内容的提问来源于stack exchange,提问作者Chris Rockwell
相关产品推荐
相关产品推荐

