本文介绍: 本文详细介绍了什么是分布式锁、分布式锁的特征应用场景;一步一步的手动实现分布式锁,分析其中需要特别注意的地方,带着大家理清其中的思路;相信对大家会有所帮助

目录

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")
@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&gt; 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) &amp;&amp; (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://blog.csdn.net/mlwsmqq/article/details/127723729

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。

如若转载,请注明出处:http://www.7code.cn/show_8669.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注