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

如何实现分布式锁?

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锁

一、添加依赖

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.2</version>
</dependency>

二、创建RedissonClient对象

java 复制代码
@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);
    }
}

三、调用分布式锁

java 复制代码
@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 端口访问接口:

分布式锁

java 复制代码
//加锁
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

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

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

加锁lock.lua

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

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文件

java 复制代码
@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分布式锁

xml 复制代码
<!-- 原生,本章使用-->
<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>

配置类

java 复制代码
@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);
	}
}

启用分布式锁

java 复制代码
@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发挥的主要阵地。

java 复制代码
/**
	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的入口

java 复制代码
<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脚本

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]);
相关推荐
努力更新中7 分钟前
Python浪漫之画一个音符♪
开发语言·python
Mr_Xuhhh15 分钟前
程序地址空间
android·java·开发语言·数据库
凤枭香24 分钟前
Python Selenium介绍(二)
开发语言·爬虫·python·selenium
疯狂吧小飞牛26 分钟前
C语言解析命令行参数
c语言·开发语言
z2023050829 分钟前
linux之调度管理(13)- wake affine 唤醒特性
java·开发语言
AI人H哥会Java30 分钟前
【JAVA】Java高级:Java网络编程——TCP/IP与UDP协议基础
java·开发语言
小白要加油哈43 分钟前
Lua--1.基础知识
开发语言·junit·lua
孙克旭_44 分钟前
第五章 RabbitMQ高级
分布式·rabbitmq
网络安全Ash1 小时前
企业网络安全之OPENVPN
开发语言·网络·php
xcLeigh1 小时前
C# Winform贪吃蛇小游戏源码
开发语言·c#