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

ASP.NET+EF关联查询部署后因数据量增大触发API超时故障排查

如何解决MariaDB数据累积导致的API查询超时问题?

初始阶段API运行正常,但随着MariaDB数据库中数据不断累积,活动参与者无法获取其研习班数据(如签到记录、历史场次),API请求因超时失败。部署一段时间后,管理员仪表盘API也出现同样的超时问题。

参与者API原代码

[HttpGet]
[Authorize(Roles = "Admin,Participant")]
[ValidateReferrerAttribute]
public async Task<ParticipantSessionDto> GetSessions()
{
    if (User.Identity is { IsAuthenticated: true })
    {
        var configuration = new MapperConfiguration(
            cfg => cfg.CreateProjection<Participant, ParticipantSessionDto>());

        return await _entities
            .AsNoTracking()
            .Include(x => x.CheckIns)
                .ThenInclude(x => x.Session)
            .Include(x => x.Events)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Event)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Speakers)
                .ThenInclude(x => x.Avatar)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Materials)
            .ProjectTo<ParticipantSessionDto>(configuration)
            .FirstAsync(x => x.UserName == User.Identity.Name);
    }

    return null;
}

超时异常日志

MySqlConnector.MySqlException (0x80004005): The Command Timeout expired before the operation completed.
     ---&gt; System.Net.Sockets.SocketException (125): Operation canceled
       at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
       at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
       at MySqlConnector.Protocol.Serialization.SocketByteHandler.DoReadBytesAsync(Memory`1 buffer) in /_/src/MySqlConnector/Protocol/Serialization/SocketByteHandler.cs:line 109
       at MySqlConnector.Protocol.Serialization.SocketByteHandler.DoReadBytesAsync(Memory`1 buffer) in /_/src/MySqlConnector/Protocol/Serialization/SocketByteHandler.cs:line 109
       at MySqlConnector.Protocol.Serialization.BufferedByteReader.ReadBytesAsync(IByteHandler byteHandler, ArraySegment`1 buffer, Int32 totalBytesToRead, IOBehavior ioBehavior) in /_/src/MySqlConnector/Protocol/Serialization/BufferedByteReader.cs:line 34
       at MySqlConnector.Protocol.Serialization.ProtocolUtility.<ReadPacketAfterHeader>g__AddContinuation|2_0(ValueTask`1 payloadBytesTask, Int32 payloadLength, ProtocolErrorBehavior protocolErrorBehavior, Exception packetOutOfOrderException) in /_/src/MySqlConnector/Protocol/Serialization/ProtocolUtility.cs:line 434
       at MySqlConnector.Protocol.Serialization.ProtocolUtility.<DoReadPayloadAsync>g__AddContinuation|5_0(ValueTask`1 readPacketTask, BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func`1 getNextSequenceNumber, ArraySegmentHolder`1 previousPayloads, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior) in /_/src/MySqlConnector/Protocol/Serialization/ProtocolUtility.cs:line 480
       at MySqlConnector.Core.ServerSession.ReceiveReplyAsyncAwaited(ValueTask`1 task) in /_/src/MySqlConnector/Core/ServerSession.cs:line 956
       at MySqlConnector.Core.ResultSet.ReadResultSetHeaderAsync(IOBehavior ioBehavior) in /_/src/MySqlConnector/Core/ResultSet.cs:line 133
       at MySqlConnector.MySqlDataReader.ActivateResultSet(CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlDataReader.cs:line 108
       at MySqlConnector.MySqlDataReader.CreateAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary`2 cachedProcedures, IMySqlCommand command, CommandBehavior behavior, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlDataReader.cs:line 456
       at MySqlConnector.Core.CommandExecutor.ExecuteReaderAsync(IReadOnlyList`1 commands, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/CommandExecutor.cs:line 56
       at MySqlConnector.MySqlCommand.ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlCommand.cs:line 331
       at MySqlConnector.MySqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlCommand.cs:line 323
       at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
       at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
       at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
       at Pomelo.EntityFrameworkCore.MySql.Storage.Internal.MySqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
       at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
       at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
       at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
       at EventManager.Server.Controllers.ParticipantController.GetSessions() in C:\Users\CMSB\Desktop\EventManager\EventManager.Server\Controllers\ParticipantController.cs:line 173

仪表盘API原代码

[HttpGet]
[Authorize(Roles = "Admin")]
[ValidateReferrerAttribute]
public async Task<Event> GetForDashboard(Guid id)
{
    if (await _entities.AnyAsync(x => x.EventId == id))
    {
        var data = await _entities
            .AsNoTracking()
            .Include(x => x.CheckIns)
                .ThenInclude(x => x.Participant)
            .Include(x => x.Participants)
                .ThenInclude(x => x.Feedbacks)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Speakers)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Participants)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.CheckIns)
                .ThenInclude(x => x.Participant)
            .FirstAsync(x => x.EventId == id);

        return data;
    }

    return null;
}

解决方案

1. 优化EF Core查询逻辑

  • 提前过滤数据:将FirstAsync(x => x.UserName == ...)的过滤条件提前到Where子句中,让数据库先筛选出目标用户的数据,再加载关联表,避免拉取全表数据后再过滤。
  • 避免过度Include:检查DTO或返回实体实际需要的字段,只加载必要的关联数据。比如如果ParticipantSessionDto不需要Speaker的Avatar,就去掉对应的ThenInclude(x => x.Avatar),减少关联查询的复杂度。
  • 复用Mapper配置:不要在每次请求时创建新的MapperConfiguration,将其注册为全局单例(比如在Program.cs中配置AutoMapper),减少重复初始化的开销。
  • 减少数据库请求次数:仪表盘API中先用AnyAsync再用FirstAsync会导致两次数据库查询,直接改用FirstOrDefaultAsync即可完成一次查询。

优化后的参与者API示例

[HttpGet]
[Authorize(Roles = "Admin,Participant")]
[ValidateReferrerAttribute]
public async Task<ParticipantSessionDto> GetSessions()
{
    if (User.Identity is { IsAuthenticated: true })
    {
        var userName = User.Identity.Name;
        // 复用全局AutoMapper配置
        return await _entities
            .AsNoTracking()
            .Where(x => x.UserName == userName) // 提前过滤数据
            .Include(x => x.CheckIns)
                .ThenInclude(x => x.Session)
            .Include(x => x.Events)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Event)
            // 只保留DTO需要的关联
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Speakers)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Materials)
            .ProjectTo<ParticipantSessionDto>(_mapper.ConfigurationProvider)
            .FirstAsync();
    }

    return null;
}

优化后的仪表盘API示例

[HttpGet]
[Authorize(Roles = "Admin")]
[ValidateReferrerAttribute]
public async Task<Event> GetForDashboard(Guid id)
{
    // 一次查询完成判断与数据获取
    return await _entities
        .AsNoTracking()
        .Where(x => x.EventId == id)
        .Include(x => x.CheckIns)
            .ThenInclude(x => x.Participant)
        .Include(x => x.Participants)
            .ThenInclude(x => x.Feedbacks)
        .Include(x => x.Sessions)
            .ThenInclude(x => x.Speakers)
        .Include(x => x.Sessions)
            .ThenInclude(x => x.Participants)
        .Include(x => x.Sessions)
            .ThenInclude(x => x.CheckIns)
            .ThenInclude(x => x.Participant)
        .FirstOrDefaultAsync();
}

2. 数据库层面优化

  • 添加索引:给查询过滤字段和外键字段添加索引,例如:
    • Participant表的UserName字段
    • Event表的EventId字段
    • 关联表的外键(如CheckIns.ParticipantId、Sessions.EventId等)
      索引可以大幅提升查询和关联的速度,避免全表扫描。
  • 分析执行计划:使用MariaDB的EXPLAIN命令分析EF Core生成的SQL语句,定位全表扫描、关联效率低的环节,针对性优化。
  • 调整数据库超时配置:临时增大查询超时时间(比如在DbContext配置中设置CommandTimeout(60)),但这只是临时方案,核心还是优化查询本身。

3. 拆分复杂查询

如果关联表过多,一次查询拉取的数据量过大,可以将大查询拆分为多个小查询,分别获取主实体和关联数据,再在内存中组装结果。例如先查Participant,再单独查其CheckIns、Sessions,最后合并到DTO中,避免一次生成复杂的多表关联SQL。

内容的提问来源于stack exchange,提问作者Ibrahim Timimi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 06:40:18