C#實現Redis的分佈式鎖
Redis實現分佈式鎖(悲觀鎖/樂觀鎖)
對鎖的概念和應用場景在此就不闡述瞭,網上搜索有很多解釋,隻是我搜索到的使用C#利用Redis的SetNX命令實現的鎖雖然能用,但是都不太適合我需要的場景。
Redis有三個最基本屬性來保證分佈式鎖的有效實現:
- 安全性: 互斥,在任何時候,隻有一個客戶端能持有鎖。
- 活躍性A:沒有死鎖,即使客戶端在持有鎖的時候崩潰,最後也會有其他客戶端能獲得鎖,超時機制。
- 活躍性B:故障容忍,隻有大多數Redis節點時存活的,客戶端仍可以獲得鎖和釋放鎖。
基於ServiceStack.Redis寫瞭一個幫助類
Redis連接池
public static PooledRedisClientManager RedisClientPool = CreateManager(); private static PooledRedisClientManager CreateManager() { var redisHosts = System.Configuration.ConfigurationManager.AppSettings["redisHosts"]; if (string.IsNullOrEmpty(redisHosts)) { throw new Exception("AppSetting redisHosts no found"); } string[] redisHostarr = redisHosts.Split(new string[] { ",", "," }, StringSplitOptions.RemoveEmptyEntries); return new PooledRedisClientManager(redisHostarr, redisHostarr, new RedisClientManagerConfig { MaxWritePoolSize = 1000, MaxReadPoolSize = 1000, AutoStart = true, DefaultDb = 0 }); }
使用Redis的SetNX命令實現加鎖,
/// <summary> /// 加鎖 /// </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方法,並沒有提供設置過期時間的方法,對於加鎖業務又不能分開執行(如果加鎖成功設置過期時間失敗導致的永久死鎖問題),所以就使用腳本實現,解決瞭異常情況死鎖問題.
- 參數key:鎖的key
- 參數selfMark:在設置鎖的時候會產生一個自己的標識,在釋放鎖的時候會用到,所謂解鈴還須系鈴人。防止鎖被誤釋放,導致鎖無效.
- 參數lockExpirySeconds:鎖的默認過期時間,防止被永久死鎖.
- 參數waitLockMilliseconds:循環獲取鎖的等待時間.
如果設置為0,為樂觀鎖機制,獲取不到鎖,直接返回未獲取到鎖.
默認值為long最大值,為悲觀鎖機制,約等於很多很多天,可以理解為一直等待.
釋放鎖
/// <summary> /// 釋放鎖 /// </summary> /// <param name="key">鎖key</param> /// <param name="selfMark">自己標記</param> public 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 }); } }
參數key:鎖的key
參數selfMark:在設置鎖的時候返回的自己標識,用來解鎖自己加的鎖(此值不能隨意傳,必須是加鎖時返回的值)
調用方式
悲觀鎖方式
int num = 10; string lockkey = "xianseng"; //悲觀鎖開啟20個人同時拿寶貝 for (int i = 0; i < 20; i++) { Task.Run(() => { string selfmark = ""; try { if (PublicLockHelper.Lock(lockkey, out selfmark)) { if (num > 0) { num--; Console.WriteLine($"我拿到瞭寶貝:寶貝剩餘{num}個\t\t{selfmark}"); } else { Console.WriteLine("寶貝已經沒有瞭"); } Thread.Sleep(100); } } finally { PublicLockHelper.UnLock(lockkey, selfmark); } }); }
樂觀鎖方式
int num = 10; string lockkey = "xianseng"; //樂觀鎖開啟10個線程,每個線程拿5次 for (int i = 0; i < 10; i++) { Task.Run(() => { for (int j = 0; j < 5; j++) { string selfmark = ""; try { if (PublicLockHelper.Lock(lockkey, out selfmark, 10, 0)) { if (num > 0) { num--; Console.WriteLine($"我拿到瞭寶貝:寶貝剩餘{num}個\t\t{selfmark}"); } else { Console.WriteLine("寶貝已經沒有瞭"); } Thread.Sleep(1000); } else { Console.WriteLine("沒有拿到,不想等瞭"); } } finally { PublicLockHelper.UnLock(lockkey, selfmark); } } }); }
單機隻能用多線模擬使用分佈式鎖瞭
此鎖已經可以滿足大多數場景瞭,若有不妥,還請多多指出,以免誤別人!
(次方案不支持Redis集群,Redis集群不能調用腳本執行)
到此這篇關於C#實現Redis的分佈式鎖的文章就介紹到這瞭,更多相關C# Redis分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 詳解RedisTemplate下Redis分佈式鎖引發的系列問題
- 分佈式面試分佈式鎖實現及應用場景
- 關於SpringBoot 使用 Redis 分佈式鎖解決並發問題
- redis分佈式鎖的8大坑總結梳理
- SpringBoot集成redis實現分佈式鎖的示例代碼