華為二面被問(wèn)Redis分布式鎖,您是不是有點(diǎn)小瞧我了?

之前寫(xiě)了兩篇有關(guān)線(xiàn)程安全的文章:

  • 你管這叫線(xiàn)程安全?
  • .NET八股文:線(xiàn)程同步技術(shù)解讀

分布式鎖是"線(xiàn)程同步"的延續(xù)

最近首度應(yīng)用"分布式鎖",現(xiàn)在想想,分布式鎖不是孤立的技能點(diǎn),這其實(shí)就是跨主機(jī)的線(xiàn)程同步。

進(jìn)程內(nèi) 跨進(jìn)程 跨主機(jī)
Lock/Monitor、SemaphoreSlim Metux、Semaphore 分布式鎖
用戶(hù)態(tài)線(xiàn)程安全 內(nèi)核態(tài)線(xiàn)程安全

單機(jī)服務(wù)器可以通過(guò)共享某堆內(nèi)存來(lái)標(biāo)記上鎖/解鎖,線(xiàn)程同步說(shuō)到底是建立在單機(jī)操作系統(tǒng)的用戶(hù)態(tài)/內(nèi)核態(tài)對(duì)共享內(nèi)存的訪問(wèn)控制。

而分布式服務(wù)器不是在同一臺(tái)機(jī)器上:跨主機(jī),因此需要將內(nèi)存標(biāo)記存儲(chǔ)在所有機(jī)器進(jìn)程都能看到的地方。

在開(kāi)發(fā)很多業(yè)務(wù)場(chǎng)景會(huì)使用到鎖,例如庫(kù)存控制,抽獎(jiǎng)等。
例如庫(kù)存只剩1個(gè)商品,有三個(gè)用戶(hù)同時(shí)打算購(gòu)買(mǎi),誰(shuí)先購(gòu)買(mǎi)庫(kù)存立即清零,不能讓其他二人也購(gòu)買(mǎi)成功。

解讀分布式鎖

我們常說(shuō)的線(xiàn)程安全、線(xiàn)程同步方案,包括此次的分布式鎖都是基于“多線(xiàn)程/多進(jìn)程對(duì)特定資源有更新操作”。

基本考量:

  1. 分布式系統(tǒng),一個(gè)鎖在同一時(shí)間只能被一個(gè)服務(wù)器獲取 (這是分布式鎖的基礎(chǔ))
  2. 具備鎖失效機(jī)制,防止死鎖 (防止某些意外,鎖沒(méi)有得到釋放,那別人也無(wú)法得到鎖)

Redis SET resource-name anystring NX EX max-lock-time 是一種最簡(jiǎn)單的分布式鎖實(shí)現(xiàn)方案。

SET 命令支持多個(gè)參數(shù):

  • EX seconds-- 設(shè)置過(guò)期時(shí)間(s)
  • NX -- 如果key不存在,則設(shè)置
    ......
    因?yàn)镾ET命令參數(shù)可以替代SETNX,SETEX,GETSET,這些命令在未來(lái)可能被廢棄。

上面的命令返回OK(或經(jīng)過(guò)重試),客戶(hù)端就獲取到這個(gè)鎖;
使用DEL命令解鎖;
到達(dá)超時(shí)時(shí)間會(huì)自動(dòng)釋放鎖。

在解鎖時(shí),增加一些設(shè)計(jì),讓系統(tǒng)更加健壯:

  1. 不要使用固定的String值,而是使用一個(gè)不易被猜中的隨機(jī)值, 業(yè)內(nèi)稱(chēng)為token
  2. 不使用DEL命令釋放鎖,而是發(fā)送script去移除key

第3、4點(diǎn)是為了解決 :“鎖提前過(guò)期,客戶(hù)端A還沒(méi)有執(zhí)行完,然后客戶(hù)端B獲取了鎖,這時(shí)客戶(hù)端A執(zhí)行完了,會(huì)不會(huì)再刪鎖的時(shí)候把B的鎖給刪掉” -- 4是3技術(shù)上的推薦實(shí)現(xiàn)。

腳本如下:

if redis.call("get",KEYS1] ==ARGV[1])
then
   return  redis.call("DEL",KEYS[1])
else
  return 0
end

下面使用StackExchange.Redis 寫(xiě)了基于以上考量的代碼示例:


        /// <summary>
        /// Acquires the lock.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="token">隨機(jī)值</param>
        /// <param name="expireSecond"></param>
        /// <param name="waitLockSeconds">非阻塞鎖</param>
        static bool Lock(string key, string token,int expireSecond=10, double waitLockSeconds = 0)
        {
            var waitIntervalMs = 50;
            bool isLock;

            DateTime begin = DateTime.Now;
            do
            {
                isLock = Connection.GetDatabase().StringSet(key, token, TimeSpan.FromSeconds(expireSecond), When.NotExists);
                if (isLock)
                    return true;

                //不等待鎖則返回
                if (waitLockSeconds == 0) break;
                //超過(guò)等待時(shí)間,則不再等待
                if ((DateTime.Now - begin).TotalSeconds >= waitLockSeconds) break;

                Thread.Sleep(waitIntervalMs);
            } while (!isLock);
            return false;
        }

        /// <summary>  
        /// Releases the lock.  
        /// </summary>  
        /// <returns><c>true</c>, if lock was released, <c>false</c> otherwise.</returns>  
        /// <param name="key">Key.</param>  
        /// <param name="value">value</param>  
        static bool UnLock(string key, string value)
        {
            string lua_script = @"  
                if (redis.call('GET', KEYS[1]) == ARGV[1]) then  
                    redis.call('DEL', KEYS[1])  
                    return true  
                else  
                    return false  
                end  
                ";

            try
            {
                var res = Connection.GetDatabase().ScriptEvaluate(lua_script,
                                                           new RedisKey[] { key },
                                                           new RedisValue[] { value });
                return (bool)res;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"ReleaseLock lock fail...{ex.Message}");
                return false;
            }
        }

        private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
        {
            ConfigurationOptions configuration = new ConfigurationOptions
            {
                AbortOnConnectFail = false,
                ConnectTimeout = 5000,
            };

            configuration.EndPoints.Add("10.100.219.9", 6379);

            return ConnectionMultiplexer.Connect(configuration.ToString());
        }); 
        public static ConnectionMultiplexer Connection => lazyConnection.Value;

以上代碼新增了第五點(diǎn)考量:

  1. 為避免無(wú)限制搶鎖,增加了非阻塞鎖: 輪詢(xún)_s等待鎖,未等到則不再搶鎖

使用方式:

下面并行開(kāi)啟三個(gè)任務(wù),減少庫(kù)存:

  static void Main(string[] args)
        {
            // 嘗試并行執(zhí)行3個(gè)任務(wù)
            Parallel.For(0, 3, x =>
            {
                string token = $"loki:{x}";
                bool isLocked = Lock("loki", token, 5, 10);

                if (isLocked)
                {
                    Console.WriteLine($"{token} begin reduce stocks (with lock) at {DateTime.Now}.");
                    Thread.Sleep(1000);
                    Console.WriteLine($"{token} release lock {UnLock("loki", token)} at {DateTime.Now}. ");
                }
                else
                {
                    Console.WriteLine($"{token} begin reduce stocks at {DateTime.Now}.");
                }
            });
        }

輸出總結(jié)

本文從基礎(chǔ)的線(xiàn)程安全,八卦文線(xiàn)程同步,延伸到跨主機(jī)的資源線(xiàn)程/進(jìn)程安全, 其中演示了利用RedisSET命令做分布式鎖的設(shè)計(jì)方案,雖然是面試八股文,我們依舊需要仔細(xì)揣摩Redis Lock的細(xì)節(jié)考量。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容