C# Windows应用从SQL Server下载700MB-2GB视频时抛出OutOfMemoryException
大体积视频文件从SQL Server下载的OOM问题优化方案
问题根因
当前代码抛出System.OutOfMemoryException的核心原因是:
- 代码通过
(byte[])reader["PATHLINK"]一次性把700MB-2GB的完整视频二进制数据全部加载到进程内存中,后续调用File.WriteAllBytes时还需要再次拷贝全量字节,全程没有分块流式处理。 - 即使是64位进程,.NET运行时对超大连续字节数组的分配也很容易因为内存碎片触发分配失败;如果是32位编译的程序,单进程最大可用寻址内存仅不到2GB,下载接近2GB的文件时必然触发内存溢出。
核心优化方案
必须将“全量加载字节再写盘”的逻辑改造为分块流式读写,全程仅使用固定大小的内存缓冲区,内存占用不会随文件大小增长。
关键改造点
- 执行数据读取时必须传入
CommandBehavior.SequentialAccess参数,启用SQL Server大字段的顺序流式返回模式,避免一次性加载全量BLOB数据。 - 严格按照SELECT查询返回的列顺序读取字段,防止顺序访问模式下的读取错误。
- 使用固定大小的内存缓冲区循环读取数据库流、写入本地文件流,替代一次性读取全量字节数组的逻辑。
- 显式在代码中配置连接、命令超时时间,不要仅依赖SSMS的配置修改;连接字符串中添加
Connection Timeout=30配置,和此前SSMS中调整的超时参数保持一致,避免大文件传输过程中连接提前断开。
改造后的核心参考代码
private async Task DownloadChatVideo(string id, CancellationToken ct = default) { // 连接字符串中同步配置Connection Timeout=30,匹配此前的超时调整 using (SqlConnection cn = new SqlConnection(GetConnectionString())) { // 仅查询必要字段,保证列读取顺序和代码逻辑匹配 string query = @"SELECT VDONAME, Extension, SentDateTime, PATHLINK FROM CHATVIDEO WHERE ID = @id"; using (SqlCommand cmd = new SqlCommand(query, cn)) { cmd.Parameters.Add("@id", SqlDbType.NVarChar).Value = id; // 匹配大文件传输的超时时长,根据实际网络环境调整 cmd.CommandTimeout = 300; await cn.OpenAsync(ct); // 启用顺序访问模式读取大二进制字段 using (SqlDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess, ct)) { if (await reader.ReadAsync(ct)) { // 按列顺序读取非大字段 var name = reader["VDONAME"].ToString(); var extn = reader["Extension"].ToString(); var sendTime = Convert.ToDateTime(reader["SentDateTime"]).ToString("yyyyMMddHHmmss"); var targetFile = Path.Combine(savePath, $"{name.Replace(extn, "")}{id}{sendTime}{extn}"); var tempFile = targetFile + ".tmp"; // 固定400KB大小的缓冲区,全程内存占用稳定在几MB级别 const int bufferSize = 409600; byte[] buffer = new byte[bufferSize]; using (FileStream writeStream = new FileStream( tempFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true)) { // 最后读取顺序排在最后的大二进制字段 using (Stream dbStream = reader.GetStream(reader.GetOrdinal("PATHLINK"))) { int readLength; while ((readLength = await dbStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) { await writeStream.WriteAsync(buffer, 0, readLength, ct); } await writeStream.FlushAsync(ct); } } // 写入完成后再替换正式文件,避免残缺文件留存 if (File.Exists(targetFile)) File.Delete(targetFile); File.Move(tempFile, targetFile); currentSavedFile = targetFile; } } } } }
注意事项
- 严格遵守
SequentialAccess模式的读取规则:必须和SELECT语句返回的列顺序保持一致,不能跳列读取,否则会抛出读取异常。 - 将项目编译目标调整为64位/任何CPU模式,不要发布32位版本,避免受单进程2GB内存上限的限制。
- 不要使用
byte[] data = (byte[])reader[col]的方式读取大BLOB字段,即使机器物理内存充足,.NET对超大连续内存块的分配也容易因为内存碎片失败。 - 写入文件时先写临时文件、完成全量写入后再重命名为目标文件名,防止程序中断时留下损坏的无效文件。
内容的提问来源于stack exchange,提问作者Rajasekaran Bose
相关产品推荐
相关产品推荐

