面試官: 你平時用過讀寫鎖嗎?

有態度的馬甲發表於2021-09-01

前情提要

同程藝龍基礎架構部推出的資料獲取元件DAL.Connection,我們要做到在切換連線配置時清空資料庫連線池, 這就涉及到切換連線的時候,觸發變更通知。

  • .NET 如何清空連線池?
  • 面試官:實現一個帶值變更通知能力的Dictionary

仔細閱讀《面試官:實現一個帶值變更通知能力的Dictionary》一文的童靴們有沒有發現一個細節: 我使用了lock語法糖無腦加鎖。

這裡面有個前置知識點:C# Dictionary執行緒不安全。
什麼叫執行緒不安全,請看這個: https://www.cnblogs.com/JulianHuang/p/14720042.html。


這在高併發下會有問題:大多數時候下DBA並不會變更業務方的資料庫連線,這是一個多讀少寫的場景, 我們無腦使用lock在多數時間會人為阻塞請求。

到這個時候,我們就要想到讀寫鎖ReaderWriterLockSlim

寶藏好物:ReaderWriterLockSlim

Use ReaderWriterLockSlim to protect a resource that is read by multiple threads and written to by one thread at a time. ReaderWriterLockSlim allows multiple threads to be in read mode, allows one thread to be in write mode with exclusive ownership of the lock, and allows one thread that has read access to be in upgradeable read mode, from which the thread can upgrade to write mode without having to relinquish its read access to the resource.

簡而言之:

ReaderWriterLockSlim提供對某資源在某時刻下的多執行緒同讀、 或單執行緒獨佔寫。
此外,ReaderWriterLockSlim還提供從讀模式無縫升級到獨佔寫模式。

總結下來:

讀寫鎖處於以下四種狀態:

  1. 未進入: 沒有執行緒進入鎖(或者所有執行緒退出鎖)
  2. 讀模式:每次呼叫EnterReadlock時,鎖計數都會增加,但允許您讀取其中的程式碼塊。
  3. 寫模式: 獨佔、排他
  4. 可升級的讀模式(upgradeable read mode): 多執行緒讀,其中一個執行緒具備在某時刻升級到排他寫模式的可能。

btw,讀寫鎖相比常規lock之外,還具備鎖超時的機制,能避免未知原因持續佔有鎖導致的死鎖。

這個就很適合常見的多讀少寫場景, 微軟ReaderWriterLockSlim頁面很貼心的提供了一個基於讀寫鎖的快取操作類SynchronizedCache

開箱即用的快取操作類

基於ReaderWriterLockSlim對執行緒不安全的Dictionary進行了包裝, 可以作為一個多讀少寫的快取操作類。

public class SynchronizedCache 
{
    private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    private Dictionary<int, string> innerCache = new Dictionary<int, string>();

    public int Count
    { get { return innerCache.Count; } }

    public string Read(int key)
    {
        cacheLock.EnterReadLock();
        try
        {
            return innerCache[key];
        }
        finally
        {
            cacheLock.ExitReadLock();
        }
    }

    public void Add(int key, string value)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Add(key, value);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public bool AddWithTimeout(int key, string value, int timeout)
    {
        if (cacheLock.TryEnterWriteLock(timeout))
        {
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
        cacheLock.EnterUpgradeableReadLock();
        try
        {
            string result = null;
            if (innerCache.TryGetValue(key, out result))
            {
                if (result == value)
                {
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
                    cacheLock.EnterWriteLock();
                    try
                    {
                        innerCache[key] = value;
                    }
                    finally
                    {
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
                cacheLock.EnterWriteLock();
                try
                {
                    innerCache.Add(key, value);
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
            cacheLock.ExitUpgradeableReadLock();
        }
    }

    public void Delete(int key)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Remove(key);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public enum AddOrUpdateStatus
    {
        Added,
        Updated,
        Unchanged
    };

    ~SynchronizedCache()
    {
       if (cacheLock != null) cacheLock.Dispose();
    }
}

快取操作類SynchronizedCache如常規的字典類一樣, 不帶值變更通知的能力,為滿足【變更前清空連線池】的需求,我們還是新增event ,註冊變更邏輯。

public event EventHandler<ValueChangedEventArgs<string>> OnValueChanged;

//--- 節選自AddOrUpdate方法
cacheLock.EnterWriteLock();
try
{
   OnValueChanged?.Invoke(this, new ValueChangedEventArgs<string>(key));
   innerCache[key] = value;
}
finally
{
    cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
                        
//---

if (sc.AddOrUpdate(key, value) == SynchronizedCache.AddOrUpdateStatus.Updated)
{
    Console.WriteLine($"已經發生了值變更,原key對應的鍵值已經被重寫。");}
}  

旁白

本文記錄了讀寫鎖在日常開發中的實踐, 大多數場景都是多讀少寫,讀者可以思考一下是不是也可以將專案中的無腦lock替換為SynchronizedCache


本文是同程藝龍DAL.Connection元件研發過程的一個小插曲,有心的讀者可以往上翻一翻,瞭解上下文背景、瞭解小碼甲的思考過程。

這就像我們高中做數學題,直接看答案並不能快速提升,結合上下文自然、流暢的轉到這個方向才是最重要的。

相關文章