ASP.NET Core自定义路由中间件并发数据库操作异常排查
我来帮你拆解一下你遇到的这两个异常,它们其实是同一个核心问题导致的连锁反应——DbContext的线程安全缺陷,结合你自定义路由中间件的实现场景,具体原因和解决办法如下:
为什么会出现这两个异常?
1. InvalidOperationException:DbContext线程不安全
ASP.NET Core的DbContext设计为非线程安全的,它的所有实例成员(比如DbSet、数据库连接操作)都不支持并发访问。当多个并发请求(比如多标签页同时刷新)尝试使用同一个DbContext实例执行数据库操作时,就会触发这个异常。
你的场景里,问题很可能出在服务生命周期不匹配:
- 自定义路由中间件(
RouteCustom)是Singleton生命周期(ASP.NET Core中间件默认都是全局单例); - 如果你的
IUnitOfWork或者IServiceConfig被错误注册为Singleton,那么它们依赖的DbContext也会变成全局单例,被所有并发请求共享,直接导致线程安全冲突。
2. MySqlException:DataReader未关闭
这个是第一个问题的衍生异常:当一个请求通过DbContext打开了DataReader读取数据时,同一个数据库连接被另一个并发请求再次使用,而MySql默认不允许同一连接同时存在多个活跃的DataReader,因此抛出该错误。就算开启多活跃结果集(MARS),也只是缓解表象,无法解决DbContext线程不安全的核心问题。
合理的实现方案
1. 修正服务生命周期配置
确保所有依赖DbContext的服务都注册为Scoped(每个请求一个实例),绝对不能是Singleton:
// 正确注册DbContext(默认就是Scoped,保持即可) services.AddDbContext<DbContext>(options => options.UseMySQL(configuration.GetConnectionString("ConnectionClient"))); // 注册IUnitOfWork为Scoped services.AddScoped<IUnitOfWork, UnitOfWork>(); // 你的IServiceConfig已经是Scoped,保持不变 services.AddScoped<IServiceConfig, ServiceConfig>();
2. 确保中间件正确获取Scoped服务
因为路由中间件是Singleton,绝对不能在它的构造函数中注入Scoped服务(会导致捕获某个请求的Scoped实例,后续请求复用它)。你当前在RouteAsync和GetVirtualPath中通过context.HttpContext.RequestServices获取IServiceConfig的方式是正确的,继续保持——HttpContext.RequestServices是当前请求的服务范围,从中获取的Scoped服务会自动绑定到当前请求,不会被共享。
3. 添加路由规则缓存(关键优化)
路由规则不会频繁变化,每次请求都查数据库不仅性能低下,还会加剧并发冲突。建议把路由规则缓存起来,比如使用IMemoryCache:
第一步:在IServiceConfig中集成缓存
public class ServiceConfig : IServiceConfig { private readonly IUnitOfWork _unitOfWork; private readonly IMemoryCache _cache; private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(30); // 30分钟刷新一次 public ServiceConfig(IUnitOfWork unitOfWork, IMemoryCache cache) { _unitOfWork = unitOfWork; _cache = cache; } public async Task<IEnumerable<RouteRule>> GetRouteRulesAsync() { // 先从缓存获取,不存在再查数据库 return await _cache.GetOrCreateAsync("RouteRules", async entry => { entry.AbsoluteExpirationRelativeToNow = _cacheExpiration; return await _unitOfWork.RouteRules.GetAllAsync(); }); } }
第二步:注册缓存服务
在ConfigureServices中添加:
services.AddMemoryCache();
4. 可选:开启MySql多活跃结果集(MARS)
如果还有零星的DataReader异常,可以在连接字符串中开启MARS(需要确保MySql Connector/NET版本支持,一般5.7+版本都支持):
Server=yourserver;Database=yourdb;Uid=user;Pwd=pass;MultipleActiveResultSets=True;
但记住,这只是辅助手段,核心问题还是要通过正确的服务生命周期和缓存来解决。
额外建议
如果你的路由规则需要动态更新(比如后台修改后立即生效),可以添加一个手动刷新缓存的接口,比如:
[ApiController] [Route("api/[controller]")] public class RouteCacheController : ControllerBase { private readonly IMemoryCache _cache; public RouteCacheController(IMemoryCache cache) { _cache = cache; } [HttpPost("refresh")] public IActionResult RefreshRouteCache() { _cache.Remove("RouteRules"); return Ok("路由缓存已刷新"); } }
内容的提问来源于stack exchange,提问作者John Mathison

