C# Redis分佈式鎖的應用

1、背景

我們在開發很多業務場景會使用到鎖,例如庫存控制,抽獎等。一般我們會使用內存鎖的方式來保證線性的執行。但現在大多站點都會使用分佈式部署,那多臺服務器間的就必須使用同一個目標來判斷鎖。分佈式與單機情況下最大的不同在於其不是多線程而是多進程。

2、演變

[分佈式站點使用內存鎖方式如下圖]
clipboard.png
假設有3個用戶同時購買一件商品,商品庫存只剩下1,如果3個用戶同時購買,負載均衡把3個用戶分別指向站點1、2、3,那結果將會是3個用戶都購買成功。下面我們使用分佈式鎖解決這個問題。

[分佈式站點使用分佈式鎖如下圖]
clipboard.png
單臺服務器由於可以共享堆內存,因此可以簡單的採取內存作爲標記存儲位置。而多服務器之間都不在同一臺物理機上,因此需要將標記存儲在一個所有進程都能看到的地方。

3、實現

選型

想想我們實現分佈式鎖要滿足哪些條件?
1、在分佈式系統環境下,一個鎖在同一時間只能被一個服務器獲取;(這是所有分佈式鎖的基礎)
2、高性能的獲取鎖和釋放鎖;(鎖用完了,要及時釋放,以供別人繼續使用)
3、具備鎖失效機制,防止死鎖;(防止因爲某些意外,鎖沒有得到釋放,那別人將永遠無法使用)
4、具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。(滿足等待鎖的同時,也要滿足非阻塞鎖特性,便於多樣性的業務場景使用)

分佈式鎖有很多的實現方式:數據庫排他鎖、Zookeeper、緩存(redis、memcached)等,本文選用Redis實現。原因如下:
(1)Redis有很高的性能;
(2)Redis命令對此支持較好,實現起來比較方便;
Redis官網上對C#的使用推薦有ServiceStack.Redis和StackExchange.Redis
由於StackExchange.Redis雖然有提供LockTake方法,很方便的使用鎖,但是隻支持.Net4.5以上。公司很多站點都是3.5和4.0的,所以選用ServiceStack.Redis,自己封裝一下。

Redis使用命令介紹

1、SETNX
SETNX key val:當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不做,返回0。
2、expire
expire key timeout:爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。
3、delete
delete key:刪除key
在使用Redis實現分佈式鎖的時候,主要就會使用到這三個命令。

C#代碼

1、鎖方法

        /// <summary>
        /// 分佈式鎖
        /// </summary>
        /// <param name="key">鎖key</param>
        /// <param name="lockExpirySeconds">鎖自動超時時間(秒)</param>
        /// <param name="waitLockMs">等待鎖時間(秒)</param>
        /// <returns></returns>
        public static bool Lock(string key, int lockExpirySeconds = 10, double waitLockSeconds = 0)
        {
            //間隔等待50毫秒
            int waitIntervalMs = 50;

            RedisClient client = (RedisClient)RedisClientManager.GetClient(RedisProviderName.RedisCommon, 0);

            string lockKey = "LockForSetNX:" + key;

            DateTime begin = DateTime.Now;
            using (client)
            {
                while (true)
                {
                    //循環獲取取鎖
                    if (client.SetNX(lockKey, new byte[] { 1 }) == 1)
                    {
                        //設置鎖的過期時間
                        client.Expire(lockKey, lockExpirySeconds);
                        return true;
                    }

                    //不等待鎖則返回
                    if (waitLockSeconds == 0) break;

                    //超過等待時間,則不再等待
                    if ((DateTime.Now - begin).TotalSeconds >= waitLockSeconds) break;

                    Thread.Sleep(waitIntervalMs);
                }
                return false;
            }
        }

2、釋放鎖

        /// <summary>
        /// 刪除鎖 執行完代碼以後調用釋放鎖
        /// </summary>
        /// <param name="key"></param>
        public static void DelLock(string key)
        {
            RedisClient client = (RedisClient)RedisClientManager.GetClient(RedisProviderName.RedisCommon, 0);
            string lockKey = "LockForSetNX:" + key;
            using (client)
            {
                client.Del(lockKey);
            }
        }

3、業務應用代碼

            try
            {
                //取鎖,設置key10秒後失效,最大等待鎖5秒
                if (RedisHelper.Lock("LockKey", 10, 5))
                {
                    //取到鎖,執行具體業務
                    //例如商品購買,庫存-1
                }
            }
            finally
            {
                //釋放鎖
                DelLock("LockKey");
            }

【如果文章對你有所幫助,請在評論區留言點贊,以資鼓勵。】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章