Redis实现分布式锁(SETNX)

这篇具有很好参考价值的文章主要介绍了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

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

        setnx,分布式锁,redis,分布式,java,spring boot,后端

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

        setnx,分布式锁,redis,分布式,java,spring boot,后端

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")
@Slf4j
public 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);

	@Resource
	private RedisTemplate<String ,Object> redisTemplate;

	@GetMapping("/testSetNX")
	@ApiOperation("SETNX")
	public ResultVO<Object> testSetNX(@RequestParam Long goodsId){
		String key = "lock_" + goodsId;
		String value = UUID.randomUUID().toString();
		ValueOperations<String, Object> 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返回值:true
2022-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锁结果:true
2022-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返回值:false
2022-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返回值:true
2022-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次执行结束,有效时间为:30
2022-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次执行结束,有效时间为:30
2022-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次执行结束,有效时间为:30
2022-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锁结果:true
2022-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返回值:false
2022-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返回值:true
2022-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次执行结束,有效时间为:30
2022-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次执行结束,有效时间为:30
2022-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次执行结束,有效时间为:30
2022-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锁结果:true
2022-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返回值:true
2022-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次执行结束,有效时间为:30
2022-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次执行结束,有效时间为:30
2022-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次执行结束,有效时间为:30
2022-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锁结果:true
2022-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://www.toymoban.com/news/detail-565700.html

到了这里,关于Redis实现分布式锁(SETNX)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Redis——》实现分布式锁

    推荐链接:     总结——》【Java】     总结——》【Mysql】     总结——》【Redis】     总结——》【Kafka】     总结——》【Spring】     总结——》【SpringBoot】     总结——》【MyBatis、MyBatis-Plus】     总结——》【Linux】     总结——》【MongoDB】    

    2024年02月10日
    浏览(58)
  • 使用redis实现分布式锁

    在一个分布式系统中,也会涉及多个节点访问同一个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”的问题,而java的synchronized这样的锁只能在当前进程中生效,在分布式的这种多个进程多个主机的场景无能为力,此时就需要分布式锁。 例如

    2024年02月07日
    浏览(54)
  • redis如何实现分布式锁?

    首先,“分布式锁”的概念,是相对“本地锁”而言。 本地锁比如java中的synchronized 这类 JDK 自带的 本地锁 ,来控制一个 JVM 进程内的多个线程对本地共享资源的访问。 同一时刻只有一个线程可以获取到本地锁访问共享资源。 分布式系统下,不同的服务/客户端通常运

    2024年02月06日
    浏览(59)
  • redis实现分布式延时队列

    延时队列是一种特殊的消息队列,它允许将消息在一定的延迟时间后再进行消费。延时队列的主要特点是可以延迟消息的处理时间,以满足定时任务或者定时事件的需求。 总之,延时队列通过延迟消息的消费时间,提供了一种方便、可靠的方式来处理定时任务和定时事件。它

    2024年02月08日
    浏览(44)
  • Redis分布式锁实现原理

    在早期互联网的架构中,一个应用都是单机进行部署,这种情况下,利用JDK提供的锁机制即可解决共享数据在多线程场景下的线程安全问题,但随着技术的发展,分布式系统架构逐渐普及,在分布式架构中,由于一个应用会进行多机部署,服务器实例之间的JVM是互相独立的,

    2024年02月16日
    浏览(40)
  • 分布式锁之redis实现

    需要挂在的data和redis.conf自行创建即可 不要忘记开放端口6379 修改redis.conf配置文件,设置 requirepass xxxxx 如果直接使用RedisTemplate使用的序列化器是jdk的,存的是二进制,使用StringRedisTemplate默认初始化序列化器就是String类型 执行票数存入redis指令  编写代码演示超卖问题  500

    2024年02月10日
    浏览(45)
  • 分布式锁实现(mysql,以及redis)以及分布式的概念

    我旁边的一位老哥跟我说,你知道分布式是是用来干什么的嘛?一句话给我干懵了,我能隐含知道,大概是用来做分压处理的,并增加系统稳定性的。但是具体如何,我却道不出个1,2,3。现在就将这些做一个详细的总结。至少以后碰到面试官可以说上个123。 那么就正式进入

    2024年01月21日
    浏览(58)
  • redis实战-redis实现分布式锁&redisson快速入门

    前言 集群环境下的并发问题  分布式锁 定义 需要满足的条件 常见的分布式锁 redis实现分布式锁 核心思路 代码实现 误删情况 逻辑说明 解决方案 代码实现 更为极端的误删情况 Lua脚本解决原子性问题 分布式锁-redission redisson的概念 快速入门 总结 在前面我们已经实现了单机

    2024年02月09日
    浏览(48)
  • 使用注解实现REDIS分布式锁

    有些业务请求,属于耗时操作,需要加锁,防止后续的并发操作,同时对数据库的数据进行操作,需要避免对之前的业务造成影响。 使用 Redis 作为分布式锁,将锁的状态放到 Redis 统一维护,解决集群中单机 JVM 信息不互通的问题,规定操作顺序,保护用户的数据正确。 梳理

    2024年02月02日
    浏览(36)
  • 解读分布式锁(redis实现方案)

    分布式锁是一种用于分布式系统中的并发控制机制,它用于确保在多个节点或多个进程之间的并发操作中,某些关键资源或代码块只能被一个节点或进程同时访问。分布式锁的目的是避免多个节点同时修改共享资源而导致的数据不一致或冲突的问题。通俗的来说,分布式锁的

    2024年02月15日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包