c# 理解csredis实现分布式锁
作者:互联网
声明:
这里首先使用的是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); } } }
这里就是对于共享锁的一点简单实现,多了挺多与本次的命令无关的代码,海涵海涵
标签:string,c#,redisClient,csredis,serviceName,key,var,public,分布式 来源: https://www.cnblogs.com/lsnct/p/15866860.html