Redis实现分布式锁(SETNX)
作者:mmseoamin日期:2023-12-11

目录

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

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

            Redis实现分布式锁(SETNX),第1张

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

            Redis实现分布式锁(SETNX),第2张

    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 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返回值: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框架提供的分布式锁(公平/非公平)、红锁、联锁的基本使用及效果演示,帮助大家快速熟悉分布式锁,相信一定对大家有所收益,欢迎观看!Redis实现分布式锁(SETNX),第3张https://blog.csdn.net/mlwsmqq/article/details/128469771

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

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

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

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

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