在跨多个请求的无状态环境中保持对文件的锁定(Asp.Net 核心)
Maintaining a lock on files in a stateless environment across multiple requests (Asp.Net Core)
我正在编写 Asp.Net 核心应用程序(使用 RazerPages),uploads/downloads 文件。我有一个控件使用 AJAX 以块的形式上传文件。这些文件被上传到服务器上的一个子目录。子目录的名称是在页面加载时生成的 guid。
当我从我的控件中删除一个文件时,它会发送一个命令来删除服务器上的关联文件。问题是对于特别大的文件,删除似乎需要很长时间,但 GUI 不会等待响应(因此它认为文件已被删除)。如果我随后尝试再次上传相同的文件,我会收到 "Access Denied" 异常,因为该文件仍在被另一个请求使用...
我曾尝试在发生文件 IO 时使用互斥锁来锁定子目录,但由于某些原因,不同的请求似乎不使用相同的互斥锁。如果我使用静态单例互斥锁,它可以工作,但这意味着整个服务器一次只能有一个文件 uploaded/deleted。
如何为我当前使用的子目录创建互斥体,并在多个请求中识别它?
public class FileIOService : IFileService
{
public string RootDiectory { get; set; }
public void CreateDirectory(Guid id)
{
// Create the working directory if it doesn't exist
string path = Path.Combine(RootDiectory, id.ToString());
lock (id.ToString())
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
}
public void AppendToFile(Guid id, string fileName, Stream content)
{
try
{
CreateDirectory(id);
string fullPath = Path.Combine(RootDiectory, id.ToString(), fileName);
lock (id.ToString())
{
bool newFile = !File.Exists(fullPath);
using (FileStream stream = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
using (content)
{
content.CopyTo(stream);
}
}
}
}
catch (IOException ex)
{
throw;
}
}
public void DeleteFile(Guid id, string fileName)
{
string path = Path.Combine(RootDiectory, id.ToString(), fileName);
lock (id.ToString())
{
if (File.Exists(path))
{
File.Delete(path);
}
string dirPath = Path.Combine(RootDiectory, id.ToString());
DirectoryInfo dir = new DirectoryInfo(dirPath);
if (dir.Exists && !dir.GetFiles().Any())
{
Directory.Delete(dirPath, false);
}
}
}
public void DeleteDirectory(Guid id)
{
string path = Path.Combine(RootDiectory, id.ToString());
lock (id.ToString())
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
}
}
}
我最终没有使用 lock
,而是使用了显式创建的全局互斥锁。我创建了一个私有 ProtectWithMutex
方法,该方法将在受保护的代码块内执行操作:
/// <summary>
/// Performs the specified action. Only one action with the same Guid may be executing
/// at a time to prevent race-conditions.
/// </summary>
private void ProtectWithMutex(Guid id, Action action)
{
// unique id for global mutex - Global prefix means it is global to the machine
string mutexId = string.Format("Global\{{{0}}}" ,id);
using (var mutex = new Mutex(false, mutexId, out bool isNew))
{
var hasHandle = false;
try
{
try
{
//change the timeout here if desired.
int timeout = Timeout.Infinite;
hasHandle = mutex.WaitOne(timeout, false);
if (!hasHandle)
{
throw new TimeoutException("A timeout occured waiting for file to become available");
}
}
catch (AbandonedMutexException)
{
hasHandle = true;
}
TryAndRetry(action);
}
finally
{
if (hasHandle)
mutex.ReleaseMutex();
}
}
}
这阻止了多个请求同时尝试操作同一个目录,但我仍然遇到其他进程(Windows Explorer、Antivirus,我不太确定)正在抓取文件的问题在请求之间。为了解决这个问题,我创建了一个 TryAndRetry
方法,它会尝试一遍又一遍地执行相同的操作,直到成功(或直到失败太多次):
/// <summary>
/// Trys to perform the specified action a number of times before giving up.
/// </summary>
private void TryAndRetry(Action action)
{
int failedAttempts = 0;
while (true)
{
try
{
action();
break;
}
catch (IOException ex)
{
if (++failedAttempts > RetryCount)
{
throw;
}
Thread.Sleep(RetryInterval);
}
}
}
那时我所要做的就是用对 Protected
方法的调用替换我所有的 lock
块:
public void AppendToFile(Guid id, string fileName, Stream content)
{
CreateDirectory(id);
string dirPath = Path.Combine(RootDiectory, id.ToString());
string fullPath = Path.Combine(dirPath, fileName);
ProtectWithMutex(id, () =>
{
using (FileStream stream = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.None))
{
using (content)
{
content.CopyTo(stream);
}
}
});
}
public void DeleteFile(Guid id, string fileName)
{
string path = Path.Combine(RootDiectory, id.ToString(), fileName);
ProtectWithMutex(id, () =>
{
if (File.Exists(path))
{
File.Delete(path);
}
string dirPath = Path.Combine(RootDiectory, id.ToString());
DirectoryInfo dir = new DirectoryInfo(dirPath);
if (dir.Exists && !dir.GetFiles().Any())
{
Directory.Delete(dirPath, false);
}
});
}
我正在编写 Asp.Net 核心应用程序(使用 RazerPages),uploads/downloads 文件。我有一个控件使用 AJAX 以块的形式上传文件。这些文件被上传到服务器上的一个子目录。子目录的名称是在页面加载时生成的 guid。
当我从我的控件中删除一个文件时,它会发送一个命令来删除服务器上的关联文件。问题是对于特别大的文件,删除似乎需要很长时间,但 GUI 不会等待响应(因此它认为文件已被删除)。如果我随后尝试再次上传相同的文件,我会收到 "Access Denied" 异常,因为该文件仍在被另一个请求使用...
我曾尝试在发生文件 IO 时使用互斥锁来锁定子目录,但由于某些原因,不同的请求似乎不使用相同的互斥锁。如果我使用静态单例互斥锁,它可以工作,但这意味着整个服务器一次只能有一个文件 uploaded/deleted。
如何为我当前使用的子目录创建互斥体,并在多个请求中识别它?
public class FileIOService : IFileService
{
public string RootDiectory { get; set; }
public void CreateDirectory(Guid id)
{
// Create the working directory if it doesn't exist
string path = Path.Combine(RootDiectory, id.ToString());
lock (id.ToString())
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
}
public void AppendToFile(Guid id, string fileName, Stream content)
{
try
{
CreateDirectory(id);
string fullPath = Path.Combine(RootDiectory, id.ToString(), fileName);
lock (id.ToString())
{
bool newFile = !File.Exists(fullPath);
using (FileStream stream = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
using (content)
{
content.CopyTo(stream);
}
}
}
}
catch (IOException ex)
{
throw;
}
}
public void DeleteFile(Guid id, string fileName)
{
string path = Path.Combine(RootDiectory, id.ToString(), fileName);
lock (id.ToString())
{
if (File.Exists(path))
{
File.Delete(path);
}
string dirPath = Path.Combine(RootDiectory, id.ToString());
DirectoryInfo dir = new DirectoryInfo(dirPath);
if (dir.Exists && !dir.GetFiles().Any())
{
Directory.Delete(dirPath, false);
}
}
}
public void DeleteDirectory(Guid id)
{
string path = Path.Combine(RootDiectory, id.ToString());
lock (id.ToString())
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
}
}
}
我最终没有使用 lock
,而是使用了显式创建的全局互斥锁。我创建了一个私有 ProtectWithMutex
方法,该方法将在受保护的代码块内执行操作:
/// <summary>
/// Performs the specified action. Only one action with the same Guid may be executing
/// at a time to prevent race-conditions.
/// </summary>
private void ProtectWithMutex(Guid id, Action action)
{
// unique id for global mutex - Global prefix means it is global to the machine
string mutexId = string.Format("Global\{{{0}}}" ,id);
using (var mutex = new Mutex(false, mutexId, out bool isNew))
{
var hasHandle = false;
try
{
try
{
//change the timeout here if desired.
int timeout = Timeout.Infinite;
hasHandle = mutex.WaitOne(timeout, false);
if (!hasHandle)
{
throw new TimeoutException("A timeout occured waiting for file to become available");
}
}
catch (AbandonedMutexException)
{
hasHandle = true;
}
TryAndRetry(action);
}
finally
{
if (hasHandle)
mutex.ReleaseMutex();
}
}
}
这阻止了多个请求同时尝试操作同一个目录,但我仍然遇到其他进程(Windows Explorer、Antivirus,我不太确定)正在抓取文件的问题在请求之间。为了解决这个问题,我创建了一个 TryAndRetry
方法,它会尝试一遍又一遍地执行相同的操作,直到成功(或直到失败太多次):
/// <summary>
/// Trys to perform the specified action a number of times before giving up.
/// </summary>
private void TryAndRetry(Action action)
{
int failedAttempts = 0;
while (true)
{
try
{
action();
break;
}
catch (IOException ex)
{
if (++failedAttempts > RetryCount)
{
throw;
}
Thread.Sleep(RetryInterval);
}
}
}
那时我所要做的就是用对 Protected
方法的调用替换我所有的 lock
块:
public void AppendToFile(Guid id, string fileName, Stream content)
{
CreateDirectory(id);
string dirPath = Path.Combine(RootDiectory, id.ToString());
string fullPath = Path.Combine(dirPath, fileName);
ProtectWithMutex(id, () =>
{
using (FileStream stream = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.None))
{
using (content)
{
content.CopyTo(stream);
}
}
});
}
public void DeleteFile(Guid id, string fileName)
{
string path = Path.Combine(RootDiectory, id.ToString(), fileName);
ProtectWithMutex(id, () =>
{
if (File.Exists(path))
{
File.Delete(path);
}
string dirPath = Path.Combine(RootDiectory, id.ToString());
DirectoryInfo dir = new DirectoryInfo(dirPath);
if (dir.Exists && !dir.GetFiles().Any())
{
Directory.Delete(dirPath, false);
}
});
}