我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Redis实现分布式锁(SETNX)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Redis实现分布式锁(SETNX)

目录

1、什么是分布式锁

2、分布式锁应具备的条件        

3、为什么使用分布式锁

4、SETNX介绍

5、分布式锁实现

6、效果演示

7、Redisson分布式锁详解

8、Lua脚本实现可重入分布式锁


1、什么是分布式锁

        分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

        在分布式系统中,常常需要协调他们的动作,若不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

2、分布式锁应具备的条件        

  • 在分布式系统环境下,一段代码在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性(一个线程多次获取同一把锁)
  • 具备锁失效机制,即自动解锁,防止死锁
  • 具备非阻塞特性,即没有获取到锁将直接返回获取锁失败

3、为什么使用分布式锁

        提起synchronized和Lock想必大家都不陌生,可以做到线程间的同步,但仅限于单机应用,在分布式集群系统中用来协调共享资源的时候肯定是不行的;例如下单减库存的操作,使用synchronized进行加锁,部署三台服务,若此时商品库存只有一个,同时刻有三个下单请求分别到三台服务上处理,这时三个请求都能抢到锁去下单减库存,就很可能出现超卖的情况,使用分布式锁便可避免此问题发生

4、SETNX介绍

        Redis实现分布式锁的核心便在于SETNX命令,它是SET if Not eXists的缩写,如果键不存在,则将键设置为给定值,在这种情况下,它等于SET;当键已存在时,不执行任何操作;成功时返回1,失败返回0

        使用示例:两次插入相同键不同值,第一次返回成功,第二次返回失败

        

        也可使用set命令实现跟SETNX一样的效果,还能设置过期时间

        

set命令介绍:

    SET key value [EX seconds] [PX milliseconds] [NX|XX]
    生存时间(TTL,以秒为单位)
    Redis 2.6.12 版本开始:(等同SETNX 、 SETEX 和 PSETEX)
    EX second :设置键的过期时间为 second 秒,SET key value EX second 效果等同于 SETEX key second value 。
    PX millisecond :设置键的过期时间为millisecond毫秒,SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    NX :只在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value 。
    XX :只在键已经存在时,才对键进行设置操作。

5、分布式锁实现

@Api(tags = "Redis")@RestController@RequestMapping("/testRedis")@Slf4jpublic class TestRedisController {private static final ThreadFactory THREAD_FACTORY = new ThreadFactoryBuilder().setNamePrefix("shouhu-").setDaemon(true).build();private static final ScheduledExecutorService daemonPool = Executors.newScheduledThreadPool(5,THREAD_FACTORY);@Resourceprivate RedisTemplate redisTemplate;@GetMapping("/testSetNX")@ApiOperation("SETNX")public ResultVO testSetNX(@RequestParam Long goodsId){String key = "lock_" + goodsId;String value = UUID.randomUUID().toString();ValueOperations valueOperations = redisTemplate.opsForValue();ScheduledFuture scheduledFuture = null;try {// 加锁Boolean ifAbsent = valueOperations.setIfAbsent(key, value, 30, TimeUnit.SECONDS);log.info("加锁{}返回值:{}",key,ifAbsent);if ((null==ifAbsent) || (!ifAbsent)){log.info("加锁失败,请稍后重试!");return ResultUtils.error("加锁失败,请稍后重试!");}// 模拟看门狗逻辑AtomicInteger count = new AtomicInteger(1);scheduledFuture = daemonPool.scheduleWithFixedDelay(() -> {log.info("看门狗第:{}次执行开始", count.get());Object cache = redisTemplate.opsForValue().get(key);if (Objects.nonNull(cache) && (value.equals(cache.toString()))) {// 重新设置有效时间为30秒redisTemplate.expire(key, 30, TimeUnit.SECONDS);log.info("看门狗第:{}次执行结束,有效时间为:{}", count.get(), redisTemplate.getExpire(key));}else {log.info("看门狗执行第:{}次异常:key:{} 期望值:{} 实际值:{}",count.get(), key, value, cache);}count.incrementAndGet();}, 10, 10, TimeUnit.SECONDS);// 执行业务逻辑TimeUnit.SECONDS.sleep(5);log.info("业务逻辑执行结束");}catch (Exception e){log.error("testSetNX exception:",e);return ResultUtils.sysError();}finally {// 释放锁,判断是否是当前线程加的锁String delVal = valueOperations.get(key).toString();if (value.equals(delVal)){Boolean delete = redisTemplate.delete(key);log.info("释放{}锁结果:{}",key,delete);// 关闭看门狗线程if (Objects.nonNull(scheduledFuture)){boolean cancel = scheduledFuture.cancel(true);log.info("关闭看门狗结果:{}",cancel);}}else {log.info("不予释放,key:{} value:{} delVal:{}",key,value,delVal);}}return ResultUtils.success("success");}} 

上面是最终实现,其中有几个需要注意的地方:

(1)防止解锁失败:如拿到锁后执行业务逻辑时一旦出现异常就无法释放锁,解决这个问题只需将释放锁的逻辑放入finally代码块中即可,无论是否有异常都会释放锁

(2)设置锁的有效期:虽然将释放锁的逻辑放在finally代码块中,但并不能达到锁失效机制要求的目标,如拿到锁的线程在执行业务过程中遇到服务重启、宕机等情况无法释放锁,锁便会一直存在,导致其它线程无法获取到那问题就大了;解决这个问题我们可以给锁设置过期时间,即便出现上述问题超时也能自动释放锁,不影响其它请求往下执行,那来看看下面的写法是否可行:

Boolean ifAbsent = valueOperations.setIfAbsent(key, value);redisTemplate.expire(key,30,TimeUnit.SECONDS);

 这样可以实现设置锁的过期时间,但是加锁和设置过期时间不是原子操作,在加锁成功之后,即将执行设置过期时间的时候系统发生崩溃还是会死锁;其实实现原子性有现成的接口,如下:

Boolean ifAbsent = valueOperations.setIfAbsent(key, value, 30, TimeUnit.SECONDS);

(3)防止误删锁:若锁的过期时间为10s,A线程抢到锁执行业务逻辑但执行了12s,在第10s时锁过期自动删除,B线程立马拿到锁执行业务,到第12s时A线程执行完去释放锁,但锁已经不是A的,A线程把B线程的锁释放了,那B线程不就无锁裸奔了,所以我们可以在加锁的时候把值设置为唯一的,如UUID、雪花算法等方式,释放锁时获取锁的值判断是不是当前线程设置的值,如果是再去删除 

(4)Watch Dog机制:也叫看门狗,旨在延长锁的过期时间;为什么要这么做呢?比如把锁的过期时间设为10秒,但拿到锁的线程要执行20秒才结束,锁超时自动释放其它线程便能获取到,这是不被允许的,所以看门狗就闪亮登场了;它的大概流程是在加锁成功后启动一个监控线程,每隔1/3的锁的过期时间就去重置锁过期时间,比如说锁设置为30秒,那就是每隔10秒判断锁是否存在,存在就去延长锁的过期时间,重新设置为30秒,业务执行结束关闭监控线程;这样就解决了业务未执行完锁被释放的问题,本文使用ScheduleThreadPool线程池模拟实现看门狗功能,每隔10秒去重置锁的过期时间。(真正的看门狗实现肯定比本文中的复杂完善很多,本文只是阐述这种思想,大家不要被带跑偏,个人练习可以,但不要在项目中使用!)

6、效果演示

        使用8701、8702端口同时启动两个服务,传入相同的参数,快速向两个服务各调用一次

        8701服务结果:

2022-12-30 17:54:43.339  INFO 9832 --- [nio-8701-exec-9] c.e.l.c.testRedis.TestRedisController    : 加锁lock_1返回值:true2022-12-30 17:54:48.340  INFO 9832 --- [nio-8701-exec-9] c.e.l.c.testRedis.TestRedisController    : 业务逻辑执行结束2022-12-30 17:54:48.343  INFO 9832 --- [nio-8701-exec-9] c.e.l.c.testRedis.TestRedisController    : 释放lock_1锁结果:true2022-12-30 17:54:48.343  INFO 9832 --- [nio-8701-exec-9] c.e.l.c.testRedis.TestRedisController    : 关闭看门狗结果:true

        8702服务结果:

2022-12-30 17:54:43.985  INFO 12068 --- [nio-8702-exec-8] c.e.l.c.testRedis.TestRedisController    : 加锁lock_1返回值:false2022-12-30 17:54:43.985  INFO 12068 --- [nio-8702-exec-8] c.e.l.c.testRedis.TestRedisController    : 加锁失败,请稍后重试!2022-12-30 17:54:43.986  INFO 12068 --- [nio-8702-exec-8] c.e.l.c.testRedis.TestRedisController    : 不予释放,key:lock_1 value:d1dd2cd5-933f-4d31-9f17-cb9ebc0fbcde delVal:25990d37-79f2-456e-b760-a4c4bd42046d

        从上述日志可看出8701服务获取成功,8702服务获取失败,已达到分布式锁的效果

        接下来我们把睡眠时间改为40s,验证下看门狗机制是否生效

        8701服务结果:

2022-12-30 18:01:50.471  INFO 2660 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : 加锁lock_1返回值:true2022-12-30 18:02:00.472  INFO 2660 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:1次执行开始2022-12-30 18:02:00.500  INFO 2660 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:1次执行结束,有效时间为:302022-12-30 18:02:10.501  INFO 2660 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:2次执行开始2022-12-30 18:02:10.504  INFO 2660 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:2次执行结束,有效时间为:302022-12-30 18:02:20.505  INFO 2660 --- [       shouhu-1] c.e.l.c.testRedis.TestRedisController    : 看门狗第:3次执行开始2022-12-30 18:02:20.508  INFO 2660 --- [       shouhu-1] c.e.l.c.testRedis.TestRedisController    : 看门狗第:3次执行结束,有效时间为:302022-12-30 18:02:30.473  INFO 2660 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : 业务逻辑执行结束2022-12-30 18:02:30.477  INFO 2660 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : 释放lock_1锁结果:true2022-12-30 18:02:30.477  INFO 2660 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : 关闭看门狗结果:true

        8702服务结果:

2022-12-30 18:01:51.931  INFO 10492 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : 加锁lock_1返回值:false2022-12-30 18:01:51.933  INFO 10492 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : 加锁失败,请稍后重试!2022-12-30 18:01:51.957  INFO 10492 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : 不予释放,key:lock_1 value:9795f2b2-1f57-4878-a399-5ba4bed80e7c delVal:ff451e43-483e-4e85-8f0e-dbdd5c8d7aeb

        从日志可看出8701服务获取锁成功,在执行业务逻辑期间看门狗线程不断的延长锁的过期时间,使得业务完整执行,在此期间锁没有失效或被其它线程获得,说明看门狗是发挥出作用啦;而8702服务加锁失败直接返回,跟预期一致

        下面我们传入不同的参数,看看两把锁同时执行是否正常

        8701服务结果:

2022-12-30 18:11:37.191  INFO 2660 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : 加锁lock_1返回值:true2022-12-30 18:11:47.192  INFO 2660 --- [       shouhu-2] c.e.l.c.testRedis.TestRedisController    : 看门狗第:1次执行开始2022-12-30 18:11:47.195  INFO 2660 --- [       shouhu-2] c.e.l.c.testRedis.TestRedisController    : 看门狗第:1次执行结束,有效时间为:302022-12-30 18:11:57.197  INFO 2660 --- [       shouhu-2] c.e.l.c.testRedis.TestRedisController    : 看门狗第:2次执行开始2022-12-30 18:11:57.199  INFO 2660 --- [       shouhu-2] c.e.l.c.testRedis.TestRedisController    : 看门狗第:2次执行结束,有效时间为:302022-12-30 18:12:07.200  INFO 2660 --- [       shouhu-2] c.e.l.c.testRedis.TestRedisController    : 看门狗第:3次执行开始2022-12-30 18:12:07.235  INFO 2660 --- [       shouhu-2] c.e.l.c.testRedis.TestRedisController    : 看门狗第:3次执行结束,有效时间为:302022-12-30 18:12:17.192  INFO 2660 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : 业务逻辑执行结束2022-12-30 18:12:17.193  INFO 2660 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : 释放lock_1锁结果:true2022-12-30 18:12:17.193  INFO 2660 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : 关闭看门狗结果:true

        8702服务结果:

2022-12-30 18:11:36.656  INFO 10492 --- [nio-8702-exec-3] c.e.l.c.testRedis.TestRedisController    : 加锁lock_2返回值:true2022-12-30 18:11:46.657  INFO 10492 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:1次执行开始2022-12-30 18:11:46.666  INFO 10492 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:1次执行结束,有效时间为:302022-12-30 18:11:56.666  INFO 10492 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:2次执行开始2022-12-30 18:11:56.668  INFO 10492 --- [       shouhu-0] c.e.l.c.testRedis.TestRedisController    : 看门狗第:2次执行结束,有效时间为:302022-12-30 18:12:06.669  INFO 10492 --- [       shouhu-1] c.e.l.c.testRedis.TestRedisController    : 看门狗第:3次执行开始2022-12-30 18:12:06.707  INFO 10492 --- [       shouhu-1] c.e.l.c.testRedis.TestRedisController    : 看门狗第:3次执行结束,有效时间为:302022-12-30 18:12:16.657  INFO 10492 --- [nio-8702-exec-3] c.e.l.c.testRedis.TestRedisController    : 业务逻辑执行结束2022-12-30 18:12:16.660  INFO 10492 --- [nio-8702-exec-3] c.e.l.c.testRedis.TestRedisController    : 释放lock_2锁结果:true2022-12-30 18:12:16.661  INFO 10492 --- [nio-8702-exec-3] c.e.l.c.testRedis.TestRedisController    : 关闭看门狗结果:true

        从日志可看出两把锁独立作用,未发现异常,达到预期的效果

        温馨提示:本文主要阐述分布式锁的思路,代码实现上还有漏洞,如果大家需要用到分布式锁可以考虑使用Redisson或zookeeper

7、Redisson分布式锁详解

        关于开源框架Redisson的使用,可参考我的另一篇博客:

Redisson分布式锁详解(非公平、公平、红锁、联锁)_mlwsmqq的博客-CSDN博客本文讲解了Redisson框架提供的分布式锁(公平/非公平)、红锁、联锁的基本使用及效果演示,帮助大家快速熟悉分布式锁,相信一定对大家有所收益,欢迎观看!https://blog.csdn.net/mlwsmqq/article/details/128469771

8、Lua脚本实现可重入分布式锁

Lua脚本实现可重入分布式锁_mlwsmqq的博客-CSDN博客提到分布式锁,那一定绕不开Redisson,在深入Redisson源码时发现它使用了大量的lua脚本,为什么要使用lua脚本呢?答案就是它能够保证Redis操作的原子性;受到Redisson的启发,本文将带领大家一步步的通过lua脚本实现可重入分布式锁,还有两篇关于分布式锁的博客供大家参考。https://blog.csdn.net/mlwsmqq/article/details/128472150

        有任何错误,欢迎大家指正!

        转载请注明出处!转载请注明出处!

        若本文对大家有所启示,请动动小手点赞和收藏哦!!!

来源地址:https://blog.csdn.net/mlwsmqq/article/details/127723729

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Redis实现分布式锁(SETNX)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

详解使用Redis SETNX 命令实现分布式锁

使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法。 SETNX命令简介 命令格式SETNX key value将 key 的值设为 value,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不
2022-06-04

Redis实现分布式锁

单体锁存在的问题 在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。 (下述实例是一个简单的下单问题:从redis中获取库存,检查库存是否够,>0才允许下单) 我们的解决办法通常是加锁。如下加单体锁
2023-08-16

python实现redis分布式锁

#!/usr/bin/env python# coding=utf-8import timeimport redisclass RedisLock(object): def __init__(self, key): se
2023-01-31

Redis分布式锁的实现方式

目录一、分布式锁是什么1、获取锁2、释放锁二、代码实例上面代码存在锁误删问题:三、基于SETNX实现的分布式锁存在下面几个问题1、不可重入2、不可重试3、超时释放4、主从一致性四、Redisson实现分布式锁1、pom2、配置类3、测试类五
2023-04-03

Redis怎么实现分布式锁

这篇文章主要介绍“Redis怎么实现分布式锁”,在日常操作中,相信很多人在Redis怎么实现分布式锁问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Redis怎么实现分布式锁”的疑惑有所帮助!接下来,请跟着小编
2023-06-02

Redis分布式锁之红锁的实现

目录一、问题二、办法三、原理四、实战一、问题分布式锁,当我们请求一个分布式锁的时候,成功了,但是这时候slave还没有复制我们的锁,masterDown了,我们的应用继续请求锁的时候,会从继任了master的原slave上申请,也会成功。
2022-08-09

基于Redis实现分布式锁

我们知道分布式锁的特性是排他、避免死锁、高可用。分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是序列中最小的节点,如
基于Redis实现分布式锁
2017-09-11

Redis实现分布式锁详解

目录一、前言为什么需要分布式锁?二、基于Redis实现分布式锁为什么redis可以实现分布式锁?如何实现?锁的获取锁的释放三、如何避免死锁?锁的过期时间如何设置?避免死锁锁过期处理释放其他服务的锁如何处理呢?那么redis宕机了呢?四、Re
2023-04-09

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录