前言

  我們之前聊過redis的,對基礎不瞭解的可以移步檢視一下:

幾分鐘搞定redis儲存session共享——設計實現:https://www.cnblogs.com/xiongze520/p/10333233.html

【原創】詳細案例解剖——淺談Redis快取的常用5種方式(String,Hash,List,set,SetSorted ):https://www.cnblogs.com/xiongze520/p/10267804.html

  對同一個資源進行操作,單一的快取讀取沒問題了,但是存在併發的時候怎麼辦呢,為了避免資料不一致,我們需要在操作共享資源之前進行加鎖操作。

我們在開發很多業務場景會使用到鎖,例如庫存控制,抽獎,秒殺等。一般我們會使用記憶體鎖的方式來保證線性的執行。

但現在大多站點都會使用分散式部署,那多臺伺服器間的就必須使用同一個目標來判斷鎖。分散式與單機情況下最大的不同在於其不是多執行緒而是多程序。

         圖1:分散式站點使用記憶體鎖

        圖2:分散式站點使用分散式鎖

當然我們暫時用不了這麼複雜的場景,我們就簡單訪問redis就行。


設計(悲觀鎖/樂觀鎖)

悲觀鎖方式(認為操作的時候,會出現問題,所以都加鎖)

悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,
所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。
傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

樂觀鎖方式(認為什麼時候不會出問題,所以不上鎖,更新的時候去查詢判斷一下,再此期間是否有人修改過這個資料。)

樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,
所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。
樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。

  兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,
加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了效能,所以這種情況下用悲觀鎖就比較合適。


Redis三個命令

1、SETNX

SETNX key value:當且僅當key不存在時,set一個key為val的字串,返回1;若key存在,則什麼都不做,返回0。

2、expire

expire key timeout:為key設定一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。

3、delete

delete key:刪除key

在使用Redis實現分散式鎖的時候,主要就會使用到這三個命令。

命題:某商品進行庫存秒殺。

假設要給某個商品舉行秒殺活動,我們事先把庫存資料100已經存入到了redis中,我們現在需要來進行庫存扣減。

             圖3:加鎖請求示意圖


程式碼實現

我們基於 ServiceStack.Redis 操作

我們建立一個控制檯應用(.NET Framework),命名為 RedisLock ,注意,如果建立的是net core的應用,引入的ServiceStack.Redis就要選擇core的。

然後在NuGet裡面安裝ServiceStack.Redis。

Redis連線池

       //Redis連線池(配置連線地址,讀寫連線地址等)
public static PooledRedisClientManager RedisClientPool = CreateManager();
private static PooledRedisClientManager CreateManager()
{
//寫節點(主節點)
List<string> writes = new List<string>();
writes.Add("10.17.3.97:6379");
//讀節點
List<string> reads = new List<string>();
reads.Add("10.17.3.97:6379"); //配置連線池和讀寫分類
return new PooledRedisClientManager(writes, reads, new RedisClientManagerConfig()
{
MaxReadPoolSize = 50, //讀節點個數
MaxWritePoolSize = 50,//寫節點個數
AutoStart = true,
DefaultDb = 0
});
}

使用Redis的SetNX命令實現加鎖

        /// <summary>
/// 加鎖(使用Redis的SetNX命令實現加鎖)
/// </summary>
/// <param name="key">鎖key</param>
/// <param name="selfMark">自己標記</param>
/// <param name="lockExpirySeconds">鎖自動過期時間[預設10](s)</param>
/// <param name="waitLockMilliseconds">等待鎖時間(ms)</param>
/// <returns></returns>
public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue)
{
DateTime begin = DateTime.Now;
selfMark = Guid.NewGuid().ToString("N");//自己標記,釋放鎖時會用到,自己加的鎖除非過期否則只能自己開啟
using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
{ string lockKey = "Lock:" + key;
while (true)
{
string script = string.Format("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE',KEYS[1],{0}) return 1 else return 0 end", lockExpirySeconds * 1000);
//迴圈獲取取鎖
if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1)
{
return true;
} //不等待鎖則返回
if (waitLockMilliseconds == 0)
{
break;
} //超過等待時間,則不再等待
if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds)
{
break;
}
Thread.Sleep(100);
} return false;
}
}

因為ServiceStack.Redis提供的SetNX方法,並沒有提供設定過期時間的方法,對於加鎖業務又不能分開執行(如果加鎖成功設定過期時間失敗導致的永久死鎖問題),所以就使用指令碼實現,解決了異常情況死鎖問題.

如果設定為0,為樂觀鎖機制,獲取不到鎖,直接返回未獲取到鎖.
預設值為long最大值,為悲觀鎖機制,約等於很多很多天,可以理解為一直等待.

釋放鎖

     /// <summary>
/// 釋放鎖
/// </summary>
/// <param name="key">鎖key</param>
/// <param name="selfMark">自己標記</param>
public static void UnLock(string key, string selfMark)
{
using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
{
string lockKey = "Lock:" + key;
var script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisClient.ExecLuaAsString(script, new[] { lockKey }, new[] { selfMark });
}
}

業務呼叫(我們使用多執行緒模擬多使用者秒殺的場景)

     //業務:悲觀鎖方式
public static void PessimisticLock()
{
int num = 10; //總數量
string lockkey = "xianseng"; //悲觀鎖開啟20個人同時拿寶貝
for (int i = 0; i < 20; i++)
{
Task.Run(() =>
{
string selfmark = "";
try
{
if (Lock(lockkey, out selfmark))
{
if (num > 0)
{
num--;
Console.WriteLine($"我拿到了寶貝:寶貝剩餘{num}個\t\t{selfmark}");
}
else
{
Console.WriteLine("寶貝已經沒有了");
}
Thread.Sleep(100);
}
}
finally
{
UnLock(lockkey, selfmark);
}
});
} Console.ReadLine();
} //業務:樂觀鎖方式
public static void OptimisticLock()
{
int num = 10; //總數量
string lockkey = "xianseng"; //樂觀鎖開啟10個執行緒,每個執行緒拿5次
for (int i = 0; i < 10; i++)
{
var lineOn = "執行緒" + (i + 1);
Task.Run(() =>
{
for (int j = 0; j < 5; j++)
{
string selfmark = "";
try
{
if (Lock(lockkey, out selfmark, 10, 0))
{
if (num > 0)
{
num--;
Console.WriteLine($"{lineOn} 第{(j+1)}次 我拿到了寶貝:寶貝剩餘{num}個\t\t{selfmark}");
}
else
{
Console.WriteLine($"{lineOn} 第{(j + 1)}次 寶貝已經沒有了");
} Thread.Sleep(1000);
}
else
{
Console.WriteLine($"{lineOn} 第{(j+1)}次 沒有拿到,不想等了");
}
}
finally
{
UnLock(lockkey, selfmark);
}
}
});
} Console.ReadLine();
}

然後在main函式裡面呼叫檢視展示效果

       static void Main(string[] args)
{
////呼叫:悲觀鎖方式(認為我操作的時候,會出現問題,所以都加鎖)
PessimisticLock(); ///呼叫:樂觀鎖方式(認為什麼時候不會出問題,所以不上鎖,更新的時候去查詢判斷一下,再此期間是否有人修改過這個資料。)
//OptimisticLock();
}

這就簡單實現了Redis分佈鎖的功能,快去試試吧。

參考文獻

 
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取

作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說


出處: https://www.cnblogs.com/xiongze520/p/15176559.html



創作不易,任何人或團體、機構全部轉載或者部分轉載、摘錄,請在文章明顯位置註明作者和原文連結。