Redis下分布式锁的实现

网友投稿 597 2022-11-06

Redis下分布式锁的实现

Redis下分布式锁的实现

本文可作为redis in action第六章的学习笔记 其实,对redis而言,锁和事务与watch等等是分不开的。 我们先来聊聊事务和watch 事务,在关系型事务上的意思就是:一个事务内的sql,要不全部都执行成功,要么全部都不执行。 不过redis的事务只能部分满足"一荣俱荣,一损俱损"的特性。 怎么说? 在关系型数据库中,一个事务内部如果发生了错误,所有sql就都回滚到初始状态 在redis的事务中,所谓的错误,至少分两种: 1 语法错误 例如 我把set dlf abc 写成了sett dlf abc 2 运行错误 dlf这是个String类型的key,sadd这是对set类型数据做操作的 命令:sadd dlf kkk也就会出错,这就是运行错误 如果在一个事务中,一共三个命令,第二个命令有语法错误,那么三条命令就等于都没有执行。 如果在一个事务中,一共三个命令,第二个命令有运行错误,那么第一三条命令还是执行了的。 所以大家得尽力解决运行错误,你得记得每一个键都是什么类型的。 我记不住呀! 你说你记不住?那你还写什么代码?不会回家看孩子去。 watch命令 watch一般也是跟实物连用的。 线程A,watch某个数据后,如果在线程A执行exec之前,线程B修改了那个数据,那么线程A的事务就会失败。 与此同时,线程B修改的那个数据也已经进入redis了。 所以一般情况下,watch都会包含在一个while循环中。

while (System.currentTimeMillis() < end) { conn.watch(inventory); Transaction trans = conn.multi(); //...进行事务操作 List results = trans.exec(); // 如果返回的是null 就说明因为watch的域被改变了 // 事务也就被打断了 if (results == null){ continue; } return true; }

那while有什么问题呢?

如果负载很大,事务就会不断的重试!

数据库的那种锁叫悲观锁,我用的时候你不能用。

redis的这个watch机制,叫做乐观锁,就是假定不会有人来打扰我,如果有人打扰(修改了我要操作的数据)我了,那就以别人的数据为准,我再做一遍。

那有什么办法呢?

锁!!

另一方面

我们抛开redis暂且不谈,在单机情况下,java的synchronized关键词能保证同步性。那么多机下呢?

就得用分布式锁了。

也就说,不管是从redis的watch的多次重试上来说,还是从多机互斥上来说,我们都得有一个能支持分布式的不会重试的锁!

OK,我们先说一个命令

SETNX命令(SET if Not eXists)

语法:

SETNX key value

功能:

当且仅当 key 不存在,将key的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

我们设计的redis锁,准确的说保存的就是一个字符串

key是锁的名字,value是一个uuid

OK,开始上代码

/** * 如果过了acquireTimeout时间后,我还没有获得锁,我就放弃了 * 同时直接返回null * 用户获得锁以后,最多使用lockTimeout长时间 过了之后 * 别的客户端 也就能取到锁了 */ public String acquireLockWithTimeout( Jedis conn, String lockName, long acquireTimeout, long lockTimeout) { // String identifier = UUID.randomUUID().toString(); String lockKey = "lock:" + lockName; int lockExpire = (int)(lockTimeout / 1000); long end = System.currentTimeMillis() + acquireTimeout; while (System.currentTimeMillis() < end) { //等于1 就说明之前并没有这个key if (conn.setnx(lockKey, identifier) == 1){ conn.expire(lockKey, lockExpire); return identifier; } //代码运行到这里 说明锁已经被别人拿走了 //等于-1 表示之前也没有设置超时时间 if (conn.ttl(lockKey) == -1) { conn.expire(lockKey, lockExpire); } try {//等1毫秒再试 Thread.sleep(1); }catch(InterruptedException ie){ Thread.currentThread().interrupt(); } } //别人拿到锁了 我等的时间太久了 老子不等了 return null; }

如果返回的值不为null,那么就说明获得锁了,而且获得的那个identifier,在释放锁的时候也有用。

下面就是释放锁了:

/** * 释放成功 返回true 反之返回false * @param conn * @param lockName * @param identifier * @return */ public boolean releaseLock(Jedis conn, String lockName, String identifier) { String lockKey = "lock:" + lockName; while (true){ conn.watch(lockKey); //如果不相等 会怎么样 为什么有这一步的检查 if (identifier.equals(conn.get(lockKey))){ Transaction trans = conn.multi(); trans.del(lockKey); List results = trans.exec(); if (results == null){ continue; } return true; } //unwatch 没有进入事务 就得手动unwatch conn.unwatch(); break; } return false; }

我自己再看到这个代码的时候,很疑惑identifier.equals(conn.get(lockKey)这个是干什么?

就算别人把我的锁的identifier从2dsafd改成了2fsae,能咋么?我直接删除了就是了么。反正只要我不还锁,别人都无法获得锁,能有啥问题么。

假如线程A再2:15获得了锁,并且锁的lockTimeout是3分钟 返回的uuid(就是那个identifier)假如是abc

2:16的时候,线程b想要获得锁,那肯定是获得不了的,线程b阻塞到那了。

到2:18的时候,线程A还没有主动释放锁,但是锁的过期时间已经到了,redis已经删除了那个锁

到2:19的时候线程b就已经获得锁了,返回的uuid假如是edf,并且过期时间也是3分钟,在2:22之前,理论上,只有线程b持有锁。

然后到2:20的时候,线程A来释放锁。如果不检查两个identifier是否相等,线程a就把线程b的锁给删除了

然后2:21线程c就获得了自己本不应该获得的锁(此时线程锁还应该在线程b手上)

亲爱的朋友们,你们明白了么。

那具体怎么使用这个锁呢?

String locked = acquireLockWithTimeout(conn, lockName,1000); //你的代码 releaseLock(conn, identifier, locked);

参考资料

​​http://qifuguang.me/2015/09/30/Redis%E4%BA%8B%E5%8A%A1%E4%BB%8B%E7%BB%8D/​​

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:常用js总结
下一篇:Mybatis应用(oracle)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~