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

.NET Core WebApi处理Range请求:优化CPU过载与避免多次查库

解决.NET Core WebApi视频Range请求的CPU高负载问题

这俩问题绝对是处理大BLOB+Range请求的典型坑,我来给你一步步拆解优化方案:

问题1:EF查询大varbinary导致CPU飙升

EF默认会把整个varbinary(MAX)字段加载到内存的byte[]里,25MB的文件每次都要完整序列化/反序列化,CPU不炸才怪。核心解决思路就是避免全量加载,只读取请求需要的字节范围,而且要用更高效的流式读取方式:

最优方案:用ADO.NET直接流式读取指定片段

跳过EF的ORM层,直接用SqlCommand配合SqlDataReader.GetStream()读取部分数据,全程不用把整个文件拉到内存里:

[HttpGet("{id}")]
public async Task<IActionResult> GetVideoSegment(int id)
{
    var rangeHeader = Request.Headers.Range.ToString();
    if (string.IsNullOrEmpty(rangeHeader))
    {
        return BadRequest("Range header is required for Safari compatibility");
    }

    // 先获取视频总长度(建议缓存这个值,后面说)
    var totalLength = await GetCachedVideoTotalLength(id);
    // 解析Range头,处理"bytes=0-1023"这类格式
    var (start, end) = ParseRange(rangeHeader, totalLength);
    var segmentLength = end - start + 1;

    using var conn = new SqlConnection(_configuration.GetConnectionString("Default"));
    await conn.OpenAsync();

    // SQL Server的SUBSTRING是1-based索引,所以start要+1
    var cmd = new SqlCommand(@"
        SELECT SUBSTRING(VideoData, @Start + 1, @Length) 
        FROM Videos 
        WHERE Id = @Id", conn);
    cmd.Parameters.AddWithValue("@Id", id);
    cmd.Parameters.AddWithValue("@Start", start);
    cmd.Parameters.AddWithValue("@Length", segmentLength);

    // 用SequentialAccess模式流式读取,避免加载全量数据
    using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
    if (await reader.ReadAsync())
    {
        var stream = reader.GetStream(0);
        var result = new FileStreamResult(stream, "video/mp4")
        {
            EnableRangeProcessing = true
        };
        result.Headers.ContentRange = new Microsoft.Net.Http.Headers.ContentRangeHeaderValue(start, end, totalLength);
        result.StatusCode = StatusCodes.Status206PartialContent;
        
        // 加缓存头,减少重复请求
        result.Headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
        {
            Public = true,
            MaxAge = TimeSpan.FromHours(24)
        };
        return result;
    }

    return NotFound();
}

// 辅助方法:解析Range头(简单实现,实际要兼容多范围等情况)
private (long Start, long End) ParseRange(string rangeHeader, long totalLength)
{
    var rangePart = rangeHeader.Replace("bytes=", "");
    var parts = rangePart.Split('-');
    long start = long.Parse(parts[0]);
    long end = parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]) 
        ? long.Parse(parts[1]) 
        : totalLength - 1;
    end = Math.Min(end, totalLength - 1);
    return (start, end);
}

// 缓存视频总长度,避免每次都查库
private async Task<long> GetCachedVideoTotalLength(int id)
{
    var cacheKey = $"VideoLength_{id}";
    if (_memoryCache.TryGetValue(cacheKey, out long length))
    {
        return length;
    }

    using var conn = new SqlConnection(_configuration.GetConnectionString("Default"));
    await conn.OpenAsync();
    var cmd = new SqlCommand("SELECT DATALENGTH(VideoData) FROM Videos WHERE Id = @Id", conn);
    cmd.Parameters.AddWithValue("@Id", id);
    length = (long)await cmd.ExecuteScalarAsync();
    
    // 缓存1天,视频更新时记得清空缓存
    _memoryCache.Set(cacheKey, length, TimeSpan.FromDays(1));
    return length;
}

备选:优化EF查询(不如ADO.NET高效)

如果不想完全抛弃EF,可以用FromSqlRaw执行片段查询,同时用AsEnumerable()避免EF把数据全加载到内存:

var segment = await _context.Videos
    .FromSqlRaw(@"SELECT SUBSTRING(VideoData, @Start + 1, @Length) AS VideoSegment FROM Videos WHERE Id = @Id",
        new SqlParameter("@Id", id),
        new SqlParameter("@Start", start),
        new SqlParameter("@Length", segmentLength))
    .Select(v => v.VideoSegment)
    .AsEnumerable() // 跳过EF的跟踪和内存加载
    .FirstOrDefaultAsync();

if (segment != null)
{
    using var ms = new MemoryStream(segment);
    return new FileStreamResult(ms, "video/mp4") { ... };
}

问题2:多Range请求重复查库导致持续高负载

iOS Safari这类客户端会拆分多个Range请求来加载视频,每次都查库肯定扛不住,核心思路是减少重复查询+卸载请求压力:

长期最优解:迁移视频到文件系统/对象存储

SQL Server根本不是为存大文件设计的!把视频转存到本地文件系统、Azure Blob Storage、AWS S3这类服务:

  • 这些存储原生支持Range请求,你的API只需要做鉴权,然后返回一个预签名URL给客户端,让客户端直接向存储服务请求视频片段。
  • 存储服务会自动处理缓存、负载均衡、Range请求,你的WebApi彻底解放CPU和数据库压力。

短期缓解:缓存热门视频片段

  • 用IMemoryCache或Redis缓存热门视频的常用片段(比如前几MB的内容,浏览器经常会请求),下次请求直接从缓存取,不用查库。
  • 视频总长度一定要缓存,这个值不会变,缓存过期时间设长一点。

必加:响应缓存头

给返回的响应添加Cache-Control、ETag头,让浏览器缓存已经请求过的片段,减少重复请求:

result.Headers.ETag = new Microsoft.Net.Http.Headers.EntityTagHeaderValue($"\"{id}-{GetVideoLastModified(id)}\"");

额外注意点

  • 一定要用流式处理,别把二进制数据加载到内存里,否则内存也会跟着飙升。
  • 处理Range请求要严格遵循HTTP规范:返回206状态码,正确设置Content-Range和Content-Length头。
  • 如果视频超过1GB,建议给SQL Server的VideoData字段开启FILESTREAM,但25MB的话普通varbinary(MAX)足够应付。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 09:29:01