Redis分布式锁的正确实现方法总结

来源:脚本之家  责任编辑:小易  

男人到了中年,不管是生活还是工作,压力都会很大,上有老下有小,肩负着重大的责任。有很多的男人会使压力变动力,但有的人却因为压力变得堕落和消沉,到最后搞得家庭争吵不断,很不和谐。男到中年,要“不睡三觉,不赚三钱,不做三.爱”!不睡三觉:1、不睡懒觉一年之计在于春,一日之计在于晨!不管你是否事业有成,男到中年,都不要贪恋睡懒觉。如果你生活清闲,事业有成,你应该多花点时间来锻炼,注重自己的身体保养,年轻时事业拼搏肯定让你的身体劳累成疾;如果你依然一事无成,上着朝九晚五的班,那么你除了应该多花点时间锻炼,还应该为自己的工作努力。2、不睡旧情人之觉男到中年,不要因为自己的婚姻不顺就做对不起妻子的事情,永

分布式锁一般有三种实现方式:

在数码相机的功能转盘上,自动档(AUTO)和程序p档其实都是程序化的全自动档,自动档就是简化了的P档。两者之间既有相同之处也有不同之处,具体如下:使用自动档时,机载闪光灯将处于自动闪光状态;而使用程序p档拍摄,则可以随意选择各种闪光模式,其中包括强制闪光、强制不闪光、慢速同步闪光、自动闪光和防红眼闪光等。自动档的感光度是由照相机自动设置的,无法人为设定;而程序p档的感光度可以由拍摄者自由设定,也可以选择感光度自动。自动档的白平衡是由照相机自动设置的,无法人为设定,也不能使用自定义白平衡功能;而程序p档的白平街可以由拍摄者自由设定,其中包括自定义白平衡,也可以选择自动自平衡。Auto是完全的傻瓜

1、数据库乐观锁;

瓷砖电视背景墙的优点就是:1,使用它装修的话会让家里的档次看起来提升很多,可以从视觉上提高空间的宽敞度,也能让居室变得明亮时尚起来。2,因为制作过程是在瓷砖上进行图案烤或雕刻完最后再把颜色弄上去,所以看起来的话视觉是很大的一个享受,基本上颜色不会再变,防潮等不会像壁纸一样的用没多久就潮湿了的特点。3,如果你家里有小孩的话,瓷砖电视墙更耐脏和清洁,不担心破坏居室的美感。而且瓷砖的耐旧度和耐看度很高,即使过了十年,也能亮洁如新。4,还要一点是很重要的由因为是根据您的个性来制作的,完全可以根据您的装修要求和实际尺寸来制作,所以在您附近的大部分地区可以说是独一无二的。缺点就是:1,价格说起来还算不便宜

2、基于Redis的分布式锁;

古人如厕,不滞於物,草木竹石,皆可擦腚。首先是一种叫做厕筹的东西。但厕筹这东西,你要明白,在民间,他不叫厕筹,叫“干屎橛”。干,干预,冒犯。橛,就是橛子,厕简。屎橛子长啥样呢?这样:拉完屎,用橛子一擦,然后把橛子头洗一下,下次再用。无噪音,无污染,利用缝隙求发展。有的则是将竹条断成小短片,擦完就扔,竹子又宽又大的地方,就用这种方法。我画的示意图:然而,事实上,并不是所有人都用厕筹。很多地区的多数百姓,没有用厕筹的习惯,也没有用纸擦腚的家庭条件。说用不起纸,不是开玩笑,是真的。所以夏秋的时候,在野外拉屎,通常就用树叶或者卵石一擦了事,免去诸多烦恼。也有用秸秆或棒子的,刮起来要小心翼翼。你不能说,

3、基于ZooKeeper的分布式锁。

本文将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

1、互斥性。在任意时刻,只有一个客户端能持有锁。

2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>

加锁代码

正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件:

1、首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。

2、其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。

3、最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx()和jedis.expire()组合实现加锁,代码如下:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁 jedis.expire(lockKey, expireTime); } }

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) { long expires = System.currentTimeMillis() + expireTime; String expiresStr = String.valueOf(expires); // 如果当前锁不存在,返回加锁成功 if (jedis.setnx(lockKey, expiresStr) == 1) { return true; } // 如果锁存在,获取锁的过期时间 String currentValueStr = jedis.get(lockKey); if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间 String oldValueStr = jedis.getSet(lockKey, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁 return true; } } // 其他情况,一律返回加锁失败 return false; }

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。

执行过程:

1、通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。

2、如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

那么这段代码问题在哪里?

1、由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。

2、当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。

3、锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。

那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey); }

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 若在此时,这把锁突然不是这个客户端的,则会误解锁 jedis.del(lockKey); } }

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

以上就是脚本之家小编给大家整理的全部相关知识点,感谢大家的学习和支持。

扩展阅读,根据您访问的内容系统为您准备了以下内容,希望对您有帮助。

什么是分布式锁及正确使用redis实现分布式锁

Redis分布式锁的安全性问题,在分布式系统专家和Redis的作者 antirez 之间就发生过一场争论。由于对这个问题一直以来比较关注,所以我前些日子仔细阅读了与这场争论相关的资料。这场争论的大概过程是这样的:

为了规范各家对基于Redis的分布式锁的实现,Redis的作者提出了一个更安全的实现,叫做 Redlock 。

  • 本文相关:
  • java使用redisson分布式锁实现原理
  • java编程redisson实现分布式锁代码示例
  • redis分布式锁的实现方式(redis面试题)
  • springboot redis分布式锁代码实例
  • springboot使用redisson实现分布式锁(秒杀系统)
  • springboot集成redisson实现分布式锁的方法示例
  • mysql借助db实现分布式锁思路详解
  • java redis分布式锁的正确实现方式详解
  • java redisson实现分布式锁原理详解
  • redis配置认证密码的方法
  • 利用redis实现sql伸缩的方法
  • redis执行redis命令的方法教程
  • 编译安装redisd的方法示例详解
  • redis的11种web应用场景简介
  • redis字符串对象实用笔记
  • redis获取某个大key值的脚本实例
  • redis-cli 使用密码登录的实例
  • 详解redis中的双链表结构
  • redis批量删除key的方法
  • 什么是分布式锁及正确使用redis实现分布式锁
  • 免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved