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ā)過來的刪除請(qǐng)求,即判斷加鎖和釋放鎖是同一個(gè)來源。
用lua腳本而不是直接使用API執(zhí)行刪除的原因:
1.A獲取鎖后因GC停頓或網(wǎng)絡(luò)延遲導(dǎo)致鎖過期,此時(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)無法完成業(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的過期時(shí)間精度約為1秒,且過期檢查是周期性執(zhí)行的(默認(rèn)每秒10次)。選擇TTL/2的間隔能:
確保在Redis下一次過期檢查前完成續(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)過期時(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#中Html.RenderPartial與Html.RenderAction的區(qū)別分析
這篇文章主要介紹了C#中Html.RenderPartial與Html.RenderAction的區(qū)別分析,需要的朋友可以參考下2014-07-07C#?webApi創(chuàng)建與發(fā)布、部署、api調(diào)用詳細(xì)教程
這篇文章主要給大家介紹了關(guān)于C#?webApi創(chuàng)建與發(fā)布、部署、api調(diào)用的相關(guān)資料,WebApi是微軟在VS2012?MVC4版本中綁定發(fā)行的,WebApi是完全基于Restful標(biāo)準(zhǔn)的框架,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12UGUI實(shí)現(xiàn)ScrollView無限滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了UGUI實(shí)現(xiàn)ScrollView無限滾動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02WPF使用Dragablz構(gòu)建可拖拽分離的Tab頁程序
這篇文章介紹了WPF使用Dragablz構(gòu)建可拖拽分離Tab頁的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06使用WPF實(shí)現(xiàn)一個(gè)虛擬鍵盤的代碼示例
在某些特定場(chǎng)景下,我們可能需要使用虛擬鍵盤來替代實(shí)體鍵盤,本文將詳細(xì)介紹如何使用 WPF 來實(shí)現(xiàn)一個(gè)虛擬鍵盤,并監(jiān)控鍵盤輸入,從而達(dá)到完全替代實(shí)體鍵盤的目的,需要的朋友可以參考下2025-04-04C#播放鈴聲最簡(jiǎn)單實(shí)現(xiàn)方法
這篇文章主要介紹了C#播放鈴聲最簡(jiǎn)單實(shí)現(xiàn)方法,通過調(diào)用系統(tǒng)方法實(shí)現(xiàn)播放wav格式音頻文件的功能,是非常實(shí)用的技巧,需要的朋友可以參考下2014-12-12