You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:41:30