C#使用StackExchange.Redis實(shí)現(xiàn)分布式鎖的兩種方式介紹
分布式鎖在集群的架構(gòu)中發(fā)揮著重要的作用。以下有主要的使用場(chǎng)景
1.在秒殺、搶購(gòu)等高并發(fā)場(chǎng)景下,多個(gè)用戶同時(shí)下單同一商品,可能導(dǎo)致庫(kù)存超賣。
2.支付、轉(zhuǎn)賬等金融操作需保證同一賬戶的資金變動(dòng)是串行執(zhí)行的。
3.分布式環(huán)境下,多個(gè)節(jié)點(diǎn)可能同時(shí)觸發(fā)同一任務(wù)(如定時(shí)報(bào)表生成)。
4.用戶因網(wǎng)絡(luò)延遲重復(fù)提交表單,可能導(dǎo)致數(shù)據(jù)重復(fù)插入。
自定義分布式鎖
獲取鎖
比如一下一個(gè)場(chǎng)景,需要對(duì)訂單號(hào)為 order-88888944010的訂單進(jìn)行扣款處理,因?yàn)楹蠖耸嵌喙?jié)點(diǎn)的,防止出現(xiàn)用戶重復(fù)點(diǎn)擊導(dǎo)致扣款請(qǐng)求到不用的集群節(jié)點(diǎn),所以需要同時(shí)只有一個(gè)節(jié)點(diǎn)處理該訂單。
public static async Task<(bool Success, string LockValue)> LockAsync(string cacheKey, int timeoutSeconds = 5)
{
var lockKey = GetLockKey(cacheKey);
var lockValue = Guid.NewGuid().ToString();
var timeoutMilliseconds = timeoutSeconds * 1000;
var expiration = TimeSpan.FromMilliseconds(timeoutMilliseconds);
bool flag = await _redisDb.StringSetAsync(lockKey, lockValue, expiration, When.NotExists);
return (flag, flag ? lockValue : string.Empty);
}
public static string GetLockKey(string cacheKey)
{
return $"MyApplication:locker:{cacheKey}";
}
上述代碼是在請(qǐng)求時(shí)將訂單號(hào)作為redis key的一部分存儲(chǔ)到redis中,并且生成了一個(gè)隨機(jī)的lockValue作為值。只有當(dāng)redis中不存在該key的時(shí)候才能夠成功設(shè)置,即為獲取到該訂單的分布式鎖了。
await LockAsync("order-88888944010",30); //獲取鎖,并且設(shè)置超時(shí)時(shí)間為30秒釋放鎖
public static async Task<bool> UnLockAsync(string cacheKey, string lockValue)
{
var lockKey = GetLockKey(cacheKey);
var script = @"local invalue = @value
local currvalue = redis.call('get',@key)
if(invalue==currvalue) then redis.call('del',@key)
return 1
else
return 0
end";
var parameters = new { key = lockKey, value = lockValue };
var prepared = LuaScript.Prepare(script);
var result = (int)await _redisDb.ScriptEvaluateAsync(prepared, parameters);
return result == 1;
}
釋放鎖采用了lua腳本先判斷l(xiāng)ockValue是否是同一個(gè)處理節(jié)點(diǎn)發(fā)過(guò)來(lái)的刪除請(qǐng)求,即判斷加鎖和釋放鎖是同一個(gè)來(lái)源。
用lua腳本而不是直接使用API執(zhí)行刪除的原因:
1.A獲取鎖后因GC停頓或網(wǎng)絡(luò)延遲導(dǎo)致鎖過(guò)期,此時(shí)客戶端B獲取了鎖。若A恢復(fù)后直接調(diào)用DEL,會(huì)錯(cuò)誤刪除B持有的鎖。
2.腳本在Redis中單線程執(zhí)行,確保GET和DEL之間不會(huì)被其他命令打斷。
自動(dòng)續(xù)期
一些比較耗時(shí)的任務(wù),可能在指定的超時(shí)時(shí)間內(nèi)無(wú)法完成業(yè)務(wù)處理,需要存在自動(dòng)續(xù)期的機(jī)制。
/// <summary>
/// 自動(dòng)續(xù)期
/// </summary>
/// <param name="redisDb"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="milliseconds">續(xù)期的時(shí)間</param>
/// <returns></returns>
public async static Task Delay(IDatabase redisDb, string key, string value, int milliseconds)
{
if (!AutoDelayHandler.Instance.ContainsKey(key))
return;
var script = @"local val = redis.call('GET', @key)
if val==@value then
redis.call('PEXPIRE', @key, @milliseconds)
return 1
end
return 0";
object parameters = new { key, value, milliseconds };
var prepared = LuaScript.Prepare(script);
var result = await redisDb.ScriptEvaluateAsync(prepared, parameters, CommandFlags.None);
if ((int)result == 0)
{
AutoDelayHandler.Instance.CloseTask(key);
}
return;
}
保存自動(dòng)續(xù)期任務(wù)的處理器
public class AutoDelayHandler
{
private static readonly Lazy<AutoDelayHandler> lazy = new Lazy<AutoDelayHandler>(() => new AutoDelayHandler());
private static ConcurrentDictionary<string, (Task, CancellationTokenSource)> _tasks = new ConcurrentDictionary<string, (Task, CancellationTokenSource)>();
public static AutoDelayHandler Instance => lazy.Value;
/// <summary>
/// 任務(wù)令牌添加到集合中
/// </summary>
/// <param name="key"></param>
/// <param name="task"></param>
/// <returns></returns>
public bool TryAdd(string key, Task task, CancellationTokenSource token)
{
if (_tasks.TryAdd(key, (task, token)))
{
task.Start();
return true;
}
else
{
return false;
}
}
public void CloseTask(string key)
{
if (_tasks.ContainsKey(key))
{
if (_tasks.TryRemove(key, out (Task, CancellationTokenSource) item))
{
item.Item2?.Cancel();
item.Item1?.Dispose();
}
}
}
public bool ContainsKey(string key)
{
return _tasks.ContainsKey(key);
}
}
在申請(qǐng)帶有自動(dòng)續(xù)期的分布式鎖的完整代碼
/// <summary>
/// 獲取鎖
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="timeoutSeconds">超時(shí)時(shí)間</param>
/// <param name="autoDelay">是否自動(dòng)續(xù)期</param>
/// <returns></returns>
public static async Task<(bool Success, string LockValue)> LockAsync(string cacheKey, int timeoutSeconds = 5, bool autoDelay = false)
{
var lockKey = GetLockKey(cacheKey);
var lockValue = Guid.NewGuid().ToString();
var timeoutMilliseconds = timeoutSeconds * 1000;
var expiration = TimeSpan.FromMilliseconds(timeoutMilliseconds);
bool flag = await _redisDb.StringSetAsync(lockKey, lockValue, expiration, When.NotExists);
if (flag && autoDelay)
{
//需要自動(dòng)續(xù)期,創(chuàng)建后臺(tái)任務(wù)
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var autoDelaytask = new Task(async () =>
{
while (!cancellationTokenSource.IsCancellationRequested)
{
await Task.Delay(timeoutMilliseconds / 2);
await Delay(lockKey, lockValue, timeoutMilliseconds);
}
}, cancellationTokenSource.Token);
var result = AutoDelayHandler.Instance.TryAdd(lockKey, autoDelaytask, cancellationTokenSource);
if (!result)
{
autoDelaytask.Dispose();
await UnLockAsync(cacheKey, lockValue);
return (false, string.Empty);
}
}
return (flag, flag ? lockValue : string.Empty);
}
Redis的過(guò)期時(shí)間精度約為1秒,且過(guò)期檢查是周期性執(zhí)行的(默認(rèn)每秒10次)。選擇TTL/2的間隔能:
確保在Redis下一次過(guò)期檢查前完成續(xù)期。
兼容Redis的主從同步延遲(通常<1秒)
StackExchange.Redis分布式鎖
獲取鎖
string lockKey = "order:88888944010:lock"; string lockValue = Guid.NewGuid().ToString(); // 唯一標(biāo)識(shí)鎖持有者 TimeSpan expiry = TimeSpan.FromSeconds(10); // 鎖自動(dòng)過(guò)期時(shí)間 // 嘗試獲取鎖(原子操作) bool lockAcquired = db.LockTake(lockKey, lockValue, expiry);
釋放鎖
bool released = await ReleaseLockAsync(db, lockKey, lockValue);
自動(dòng)續(xù)期
同樣需要自己實(shí)現(xiàn)
到此這篇關(guān)于C#使用StackExchange.Redis實(shí)現(xiàn)分布式鎖的兩種方式介紹的文章就介紹到這了,更多相關(guān)C# StackExchange.Redis實(shí)現(xiàn)分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
c# 如何實(shí)現(xiàn)自動(dòng)更新程序
這篇文章主要介紹了如何用c# 自動(dòng)更新程序,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-03-03
C#實(shí)現(xiàn)移動(dòng)窗體的兩種共方法
在C#Form窗體設(shè)計(jì)中,如果我們不需要使用默認(rèn)邊框設(shè)計(jì)自己個(gè)性化的窗體,這時(shí)候你會(huì)發(fā)現(xiàn)拖動(dòng)窗體的功能就沒(méi)有了,這里需要自己構(gòu)建方法讓用戶可以拖動(dòng)整個(gè)窗體,下面就介紹兩種方法來(lái)實(shí)現(xiàn),需要的朋友可以參考下2024-09-09
C# winformTextBox 鍵盤監(jiān)聽(tīng)方式
C#學(xué)習(xí)教程之Socket的簡(jiǎn)單使用

