FreeRedis分散式鎖實現以及使用

TfcYe發表於2021-03-10

前言

今日上班聽到同事在準備面試題分散式鎖(準備溜溜球),隨即加入了群聊複習了一波,於是有了這篇小作文。

場景

本文中的演示 DEMO, 以下訂單減庫存為例。

無鎖裸奔表現

示例程式碼:

先來模擬一個庫存服務唄!

    /// <summary>
    /// 模擬庫存服務
    /// </summary>
    public class StockService
    {
        private static RedisClient cli = new RedisClient("127.0.0.1:6379");

        /// <summary>
        /// 減庫存操作
        /// </summary>
        /// <param name="goodsCount">商品數</param>
        /// <returns></returns>
        public bool ReduceStock(int goodsCount)
        {
            var stockCount = cli.Get<int>("StockCount");
            if (stockCount > 0 && stockCount >= goodsCount)
            {
                stockCount -= goodsCount;
                cli.Set("StockCount", stockCount, 10);
                Console.WriteLine($"執行緒Id:{Thread.CurrentThread.ManagedThreadId},搶購成功!庫存數:{stockCount}");
                return true;
            }

            Console.WriteLine($"執行緒Id:{Thread.CurrentThread.ManagedThreadId},搶購失敗!");

            return false;
        }
    }

模擬500個併發請求,開始測試。

        static void Main(string[] args)
        {
            var stockService = new StockService();
            
            // 初始化庫存
            var cli = new RedisClient("127.0.0.1:6379");
            cli.Set("StockCount", 10, 10);

            // 模擬 500 個併發
            Parallel.For(0, 500, (i) => { Task.Run(() => { stockService.ReduceStock(1); }); });
        }

執行完成後,結果如下圖所示:

我們的庫存只有 10 個,截圖可見,至少有 29 個請求搶購成功了,出現了超賣的現象。

上分散式鎖表現

針對無鎖情況下出現的併發問題,如果是單體應用,用 lock 可以解決,但不適用於分散式應用。FreeRedis 中已有現成實現的分散式鎖,我們先來看看是如何使用的吧!

修改一下訂單服務程式碼:

    /// <summary>
    /// 模擬庫存服務
    /// </summary>
    public class StockService
    {
        private static RedisClient cli = new RedisClient("127.0.0.1:6379");
        private static readonly string _distributedLockKey = "DISTRIBUTEDLOCKKEY";

        /// <summary>
        /// 減庫存操作
        /// </summary>
        /// <param name="goodsCount">商品數</param>
        /// <returns></returns>
        public bool ReduceStock(int goodsCount)
        {
            // 取鎖
            var lockObj = cli.Lock(_distributedLockKey, 1);
            if (lockObj != null)
            {
                var stockCount = cli.Get<int>("StockCount");
                if (stockCount > 0 && stockCount >= goodsCount)
                {
                    stockCount -= goodsCount;
                    cli.Set("StockCount", stockCount, 10);
                    Console.WriteLine($"執行緒Id:{Thread.CurrentThread.ManagedThreadId},搶購成功!庫存數:{stockCount}");
                    lockObj.Unlock(); // 解鎖
                    return true;
                }

                Console.WriteLine($"執行緒Id:{Thread.CurrentThread.ManagedThreadId},搶購失敗!");
                lockObj.Unlock(); // 解鎖
            }

            return false;
        }
    }

執行結果如下所示:

從輸出結果中可以看出,庫存有序的扣除中,確實只有 10 個請求是搶購成功。

看看 FreeRedis 實現的分散式鎖

通過上面示例可以看見,分散式鎖的使用無非就是 LockUnLock 的操作。我這裡直接用編輯器除錯進去看了,就不是上 GitHub 上下載程式碼看了。體驗不好,還請擔待。

上鎖

  1. 迴圈檢測獲取鎖操作是否過期,過期直接返回 Null, 否則繼續步驟二
  2. SetNx 設定值,如果成功,建立分散式鎖物件,否則執行緒等待一會,繼續第一步,如此迴圈

為啥不可以設定唯一值呢?在沒有啟動自動續時(看門狗機制),業務執行時間超過了鎖的過期時間時,會引發問題。

  • 比如說現在 請求1請求2請求3 同時過來,請求1 先搶到了鎖,開始執行。
  • 但是 請求1 的業務執行時間比較長,鎖已經過期失效了,業務還沒有執行完成。這時 請求2 獲取到鎖,執行自己的業務。就出現了 請求1請求2 併發執行了
  • 請求1 執行完自己的業務的時候,執行解鎖操作,因為鍵值都一樣,會誤把 請求2 的鎖給釋放掉,導致故障

通過設定值的唯一,當刪除快取的時候,還需要判斷一下值是不是一致,來防止誤釋放其他鎖。

看門狗機制


  1. 定時執行 Refresh 方法
  2. 通過 lua 指令碼設定新的過期時間,不成功的話(已解鎖),刪除定時器

解鎖

  1. 通過 lua 指令碼匹配 都一樣的key, 才能刪除

分散式鎖的坑參考連線

相關文章