前言
今日上班聽到同事在準備面試題分散式鎖(準備溜溜球),隨即加入了群聊複習了一波,於是有了這篇小作文。
場景
本文中的演示 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 實現的分散式鎖
通過上面示例可以看見,分散式鎖的使用無非就是 Lock
和 UnLock
的操作。我這裡直接用編輯器除錯進去看了,就不是上 GitHub
上下載程式碼看了。體驗不好,還請擔待。
上鎖
- 迴圈檢測獲取鎖操作是否過期,過期直接返回
Null
, 否則繼續步驟二 SetNx
設定值,如果成功,建立分散式鎖物件,否則執行緒等待一會,繼續第一步,如此迴圈
為啥不可以設定唯一值呢?在沒有啟動自動續時(看門狗機制),業務執行時間超過了鎖的過期時間時,會引發問題。
- 比如說現在
請求1
,請求2
,請求3
同時過來,請求1
先搶到了鎖,開始執行。 - 但是
請求1
的業務執行時間比較長,鎖已經過期失效了,業務還沒有執行完成。這時請求2
獲取到鎖,執行自己的業務。就出現了請求1
和請求2
併發執行了 - 當
請求1
執行完自己的業務的時候,執行解鎖操作,因為鍵值都一樣,會誤把請求2
的鎖給釋放掉,導致故障
通過設定值的唯一,當刪除快取的時候,還需要判斷一下值是不是一致,來防止誤釋放其他鎖。
看門狗機制
- 定時執行
Refresh
方法 - 通過
lua
指令碼設定新的過期時間,不成功的話(已解鎖),刪除定時器
解鎖
- 通過
lua
指令碼匹配鍵
和值
都一樣的key, 才能刪除