在跨多个请求的无状态环境中保持对文件的锁定(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);
        }
    });
}