Redisson分布式锁详解(非公平、公平、红锁、联锁)

目录

1、分布式锁理解及手动实现

2、Redisson介绍

3、引入依赖

4、Redis配置

5、RedissonConf配置

6、API介绍

7、效果演示

7.1、lock()方法演示

7.2、演示看门狗机制

7.3、演示无看门狗情况

8、公平锁

9、红锁

10、联锁

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


1、分布式锁理解及手动实现

Redis实现分布式锁(SETNX)_mlwsmqq的博客-CSDN博客本文详细介绍了什么是分布式锁、分布式锁的特征、应用场景;一步一步的手动实现分布式锁,分析其中需要特别注意的地方,带着大家理清其中的思路;相信对大家会有所帮助https://blog.csdn.net/mlwsmqq/article/details/127723729

2、Redisson介绍

        Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid),是一款具有诸多高性能功能的综合类开源中间件,提供的功能特性及其在项目中所起的作用远大于原生Redis所提供的各种功能,让开发者对Redis的关注进行分离,可以将更多的精力放在处理业务逻辑上。

        Redisson支持Redis多种连接方式(单机、集群、主从、哨兵等),包含分布式锁、布隆过滤器、分布式对象、分布式集合等多种工具。

3、引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.0</version>
</dependency>

4、Redis配置

redis:
  host: 192.168.xx.xx
  port: xxx
  password: xxx
  database: 1
  timeout: 7200s
  jedis:
    pool:
      max-idle: 500
      min-idle: 50
      max-wait: -1
      max-active: -1

5、RedissonConf配置

package com.example.learningexpansion.conf;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class RedissonConf {

	@Value("${spring.redis.host}")
	private String host;

	@Value("${spring.redis.port}")
	private String port;

	@Value("${spring.redis.password}")
	private String password;

	@Bean(destroyMethod = "shutdown")
	public RedissonClient redissonClient(){
		Config config = new Config();
         /**
		 *  连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()
		 *  连接集群:config.useClusterServers().addNodeAddress()
		 *  连接主从:config.useMasterSlaveServers().setMasterAddress("xxx").addSlaveAddress("xxx")
		 */
        // 连接单机
		config.useSingleServer()
				.setAddress("redis://"+host+":"+port)
				.setPassword(password);
		RedissonClient client = Redisson.create(config);
		return client;
	}

}

6、API介绍

@Api(tags = "Redis")
@RestController
@RequestMapping("/testRedis")
@Slf4j
public class TestRedisController {

    @Resource
	private RedissonClient redissonClient;
    
    @GetMapping("/testRedisson")
	@ApiOperation("Redisson")
	public ResultVO<Object> testRedisson(@RequestParam Long goodsId) {
		RLock lock = redissonClient.getLock("lock_" + goodsId);
		String threadName = Thread.currentThread().getName();
		try {
			// 注意:若设置了锁的过期时间则没有看门狗机制

			// 阻塞,拿不到锁会一直尝试获取;锁的有效期默认30秒,有看门狗机制延长锁的有效期
			lock.lock();

			// 阻塞,加锁成功后设置指定的有效时间,时间到自动释放锁(无论拿到锁线程是否执行结束),前提是没有调用解锁方法;没有看门狗
			lock.lock(10,TimeUnit.SECONDS);

			// 尝试获取锁,加锁成功后启动看门狗;非阻塞,失败立马返回;注意释放锁时要判断是否存在及是否被当前线程保持
			boolean tryLock = lock.tryLock();
			if (!tryLock){
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			// 在指定时间内尝试获取锁,失败立即返回;有看门狗
			boolean tryLock2 = lock.tryLock(5, TimeUnit.SECONDS);
			if (!tryLock2){
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			// 指定时间内尝试获取锁,失败立即返回;成功后设置有效时间为指定值,无看门狗
			boolean tryLock1 = lock.tryLock(5, 10, TimeUnit.SECONDS);
			if (!tryLock1){
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			// 注意:异步加锁需要调用get()方法使线程执行完成,否则会造成多个线程同时拿到锁
			RFuture<Void> voidRFuture = lock.lockAsync();
			voidRFuture.get();

			// 同lock.tryLock();
			RFuture<Boolean> booleanRFuture = lock.tryLockAsync();
			Boolean aBoolean = booleanRFuture.get();
			if (!aBoolean){
				return ResultUtils.error("加锁失败,请稍后成重试!");
			}

			// 注意重载方法中只有一个long时,要传的是线程ID
			RFuture<Boolean> booleanRFuture1 = lock.tryLockAsync(Thread.currentThread().getId());
			Boolean aBoolean1 = booleanRFuture1.get();
			if (!aBoolean1){
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			// 同lock.tryLock(5, TimeUnit.SECONDS)
			RFuture<Boolean> rFuture = lock.tryLockAsync(3, TimeUnit.SECONDS);
			Boolean aBoolean3 = rFuture.get();
			if (!aBoolean3){
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			// 同lock.tryLock(5, 10, TimeUnit.SECONDS)
			RFuture<Boolean> booleanRFuture4 = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
			Boolean aBoolean4 = booleanRFuture4.get();
			if (!aBoolean4){
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			// 原理同lock.tryLockAsync(3, 10, TimeUnit.SECONDS),区别在于多个是线程ID的参数
			RFuture<Boolean> booleanRFuture5 = lock.tryLockAsync(3, 10, TimeUnit.SECONDS, Thread.currentThread().getId());
			Boolean aBoolean5 = booleanRFuture5.get();
			if (!aBoolean5) {
				return ResultUtils.error("加锁失败,请稍后重试!");
			}

			log.info("{}:获取到锁", threadName);
			TimeUnit.SECONDS.sleep(5);
			log.info("{}:业务执行结束", threadName);
		} catch (Exception e) {
			log.error("testRedisson exception:", e);
			return ResultUtils.sysError();
		} finally {
			// 判断锁是否存在
			boolean locked = lock.isLocked();
			// 判断锁是否被当前线程保持
			boolean heldByCurrentThread = lock.isHeldByCurrentThread();
			log.info("{}:获取锁状态:{} 是否当前线程保留:{}", threadName, locked, heldByCurrentThread);
			if (locked && heldByCurrentThread) {
				lock.unlock();
				log.info("{}:释放锁", threadName);
			} else {
				log.info("{}:未获得锁不用释放", threadName);
			}
		}
		return ResultUtils.success();
	}

}

 需要注意的一点是只有在不指定锁的过期时间时,看门狗机制才会生效,从源码可知:

if (leaseTime != -1L) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
    this.scheduleExpirationRenewal(threadId);
}

7、效果演示

7.1、lock()方法演示

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

        8701服务效果:

2022-12-30 11:10:12.111  INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-3:获取到锁
2022-12-30 11:10:17.112  INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-3:业务执行结束
2022-12-30 11:10:17.114  INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-3:获取锁状态:true 是否当前线程保留:true
2022-12-30 11:10:17.115  INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-3:释放锁

        8702服务效果:

2022-12-30 11:10:17.122  INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-6:获取到锁
2022-12-30 11:10:22.124  INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-6:业务执行结束
2022-12-30 11:10:22.126  INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-6:获取锁状态:true 是否当前线程保留:true
2022-12-30 11:10:22.128  INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-6:释放锁

        从上述日志可看出:8701服务先拿到锁,执行完业务释放锁后8702服务才能拿到锁,达到了分布式锁想要的效果

7.2、演示看门狗机制

        同样使用lock()方法,不指定过期时间(默认30秒),睡眠40s模拟执行业务,看是否自动续期

         8701服务效果:

2022-12-30 14:41:25.898  INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:获取到锁
2022-12-30 14:42:05.899  INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:业务执行结束
2022-12-30 14:42:05.901  INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:获取锁状态:true 是否当前线程保留:true
2022-12-30 14:42:05.905  INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:释放锁

          8702服务效果:

2022-12-30 14:42:05.913  INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-1:获取到锁
2022-12-30 14:42:45.914  INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-1:业务执行结束
2022-12-30 14:42:45.917  INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-1:获取锁状态:true 是否当前线程保留:true
2022-12-30 14:42:45.921  INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-1:释放锁

        从日志可看出每个拿到锁的线程都执行了40s,并且8702服务在8701服务执行到第30s的时候仍然没有拿到锁,说明自动续期生效啦

7.3、演示无看门狗情况

        同样使用lock()方法,指定过期时间为5s,睡眠8s模拟执行业务,看是否自动续期

        8701服务效果:

2022-12-30 14:33:49.840  INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:获取到锁
2022-12-30 14:33:57.840  INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:业务执行结束
2022-12-30 14:33:57.846  INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:获取锁状态:true 是否当前线程保留:false
2022-12-30 14:33:57.846  INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController    : http-nio-8701-exec-1:未获得锁不用释放

        8702服务效果:

2022-12-30 14:33:54.844  INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-5:获取到锁
2022-12-30 14:34:02.845  INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-5:业务执行结束
2022-12-30 14:34:02.848  INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-5:获取锁状态:false 是否当前线程保留:false
2022-12-30 14:34:02.848  INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController    : http-nio-8702-exec-5:未获得锁不用释放

        从日志可看出:8701服务先拿到锁执行,5s后锁自动失效被8702服务获取到锁,此时8701的业务仍未执行结束,因此可验证结论--指定过期时间时,不会自动续期

8、公平锁

        Redisson分布式锁支持公平和非公平,上文中使用的是非公平锁

        公平锁遵循先到先得的原则

	@GetMapping("/testFairLock")
	@ApiOperation("公平锁")
	public ResultVO<Object> testFairLock(@RequestParam Long goodsId) {
		RLock fairLock = redissonClient.getFairLock("fairLock_" + goodsId);
		String threadName = Thread.currentThread().getName();
		try {
			fairLock.lock();
			log.info("{}:获得锁,开始执行业务", threadName);
			TimeUnit.SECONDS.sleep(3);
			log.info("{}:执行结束", threadName);
			return ResultUtils.success();
		} catch (Exception e) {
			log.error("testFairLock exception:", e);
			return ResultUtils.sysError();
		} finally {
			boolean locked = fairLock.isLocked();
			boolean heldByCurrentThread = fairLock.isHeldByCurrentThread();
			log.info("{}:获取锁状态:{} 是否当前线程保留:{}", threadName, locked, heldByCurrentThread);
			if (locked && heldByCurrentThread) {
				fairLock.unlock();
				log.info("{}:释放锁成功", threadName);
			} else {
				log.info("{}:未获得锁不用释放", threadName);
			}
		}
	}

9、红锁

        可以使用红锁来解决主从架构锁失效问题:就是说在主从架构系统中,线程A从master中获取到分布式锁,数据还未同步到slave中时master就挂掉了,slave成为新的master,其它线程从新的master获取锁也成功了,就会出现并发安全问题

        红锁算法:

  1.  应用程序获取系统当前时间,毫秒级
  2.  应用程序使用相同的key、value值依次从多个Redis实例中获取锁,如果某一个节点超过一定时间仍然没有获取到锁则直接放弃,尽快尝试从下一个Redis节点获取锁,以避免被宕机的节点阻塞
  3. 计算获取锁的消耗时间=客户端程序当前时间-step1中的时间,获取锁的消耗时间小于总的锁定时间(例如30s)并且半数以上节点(假如有5个节点,则至少有3个节点)获取锁成功,才认为获取锁成功
  4. 计算剩余锁定时间=总的锁定时间-step3中的消耗时间
  5. 如果获取锁失败,对所有的Redis节点释放锁(无论加锁是否成功)
	// 用于Redis集群架构下,这些节点是完全独立的,所以不使用复制或任何其他隐式协调系统
	// 该对象可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例
	@GetMapping("/testRedLock")
	@ApiOperation("红锁")
	public ResultVO<Object> testRedLock(@RequestParam Long id) {
		String threadName = Thread.currentThread().getName();
		RLock one = redissonClient.getLock("one_" + id);
		RLock two = redissonClient.getLock("two_" + id);
		RLock three = redissonClient.getLock("three_" + id);
		RedissonMultiLock redLock = new RedissonRedLock(one, two, three);
		try {
			redLock.lock();
			log.info("{}:获得锁,开始执行业务", threadName);
			TimeUnit.SECONDS.sleep(2);
			log.info("{}:执行结束", threadName);
			return ResultUtils.success();
		} catch (Exception e) {
			log.error("testRedLock exception:", e);
			return ResultUtils.sysError();
		} finally {
			// 注意:不能使用isLocked()和isHeldByCurrentThread()方法,会抛出UnsupportedOperationException异常
			redLock.unlock();
			log.info("{}:释放锁成功", threadName);
		}
	}

10、联锁

        联锁(RedissonMultiLock)对象可以将多个RLock对象关联为一个联锁,实现加锁和解锁功能。每个RLock对象实例可以来自于不同的Redisson实例。

        

	@GetMapping("/testMultLock")
	@ApiOperation("联锁")
	public ResultVO<Object> testMultLock(@RequestParam Long id) {
		String threadName = Thread.currentThread().getName();
		RLock one = redissonClient.getLock("one_" + id);
		RLock two = redissonClient.getLock("two_" + id);
		RLock three = redissonClient.getLock("three_" + id);
		RedissonMultiLock multiLock = new RedissonMultiLock(one, two, three);
		try {
			// 所有的锁都上锁成功才算成功
			multiLock.lock();
			log.info("{}:获得锁,开始执行业务", threadName);
			TimeUnit.SECONDS.sleep(3);
			log.info("{}:执行结束", threadName);
			return ResultUtils.success();
		} catch (Exception e) {
			log.error("testMultLock exception:", e);
			return ResultUtils.sysError();
		} finally {
			// 注意:不能使用isLocked()和isHeldByCurrentThread()方法,会抛出UnsupportedOperationException异常
			multiLock.unlock();
			log.info("{}:释放锁成功", threadName);
		}
	}

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

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

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

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

  • 4
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值