聲明:
這里首先使用的是csredis,地址是https://github.com/2881099/csredis
該庫本身已經足夠完善,這里我畫蛇添足一下,為了方便自己的使用。
本身csredis庫已經實現了完整的加鎖和去鎖的邏輯,這里實現的與庫本身所實現的有以下幾點區別(csredis實現代碼位置為:https://github.com/2881099/csredis/blob/bb6d947695770333027f3936f80052041db41b64/src/CSRedisCore/CSRedisClient.cs#L4344,有興趣可以去了解看下)
1. 去掉了csredis的鎖續租部分的功能,盡量簡化
2. 將鎖的token的設定交給外部,使用guid也罷,使用id也行。通過已知的token,保證了你可以在任意地方以觀察者的身份釋放鎖。
3. 盡量不修改其key的原本值,不添加前綴,防止在觀測時出現不必要的麻煩。
邏輯:
加鎖就是set 一個 key ,如果key 存在的情況下則返回失敗。那么典型的命令就是setnx.
一個鎖顯然是需要一個過期時間的,那么我們可能要用到 expire命令。
釋放鎖則是一個del命令
查看鎖的值是需要get命令
比較常見的加鎖使用的是setnx,不過由於redis支持了SET key token NX EX/PX max-lock-time(sec/millsec) (設置key token 是否不存在才set 秒數模式/毫秒數模式 秒數或毫秒數) 這種傳參模式,由此,這里更加推薦使用set 命令。
如果在我們的代碼端執行del 則小概率發生以下情況:
A 申請鎖set x,過期時間為t。
經過時間t后,A恰好忙完了,A通過get命令看看token是否一致,得到結果發現一致的。
A決定發送del到redis服務器,此時A恰好網絡擁堵。
redis服務器由於鎖x超時,進而釋放了鎖x。
此時B恰好也申請了鎖x,無過期時間。
A網絡恢復,del命令發送成功。
結果 B的鎖被A釋放了。
幸好redis支持了lua腳本。讓我們得以簡單的實現過期,加鎖,去鎖功能,而不需要自己手動timer過期。
這里要使用到eval命令執行腳本。
代碼
using CSRedis; using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace CsRedis.Helper { /// <summary> /// 基於csredis的簡單封裝 /// </summary> public class CsRedisManager { private ConcurrentDictionary<string, CSRedisClient> _serviceNameWithClient; /// <summary> /// 初始化 /// </summary> public void Init() { _serviceNameWithClient = new ConcurrentDictionary<string, CSRedisClient>(); } /// <summary> /// 獲取業務redis服務 /// </summary> /// <param name="serviceName"></param> /// <returns></returns> public CSRedisClient GetRedisClient(string serviceName) { CSRedisClient result = null; _serviceNameWithClient.TryGetValue(serviceName,out result); return result; } /// <summary> /// 添加redis服務 /// </summary> /// <param name="serviceName"></param> /// <param name="connectStr"></param> /// <returns></returns> public bool AddRedisClient(string serviceName,string connectStr) { CSRedisClient cSRedisClient = new CSRedisClient(connectStr); return _serviceNameWithClient.TryAdd(serviceName, cSRedisClient); } /// <summary> /// 設置字符串型kv /// </summary> /// <param name="key">key</param> /// <param name="value">value</param> /// <param name="expireSecond">過期時間(秒)</param> /// <returns>是否成功</returns> public bool Set(string serviceName,string key,string value,int expireSecond=-1) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); return redisClient.Set(key, value, expireSecond); } /// <summary> /// 獲取相應key的值 /// </summary> /// <param name="key"></param> /// <returns></returns> public string Get(string serviceName, string key) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); return redisClient.Get(key); } /// <summary> /// 如果不存在則執行,存在則忽略 /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public bool SetNx(string serviceName, string key, string value) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); var res = redisClient.SetNx(key, value); return res; } /// <summary> /// 帶過期時間的setNx /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="seconds"></param> /// <returns></returns> public bool SetNx(string serviceName, string key, string value, int millSeconds = -1) { var res = Set(serviceName, key, value, RedisExistence.Nx, millSeconds); return res; } /// <summary> /// 帶過期時間的SetXx /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="seconds"></param> /// <returns></returns> public bool SetXx(string serviceName, string key, string value, int millSeconds = -1) { var res = Set(serviceName, key, value, RedisExistence.Xx, millSeconds); return res; } /// <summary> /// 帶參數set /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="existence"></param> /// <param name="seconds"></param> /// <returns></returns> public bool Set(string serviceName, string key, string value, RedisExistence existence, int millSeconds = -1) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); var res = redisClient.Set(key, value, millSeconds, existence); return res; } /// <summary> /// 設置生存時間 /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <param name="seconds"></param> /// <returns></returns> public bool Expire(string serviceName, string key, int seconds) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); return redisClient.Expire(key, seconds); } /// <summary> /// 獲取剩余的生存時間(秒) /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <returns></returns> public long Ttl(string serviceName, string key) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); return redisClient.Ttl(key); } /// <summary> /// 刪除del /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <returns></returns> public long Del(string serviceName,params string[] keys) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); return redisClient.Del(keys); } /// <summary> /// 執行腳本 /// </summary> /// <param name="serviceName"></param> /// <param name="script"></param> /// <param name="key"></param> /// <param name="args"></param> /// <returns></returns> public object Eval(string serviceName, string script,string key,params object[] args) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); var res = redisClient.Eval(script, key,args); return res; } /// <summary> /// 添加共享鎖 /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <param name="seconds"></param> /// <returns></returns> public bool AddLock(string serviceName, string key,string token, int millSeconds = -1) { var valRes = SetNx(serviceName, key, token, millSeconds); return valRes; } /// <summary> /// 刪除共享鎖 /// </summary> /// <param name="serviceName"></param> /// <param name="key"></param> /// <returns></returns> public bool ReleaseLock(string serviceName, string key,string token) { var script = GetReleaseLockScript(); var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); var res = redisClient.Eval(script, key, token); if (0== (long)res) { return false; } return true; } /// <summary> /// 獲取鍵值 /// </summary> /// <param name="serviceName"></param> /// <param name="pattern"></param> /// <returns></returns> public string[] Keys(string serviceName, string pattern) { var redisClient = GetRedisClient(serviceName); GetExceptionOfClient(redisClient); var res = redisClient.Keys(pattern); return res; } /// <summary> /// 獲取client發生異常 /// </summary> /// <param name="client"></param> private void GetExceptionOfClient(CSRedisClient client) { if (client == null) { throw new Exception("無有效的redis服務"); } } /// <summary> /// lua腳本刪除共享鎖 /// 解決在A申請鎖 xxkey 過期的瞬間,B 申請鎖xxkey, /// 此時恰好A執行到釋放xxkey從而引起的異常釋放 /// </summary> /// <returns></returns> private static string GetReleaseLockScript() { return "if redis.call(\"get\",KEYS[1]) == ARGV[1] \nthen\nreturn redis.call(\"del\", KEYS[1])\nelse\nreturn 0\nend"; } } }
這里我把要單獨執行的lua腳本單獨提出來
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
這段腳本對應的是c# 中的 GetReleaseLockScript()方法中的文字。
這里我個人偷了個懶,按照道理,這里應該有個LoadScriptPath,加載腳本所在位置,調用的時候先檢查腳本是否在內存中,不在則去LoadScriptPath找對應的腳本,方便不同的人協同合作。不過那個就是腳本管理器了,還要設計interface,有點偏離主題了。
下面是測試代碼
using CsRedis.Helper; using NUnit.Framework; namespace TestProject { public class Tests { [SetUp] public void Setup() { } [Test] public void Test1() { CsRedisManager csRedisManager = new CsRedisManager(); csRedisManager.Init(); csRedisManager.AddRedisClient("TEST", "127.0.0.1:6379,password=123456, connectTimeout =1000,connectRetry=1,syncTimeout=10000,defaultDatabase=0"); //csRedisManager.AddRedisClient("PRODUCT", "127.0.0.1:6379,password=123456, connectTimeout =1000,connectRetry=1,syncTimeout=10000,defaultDatabase=1"); var token = "123"; var lockKey = "LOCKKEY1"; csRedisManager.AddLock("TEST", lockKey,token,20 * 1000); csRedisManager.ReleaseLock("TEST", lockKey, token); } } }
這里就是對於共享鎖的一點簡單實現,多了挺多與本次的命令無關的代碼,海涵海涵