分布式锁工具Redisson(Lua脚本)

news2024/11/19 1:38:55

如何实现分布式锁?

Redis 可以通过 setnx(set if not exists)命令实现分布式锁
通过执行结果是否为 1 可以判断是否成功获取到锁

  • setnx mylock true 加锁
  • del mylock 释放锁
    在这里插入图片描述

在这里插入图片描述

分布式锁存在的问题:

  1. 死锁问题,未设置过期时间,锁忘记释放,加锁后还没来得及释放锁就宕机了,都会导致死锁问题
  2. 锁误删问题,设置了超时时间,但是线程执行超时时间后误删问题

解决死锁问题:
MySQL 中解决死锁问题是通过设置超时时间,Redis 也是如此
官方在 Redis 2.6.12 版本之后,新增了一个功能,我们可以使用一条命令既执行加锁操作,又设置超时时间:setnx 和 expire

第一条命令成功加锁,并设置 30 s 过期时间
第二条命令跟在第一条命令后,还没有超过 30s,所以获取失败
在这里插入图片描述
解决锁误删问题:

通过添加锁标识来解决,前面我们使用 set 命令的时候,只使用到了 key,那么可以给 value 设置一个标识,表示当前锁归属于那个线程,例如 value=thread1,value=thread2…
但是这样解决依然存在问题,因为新增锁标识之后,线程在释放锁的时候,需要执行两步操作了:

  • 判断锁是否属于自己
  • 如果是,就删除锁
    这样就不能保证原子性了,那该怎么办?

解决方案:
使用 lua 脚本来解决 (Redis 本身就能保证 lua 脚本里面所有命令都是原子性操作)
使用 Redisson 框架来解决(主流)

在这里插入图片描述

如何使用Redisson锁

一、添加依赖

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

二、创建RedissonClient对象

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 如果有密码需要设置密码
        return Redisson.create(config);
    }
}

三、调用分布式锁

@RestController
public class LockController{
	
	@Resource
	private RedissonClient redissonClient;

	@RequetMapping("/lock")
	public String lockResource() throws InterruptedException{
		
		String lockKey = "myLock";

		//获取锁
		RLock lock = redissonClient.getLock(lockKey);
		try{
			boolean isLocked = lock.tryLock(20,TimeUnit.SECONDS);
			if(isLocked){
				try{
					TimeUnit.SECONDS.sleep(5);
					return "成功获取到锁,并执行业务代码";
				}catch(InterruptedException e){
					e.printStackTrace();
				}finally{
					//释放锁
					lock.unLock();
				}
			}else{
				//获取锁失败
				return "获取锁失败";
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		return "获取锁成功";
	}
}

启动项目,使用 8080 端口访问接口:
在这里插入图片描述

分布式锁

//加锁
public Boolean tryLock(String key,String value,long timeout,TimeUnit unit){
	
	return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}

//解锁,防止删除别人的锁,以uuid为value校验是否自己的锁
public void unlock(String lockName,String uuid){
	
	if(uuid.equals(redisTemplate.opsForValue().get(lockName))){
		
		redisTemplate.opsForValue().del(lockName);
	}
}

// 结构
if(tryLock){
    // todo
}finally{
    unlock;
}

get和del操作非原子性,并发一旦大了,无法保证进程安全。
建议用Lua脚本
Lua脚本是redis已经内置的一种轻量小巧语言,其执行是通过redis的eval /evalsha 命令来运行,把操作封装成一个Lua脚本,如论如何都是一次执行的原子操作。

lockDel.lua

if redis.call('get', KEYS[1]) == ARGV[1] 
    then 
 -- 执行删除操作
        return redis.call('del', KEYS[1]) 
    else 
 -- 不成功,返回0
        return 0 
end
//解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathReource("lockDDel.lua")));

//执行lua脚本解锁
redisTemplate.execute(unlockScript,Collections.singletonList(keyName),value);

加锁lock.lua

local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];

-- localname不存在
if(redis.call('exists',key) == 0) then

	redis.call('hset',key,threadId,'1');
	return 1;
end;

-- 当前线程id存在
if(redis.call('hexists',key,thread) == 1) then
	redis.call('hincrby',key,threadId,'1');
	redis.call('expire',key,releaseTime);
	return 1;
end;

return 0;

解锁unlock.lua

local key = KEYS[1];
local threadId = ARGV[1];

-- lockname、threadId不存在
if(redis.call('hexists',key,threadId) == 0) then
	return nil;
end;

-- 计数器-1
local count = redis.call('hincrby',key,threadId,-1);;

-- 删除lock
if(count == 0) then
	redis.call('del',key);
	return nil;
end;

代码进行解释.lua文件

@Getter
@Setter
public class RedisLock{
	
	private RedisTemplate redisTemplate;
	private DefaultRedisScript<Long> lockScript;
	private DefaultRedisScript<Object> unlockScript;

	public RedisLock(RedisTemplate redisTemplate){
		
		this.redisTemplate = redisTemplate;

		//加载加锁脚本
		lockScript = new DefaultRedisScript<>();
		this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
		this.lockScript.setResultType(Long.class);

		//加载释放锁的脚本
		unlockScript = new DefaultRedisScript<>();
		this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
	}

	/**
		获取锁
	*/
	public String tryLock(String lockName,long releaseTime){
		
		//存入线程信息的前缀
		String key = UUID.randomUUID().toString();
	
		//执行脚本
		Long result = (Long) redisTemplate.execute(lockScript,
											Collections.singletonList(lockName),
											key+Thread.currentThread().getId(),
											releaseTime	
											);
		if(result != null && result.intValue() == 1){
			return key;
		}else{
			return null;
		}
	}

	/**
		解锁
	*/
	public void unlock(String lockName,String key){
		redisTemplate.execute(unlockScript,Collections.singletonList(lockName),key+Thread.currentThread().getId());
	}
}

至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。
比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题
而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态 。

Redisson分布式锁

<!-- 原生,本章使用-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

<!-- 另一种Spring集成starter,本章未使用 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>

配置类

@Confituration
public class RedissionConfig{
	
	@Value("${spring.redis.host}")
	private String redisHost;

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

	private int port = 6379;

	@Bean
	public RedissonCient getRedisson(){
		Config config = new Config();
		config.useSingleServer()
			.setAddress("redis://" + redisHost + ":" + port)
			.setPassword(password);

		config.setCodec(new JsonJacksonCodec());
		return Redisson.create(config);
	}
}

启用分布式锁

@Resource
private RedissonClient redissonClient;

RLock rLock = redissonClient.getLock(lockName);
try{
	boolean isLocked = rLock.tryLock(expireTime,TimeUnit.MILLISECONDS);
	if(isLocked){
	}
}catch(Exception e){
	rLock.unlock();
}

RLock

RLock是Redisson分布式锁的最核心接口,继承了concurrent包的Lock接口和自己的RLockAsync接口

RLockAsync的返回值都是RFuture,是Redisson执行异步实现的核心逻辑,也是Netty发挥的主要阵地。

/**
	RLock如何加锁?
	从RLock进入,找到RedissonLock类,找到tryLock方法再递进到干事的tryAcquireOnceAsync方法
	
*/
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime,long leaseTime,TimeUnit unit,long threadId){
	
	if(leaseTime != -1L){
		return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
	}else{
		RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
	}
}

此处出现leaseTime时间判断的2个分支,实际上就是加锁时是否设置过期时间,未设置过期时间(-1)时则会有watchDog 的锁续约 (下文),一个注册了加锁事件的续约任务。

有过期时间tryLockInnerAsync 部分,evalWriteAsync是eval命令执行lua的入口

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

eval命令执行Lua脚本

-- 不存在该key时
if (redis.call('exists', KEYS[1]) == 0) then 
  -- 新增该锁并且hash中该线程id对应的count置1
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  -- 设置过期时间
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end; 

-- 存在该key 并且 hash中线程id的key也存在
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
  -- 线程重入次数++
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end; 
return redis.call('pttl', KEYS[1]);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1046804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

运行在浏览器中的Domino Designer开发客户机

大家好&#xff0c;才是真的好。 首先讨论一个非常有意思的事情&#xff0c;就是有人问&#xff0c;如果我用很老的Lotus软件&#xff0c;它是免费的吗&#xff1f; 这估计代表了很多盆友的心声。但不太友好的是&#xff0c;即使你用很老的Lotus软件&#xff08;例如Notes R4…

【2023集创赛】芯原杯一等奖作品:基于芯原DSP核的智能语音SoC设计

本文为2023年第七届全国大学生集成电路创新创业大赛&#xff08;“集创赛”&#xff09;芯原杯一等奖作品分享&#xff0c;参加极术社区的【有奖征集】分享你的2023集创赛作品&#xff0c;秀出作品风采&#xff0c;分享2023集创赛作品扩大影响力&#xff0c;更有丰富电子礼品等…

软件测试面试经验分享,真实面试题

前言 本人普通本科计算机专业&#xff0c;做测试也有3年的时间了&#xff0c;讲下我的经历&#xff0c;我刚毕业就进了一个小自研薪资还不错&#xff0c;有10.5k&#xff08;个人觉得我很优秀&#xff09;&#xff0c;在里面呆了两年&#xff0c;积累了一些的经验和技能&#…

红米手机 导出 通讯录 到电脑保存

不要搞什么 云服务 不要安装什么 手机助手 不要安装 什么app 用 usb 线 连接 手机 和 电脑 手机上会跳出 提示 选择 仅传输文件 会出现下面的 一个 盘 进入 MIUI目录 然后进入 此电脑\Redmi Note 5\内部存储设备\MIUI\backup\AllBackup\20230927_043337 如何没有上面的文件&a…

Linux查找运行的Python脚本路径

目录 查看Python脚本进程id 根据进程ID找脚本路径 查看Python脚本进程id ps -ef|grep python 该命令会输出在运行中的Python脚本&#xff0c;找到你需要的Python脚本进程ID即可&#xff0c;进程ID如图&#xff1a; 根据进程ID找脚本路径 # 将PID替换为上一步中你拷贝的i…

项目03-基于Docker_Nginx+LVS+Flask+MySQL的高可用Web集群

文章目录 一.项目介绍1.拓扑图2.详细介绍 二.前期准备1.项目环境2.IP划分 三. 项目步骤1.ansible部署软件环境1.1 安装ansible环境1.2 建立免密通道1.3 ansible批量部署软件 2.部署nginx和lvs主从服务器2.1 docker配置nginx静态双web服务器从nfs主服务器上那页面数据2.2 使用ke…

DiskGenius -/ 20 年老牌匠心国产数据恢复、分区管理、备份还原软件!

数据恢复 / 分区管理 / 备份还原 20 年匠心开发&#xff0c;多功能磁盘工具软件&#xff01; 数据恢复 DiskGenius 是一款专业级的 数据恢复软件 &#xff0c;算法精湛、功能强大&#xff0c;用户群体广泛&#xff1b;支持各种情况下的文件恢复、分区恢复&#xff0c;恢复效果…

JavaSE16——抽象类(Abstract Class)

抽象类&#xff08;Abstract Class&#xff09; 1 抽象类概述 抽象类是面向对象编程中的一个重要概念&#xff0c;它是一种特殊的类。抽象类不能被实例化&#xff0c;只能用作其他类的基类&#xff08;父类&#xff09;&#xff0c;通过继承抽象类来实现其子类的定义。 在继…

某瑞集团安全技术研发岗位面试

本文由掌控安全学院 - sbhglqy 投稿 一、自我介绍 阿吧阿吧&#xff0c;不多说 二、就ctf比赛经历方面提些问题 面试官&#xff1a;ctf打了多久了 我&#xff1a;两三年了。 面试官&#xff1a;得过什么奖项没有 我&#xff1a;本科的时候得过一个校一等奖。 面试官&#x…

跳槽去搞国产大模型,收入能涨多少?

原创&#xff1a;谭婧 夏尽秋来&#xff0c;2023年国产大模型看似喧闹已止&#xff0c;进入稳定竞争期。 作为一种新的IT解决方案&#xff0c;国产大模型一出生便伴随着激烈竞争。 外有GPT4&#xff0c;内有多家公司角逐“中国版ChatGPT”。 据我所知&#xff0c;就国内某家头部…

Mysql 子查询,最值查询

1.leetcode-184&#xff1a;查找部门内最高的薪水 首先是一个子查询&#xff0c;找出一个部门里最高的那个工资 随后查找最终需要的值&#xff0c;并且部门编号以及薪水应该包含在这个子查询中 最终答案&#xff1a; # Write your MySQL query statement below SELECT Depar…

全渠道客服体验:Rocket.Chat 的无缝互动 | 开源日报 No.41

RocketChat/Rocket.Chat Stars: 36.9k License: NOASSERTION Rocket.Chat 是一个完全可定制的开源通信平台&#xff0c;适用于具有高标准数据保护要求的组织。我们是团队沟通场景下的最终免费开源解决方案&#xff0c;可以实现同事之间、公司之间或客户之间的实时对话。提高生…

LiveGBS流媒体平台GB/T28181功能-海康大华摄像头接入无法语音对讲通道为0无法播放时候如何抓包分析windows抓包和Linux抓包

LiveGBS通道数为0无法播放的时候如何抓包分析windows抓包和Linux抓包 1、第一步&#xff1a;抓包工具准备1.1、Linux1.2、windows 2、第二步&#xff1a;找到设备出口ip3、第三步&#xff1a;执行命令抓设备出口ip3.1 Linux3.2 Windwos 4、第四步&#xff1a;触发相关页面操作4…

操作系统备考学习 day6(2.3.2 - 2.3.4)

操作系统备考学习 day6 第二章 进程与线程2.3 同步与互斥2.3.2 实现临界区互斥的基本方法单标记法双标志先检查法双标志后检查法Peterson算法 进程互斥的硬件实现方法中断屏蔽方法TestAndSet指令Swap指令 2.3.3 互斥锁2.3.4 信号量整型信号量记录型信号量 第二章 进程与线程 2…

安利上榜福布斯中国客户服务企业

9月25日至27日&#xff0c;福布斯中国、中国电子商会、保定市人民政府联合主办的“数智化服务产业发展论坛暨2022福布斯中国客户服务企业Top 100评选”在保定举办。活动通过剖析企业经营能力、企业服务能力、客服运营能力、企业发展潜力等多方面数据和信息&#xff0c;对数智化…

CH34X-MPHSI高速Master扩展应用—I2C设备调试

一、前言 本文介绍&#xff0c;基于USB2.0高速USB转接芯片CH347&#xff0c;配合厂商提供的USB转MPHSI&#xff08;Multi Protocol High-Speed Serial Interface&#xff09;Master总线驱动&#xff08;CH34X-MPHSI-Master&#xff09;为系统扩展I2C总线的用法&#xff0c;除…

python字符串前加r、f、u、l 的区别

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 &#x1f447; &#x1f447; &#x1f447; 更多精彩机密、教程&#xff0c;尽在下方&#xff0c;赶紧点击了解吧~ python源码、视频教程、插件安装教程、资料我都准备好了&#xff0c;直接在文末名片自取就可 f-strings 是…

Kubernetes基础(三)-Service外部网络访问方式

1 概述 NodePort、LoadBalancer 和 Ingress 都是将集群外部流量导入到集群内的方式&#xff0c;只是实现方式不同。以下是三种方式的工作原理 注意&#xff1a;这里说的每一点都基于Google Kubernetes Engine。如果用 minikube 或其它工具&#xff0c;以预置型模式&#xff08…

基于nodejs+vue学生成绩动态追踪管理系统_x3ime-

这个功能模块用来让工作人员进行后端登录。管理人员通过网站。在输入自己的用户名和密码、角色进行登录&#xff0c;登录后管理人员就可以对后台的信息相关的操作。 首先&#xff0c;设计后端管理的登陆页面。对页面的各个板块进行详细的设计&#xff0c;规划它们的字体大小&am…

数据存储——(探究内存里的秘密)

内存中的数据是代码的基因&#xff0c;相当于人体中的DNA。 你看代码就是看内存&#xff0c;因此了解内存中的数据存储十分有必要。 内容较多&#xff0c;制作不易&#xff0c;多多点赞支持一下&#xff0c;我自己会总结科班和it培训班的精华共享。 下面的图片有代码和解释 内…