SQL Server 中 VARBINARY(MAX)
字段读写大文件容易卡死
直接用
SqlDataReader.GetBytes()读取几百 MB 的 BLOB,内存暴涨、GC 压力大、甚至 OOM——这不是数据太大,是加载方式错了。SQL Server 默认把整个
VARBINARY(MAX)拉进内存,哪怕你只想要前 100 字节。 改用
SqlDataReader.GetStream(),它返回
SqlBytes.Stream,支持流式读取,内存占用恒定在几 KB 写入时别用
SqlCommand.Parameters.AddWithValue(),它会把整个
byte[]加载进内存;改用
SqlParameter显式指定
SqlDbType.VarBinary+
Size = -1,再传
Stream或
SqlBytes连接字符串必须加
Packet Size=8192(默认值),但若文件普遍 >1MB,可尝试
Packet Size=32767减少网络往返次数(注意:不是越大越好,TCP 层有 MTU 限制)
.NET 6+ 中用 FileStream
和 SqlDataReader.GetStream()
配合出错
常见报错:
InvalidOperationException: Invalid attempt to call GetStream on a closed DataReader。根本原因不是代码顺序写错了,而是
GetStream()返回的流和
SqlDataReader强绑定——只要 reader 关闭或移动到下一行,流就失效。 必须在
using (var reader = cmd.ExecuteReader())作用域内完成全部流操作,不能把 stream 存到外面再读 别用
CopyToAsync()直接丢给后台线程——除非你确保 reader 在整个 copy 过程中保持打开且位置不动(实际极难保证) 安全做法:用
reader.GetStream().CopyTo(fileStream)同步执行,或改用
GetFieldValueAsync<stream>(index)</stream>+
AsStream()(.NET 6+),它返回独立缓冲流
SQLite 的 BLOB
字段不支持流式读写,怎么避免内存爆炸
SQLite ADO.NET 驱动(如
Microsoft.Data.Sqlite)不提供类似
GetStream()的 API,
GetValue()或
GetBytes()必然全量加载。对 >50MB 文件,这很危险。 绕过 ORM,用原生 SQLite C API 的
sqlite3_blob_open()—— 通过
Microsoft.Data.Sqlite的
SqliteConnection.Handle获取底层句柄,再 P/Invoke 调用(需启用
UnmanagedCallersOnly支持) 更实用的折中:把大文件拆成固定块(如 1MB/chunk),存进带
chunk_index的子表,读取时按需拉取对应 chunk,拼接逻辑放在应用层 如果只是偶尔导出,接受短暂卡顿,至少加上
GC.Collect()+
GC.WaitForPendingFinalizers()在读完后立即释放大数组(仅限桌面端,别在服务器上滥用)
从数据库 BLOB 写文件时,FileMode.Create
和 FileMode.CreateNew
选哪个
看似是文件系统问题,实则影响并发安全。比如多个线程同时写同一路径,用
Create会互相覆盖;用
CreateNew则第二个线程直接抛
IOException。 如果目标路径由业务唯一生成(如
Guid.NewGuid().ToString("N") + ".pdf"),用 CreateNew更稳妥,能暴露重复写入逻辑缺陷 如果路径固定(如配置的临时目录),必须加文件锁:
new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None),否则 Windows 下可能报“文件正被另一进程使用” 别依赖
File.Exists()+
CreateNew组合做判断——存在竞态:检查完存在,写入前已被删,导致失败。直接
CreateNew尝试,捕获异常处理更可靠
真正麻烦的从来不是“怎么转”,而是“谁在什么时候持有哪段内存”。BLOB 操作里最易忽略的是流生命周期和数据库连接状态的耦合——一个没关 reader,另一个线程已开始读 stream,这种 bug 往往只在高负载时复现。
