FreeRedis分布式鎖實現以及使用


前言

最近公司的小伙伴在准備面試題,隨時准備跑路。聽到他們正在討論分布式鎖相關知識,便也立即加入了群聊(我也想溜溜球了)。於是有了今天這篇小作文,記錄一下知識點,也希望能幫助其他的小伙伴共同學習,共同進步。

場景

本文中的演示 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, 才能刪除

分布式鎖的坑參考連接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM