之前寫(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ì)特定資源有更新操作”。
基本考量:
- 分布式系統(tǒng),一個(gè)鎖在同一時(shí)間只能被一個(gè)服務(wù)器獲取 (這是分布式鎖的基礎(chǔ))
- 具備鎖失效機(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)更加健壯:
- 不要使用固定的String值,而是使用一個(gè)不易被猜中的隨機(jī)值, 業(yè)內(nèi)稱(chēng)為
token - 不使用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)考量:
- 為避免無(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é)考量。

