基于Redis的分布式锁
分布式锁可以基于很多种方式实现,比如zookeeper、redis...。基本原理用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
上次搞了zookeeper,今天把redis的也给补上,demo写完好一阵了,一直没发出来,当时写完也没测试,今天测试测试修补修补就给放出来,大多也是参照java的版本去仿着改成c#版本的,感觉原理都是相同的,就是语法啥的有些不同而已,用的还是StackExchange.Redis。(demo见结尾)
1.redis的锁主要基于setNX跟getSET命令的
1.1 setNX(SET if Not eXists),字面意思(如果不存在,则 SET),如果存在,那就不SET了。
命令:SETNX key value 返回值:写成功 1 ,不成功 0 (是不是相当好判断)
1.2 GETSET ,将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
命令:GETSET key value 返回值:写成功,返回给定 key 的旧值,当 key 没有旧值时,也即是, key 不存在时,返回 nil 。
2.了解完这两个命令,可以把思路先理一下!
比如我们要对某个商品进行加锁操作,暂定为 product112233 112233为商品编号(<current Unix time + lock timeout + 1> 为过期时间)。
步骤1. 首先要操作啦,先加锁
执行命令SETNX product112233 <current Unix time + lock timeout + 1>
返回1 加锁成功 继续业务...... 完成后解锁(解锁最后再说)
返回0 加锁失败
步骤2. 上面加锁失败了,看来是已经有人在占用112233的资源,那我们看看它的过期时间
执行命令Get product112233 返回了过期时间 这时候对比下当前时间 看看,无非也就两种结果
过期了 再加锁 come on
执行命令 GetSet product112233 <current Unix time + lock timeout + 1>
返回 oldValue 对比下 oldValue
是你set的Value 恭喜加锁成功 继续业务...... 完成后解锁(解锁最后再说)
不是你set的Value 加锁又没成功 跟没过期一样 反正就是没获取锁
没过期
一定时间间隔再去尝试加锁(就是上面步骤再来一次),总体时间限制有一个限定,超过时间锁还没拿到那就没办法了,提示加锁失败......
步骤3. 释放锁 释放锁就是删除锁
删除锁DEL,需要先判断锁是否已超时,如果锁已超时,那锁有可能被其他进程获得了,也就不属于你了,这时候直接del,你把别人的锁给del了,那可就不太好了
3.ok,整体思路就表述完了,下面给出代码实现。
简单贴一下关于操作redis的几个Api
/// <summary> /// 操作在设置键的值的同时,还会返回键的旧值 /// </summary> /// <param name="key">键值</param> /// <param name="val">新值</param> /// <returns>旧值</returns> public RedisValue StringGetSet(string key, string val) { key = AddSysCustomKey(key); return Do(db => db.StringGetSet(key, val)); } /// <summary> /// SET if Not eXists 将 key 的值设为 value,当且仅当 key 不存在 /// 若给定的 key 已经存在,则 SETNX 不做任何动作。 /// </summary> /// <param name="key"></param> /// <param name="val"></param> /// <returns>- 1,当 key 的值被设置 - 0,当 key 的值没被设置</returns> public string StringSETNX(string key, string val) { key = AddSysCustomKey(key); RedisResult RResult = Do(db => db.ScriptEvaluate( LuaScript.Prepare(" local lockSet = redis.call('setnx', @key, @val) return lockSet"), new { key, val })); return RResult.ToString(); } /// <summary> /// 执行lua脚本 /// </summary> /// <param name="luascript"></param> /// <param name="parameters"></param> /// <returns></returns> public string ScriptEvaluate(string luascript, object parameters) { RedisResult RResult = Do(db => db.ScriptEvaluate( LuaScript.Prepare(luascript), parameters)); return RResult.ToString(); }
关于操作锁的几个action
/// <summary> /// 获取锁 /// </summary> /// <param name="lockkey">锁的key值</param> /// <param name="expireMsecs">锁到期时间</param> /// <returns></returns> public bool tryGetLock(string lockkey, int expireMsecs) { try { int timeout = _timeoutMsecs; while (timeout >= 0) { long expires = DateTime.Now.DateTimeToUnixTimestamp() + expireMsecs + 1; String expiresStr = expires.ToString(); //锁到期时间 RedisHelper Redis = new RedisHelper(0); if (Redis.StringSETNX(lockkey, expiresStr) == "1") { return true; } String currentValueStr = Redis.StringGet(lockkey); //redis里的时间 if (currentValueStr != null && long.Parse(currentValueStr) < DateTime.Now.DateTimeToUnixTimestamp()) { //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的 String oldValueStr = Redis.StringGetSet(lockkey, expiresStr); //获取上一个锁到期时间,并设置现在的锁到期时间, //只有一个线程才能获取上一个线上的设置时间,因为getSet是同步的 if (oldValueStr != null && oldValueStr.Equals(currentValueStr)) { //如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁 return true; } } timeout -= 100; Thread.Sleep(100); } } catch (Exception) { unlock(lockkey); } return false; } /// <summary> /// 解除锁占用 /// </summary> /// <param name="lockkey"></param> public void unlock(string lockkey) { //在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。如果锁已超时, //那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。 RedisHelper Redis = new RedisHelper(0); String currentValueStr = Redis.StringGet(lockkey); //redis里的时间 if (currentValueStr != null && long.Parse(currentValueStr) < DateTime.Now.DateTimeToUnixTimestamp()) { Redis.KeyDelete(lockkey); } }
测试用了两个demo
最后的测试结果就不贴了,自己想知道的下demo测试测试吧哈!
要开始清明假期了,大家清明节快乐哈!
作者:_Burt
出处:http://www.cnblogs.com/Burt/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。