如何从Blob解压大Zip文件且不将全部内容加载到内存?
从Azure Blob解压大Zip文件避免内存溢出的解决方案
原代码中,Azure Blob的OpenReadAsync返回的是不可查找的网络流,而ZipArchive要求流支持随机访问(CanSeek = true),因此它会自动将整个流复制到内存中的MemoryStream,导致处理4G级大文件时出现内存溢出异常。以下是两种可行的解决方案:
方案1:先将Blob下载到本地临时文件再解压
这种方式将Blob数据存储在本地磁盘而非内存,内存占用极低,是处理超大文件的首选方案。
// 下载Blob到本地临时文件 string tempFilePath = Path.GetTempFileName(); await blobClient.DownloadToAsync(tempFilePath); // 基于本地文件流创建ZipArchive(文件流原生支持Seek,无需加载全量数据到内存) using (FileStream fileStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read)) using (ZipArchive zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read)) { foreach (ZipArchiveEntry entry in zipArchive.Entries) { var destinationFilePath = $@"c:\temp\Unzipfiles-zip\{entry.FullName}"; // 提前创建目标文件的父目录,避免路径不存在异常 Directory.CreateDirectory(Path.GetDirectoryName(destinationFilePath)); using (Stream entryStream = entry.Open()) using (FileStream targetFileStream = new FileStream(destinationFilePath, FileMode.Create)) { await entryStream.CopyToAsync(targetFileStream).ConfigureAwait(false); } } } // 解压完成后清理临时文件 File.Delete(tempFilePath);
方案2:使用自定义可查找Blob流(无需完整下载)
如果对磁盘占用有严格要求,可以通过自定义流包装器,利用Azure Blob的范围读取能力实现可查找流,避免完整下载Blob。
自定义可查找Blob流实现
public class SeekableBlobStream : Stream { private readonly BlobClient _blobClient; private readonly long _blobTotalLength; private long _currentPosition; public SeekableBlobStream(BlobClient blobClient) { _blobClient = blobClient; _blobTotalLength = _blobClient.GetProperties().Value.ContentLength; _currentPosition = 0; } public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; public override long Length => _blobTotalLength; public override long Position { get => _currentPosition; set => _currentPosition = Math.Clamp(value, 0, _blobTotalLength); } public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (_currentPosition >= _blobTotalLength) return 0; long readEndPosition = Math.Min(_currentPosition + count, _blobTotalLength); using (var tempStream = new MemoryStream()) { await _blobClient.DownloadRangeToAsync(tempStream, new HttpRange(_currentPosition, readEndPosition - _currentPosition), cancellationToken: cancellationToken); tempStream.Position = 0; int bytesRead = await tempStream.ReadAsync(buffer, offset, count, cancellationToken); _currentPosition += bytesRead; return bytesRead; } } public override long Seek(long offset, SeekOrigin origin) { switch (origin) { case SeekOrigin.Begin: _currentPosition = offset; break; case SeekOrigin.Current: _currentPosition += offset; break; case SeekOrigin.End: _currentPosition = _blobTotalLength + offset; break; } _currentPosition = Math.Clamp(_currentPosition, 0, _blobTotalLength); return _currentPosition; } public override void Flush() => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); }
使用自定义流的解压代码
using (var seekableStream = new SeekableBlobStream(blobClient)) using (ZipArchive zipArchive = new ZipArchive(seekableStream, ZipArchiveMode.Read)) { foreach (ZipArchiveEntry entry in zipArchive.Entries) { var destinationFilePath = $@"c:\temp\Unzipfiles-zip\{entry.FullName}"; Directory.CreateDirectory(Path.GetDirectoryName(destinationFilePath)); using (Stream entryStream = entry.Open()) using (FileStream targetFileStream = new FileStream(destinationFilePath, FileMode.Create)) { await entryStream.CopyToAsync(targetFileStream).ConfigureAwait(false); } } }
注意:该方案会增加Blob的请求次数,需要在内存占用和请求性能之间做权衡。
内容的提问来源于stack exchange,提问作者Youxu
相关产品推荐
相关产品推荐

