.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
相关产品推荐
相关产品推荐

