跨多个请求在无状态环境中保持文件锁定(Asp.Net Core)

时间:2018-10-10 19:11:42

标签: c# asp.net-core file-io mutex

我正在编写一个Asp.Net Core应用程序(带有RazerPages),用于上载/下载文件。我有一个使用AJAX上载文件的控件。文件将上传到服务器上的子目录。子目录的名称是页面加载时生成的Guid。

当我从控件中删除文件时,它会发送一条命令来删除服务器上的关联文件。问题在于,对于特别大的文件,删除似乎需要很长时间,但GUI不在等待响应(因此认为文件已被删除)。如果然后我尝试再次上传相同的文件,则会收到“访问被拒绝”异常,因为该文件仍被另一个请求使用...

无论何时发生文件IO,我都试图使用互斥锁来锁定子目录,但是由于某些原因,不同的请求似乎并没有使用相同的互斥锁。如果我使用静态单例互斥量,则可以使用,但是这意味着整个服务器一次只能上传/删除一个文件。

如何为当前正在使用的子目录创建互斥锁,并在多个请求中识别它?

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);
            }
        }
    }
}

1 个答案:

答案 0 :(得分:0)

我最终没有使用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);
            }
        }
    }

这时我要做的就是用对我的lock方法的调用替换我所有的Protected块:

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);
        }
    });
}