.NET 7 REST API自动缩放时单例类异常问题求助
多实例场景下适配单例类操作共享目录的解决方案
问题根源
你当前的代码在启动时直接实例化Engine并注册为单例,多实例部署时每个API实例都会拥有独立的Engine实例。当它们同时操作共享的wwwroot目录(比如挂载了同一个网络存储)时,会引发文件访问冲突、操作互斥问题;如果是本地存储,还会出现各实例数据不一致的情况。
具体解决方案
1. 引入分布式锁控制共享资源访问
针对共享目录的操作,必须通过分布式锁保证同一时间只有一个实例执行文件操作。可以使用基于Redis的分布式锁(.NET中可通过StackExchange.Redis或自定义实现分布式锁)。
修改Engine类,在关键操作(删除、加载文件夹)前后加分布式锁:
public class Engine { private readonly ILogger _logger; private readonly IDistributedLock _distributedLock; private const string LockKey = "wwwroot-folder-operation-lock"; public Engine(ILoggerFactory loggerFactory, IDistributedLock distributedLock) { _logger = loggerFactory.CreateLogger<Engine>(); _distributedLock = distributedLock; } public void DeleteFolder(string folderPath) { using var lockHandle = _distributedLock.TryAcquireLock(LockKey, TimeSpan.FromSeconds(30)); if (lockHandle == null) { _logger.LogWarning("获取分布式锁失败,无法执行文件夹删除操作"); throw new InvalidOperationException("当前有其他实例正在操作目录,请稍后重试"); } // 执行实际删除逻辑 Directory.Delete(Path.Combine("wwwroot", folderPath), recursive: true); } public void LoadFolder(string folderPath, Stream content) { using var lockHandle = _distributedLock.TryAcquireLock(LockKey, TimeSpan.FromSeconds(30)); if (lockHandle == null) { _logger.LogWarning("获取分布式锁失败,无法执行文件夹加载操作"); throw new InvalidOperationException("当前有其他实例正在操作目录,请稍后重试"); } // 执行实际加载/写入逻辑 var targetPath = Path.Combine("wwwroot", folderPath); Directory.CreateDirectory(targetPath); using var fileStream = new FileStream(Path.Combine(targetPath, "content.dat"), FileMode.Create); content.CopyTo(fileStream); } }
注册分布式锁服务(以Redis为例):
// 注册StackExchange.Redis builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); }); // 注册分布式锁实现 builder.Services.AddSingleton<IDistributedLock, RedisDistributedLock>(); // 注册Engine为单例(让DI容器负责实例化,避免进程间独立实例) builder.Services.AddSingleton<Engine>();
2. 统一使用共享存储
确保所有API实例都指向同一个共享存储(比如NAS、云对象存储如Azure Blob、AWS S3等),替换本地wwwroot目录。云存储服务本身会处理多客户端访问的一致性问题,无需额外实现锁逻辑。
比如改用Azure Blob Storage的实现:
public class Engine { private readonly ILogger _logger; private readonly BlobServiceClient _blobServiceClient; public Engine(ILoggerFactory loggerFactory, BlobServiceClient blobServiceClient) { _logger = loggerFactory.CreateLogger<Engine>(); _blobServiceClient = blobServiceClient; } public async Task DeleteFolderAsync(string folderPrefix) { var containerClient = _blobServiceClient.GetBlobContainerClient("wwwroot-container"); await foreach (var blob in containerClient.GetBlobsAsync(prefix: folderPrefix)) { await containerClient.DeleteBlobAsync(blob.Name); } } public async Task LoadFolderAsync(string folderPrefix, Stream content) { var containerClient = _blobServiceClient.GetBlobContainerClient("wwwroot-container"); var blobClient = containerClient.GetBlobClient($"{folderPrefix}/content.dat"); await blobClient.UploadAsync(content, overwrite: true); } }
3. 修正单例注册方式
你当前提前实例化Engine的方式,会导致每个进程创建独立的实例。正确的做法是让DI容器负责实例化,保证进程内单例的同时,配合分布式锁或共享存储解决跨进程问题:
// 错误方式:提前实例化,每个进程会生成独立的Engine实例 // Engine _engine = new Engine(loggerFactory); // builder.Services.AddSingleton<Engine>(_engine); // 正确方式:由DI容器管理实例化进程内单例 builder.Services.AddSingleton<Engine>();
额外注意事项
- 操作文件/存储时要捕获
IOException、网络异常等,避免因锁超时、网络波动导致服务崩溃。 - 如果业务允许各实例数据独立,可配置各实例使用本地wwwroot,但这种场景不适用于需要数据一致的业务。
内容的提问来源于stack exchange,提问作者user2030504
相关产品推荐
相关产品推荐

