Redisson分布式锁

目录

Redisson的基本使用

Redisson的基本原理

Redis中的使用

简单了解一下Lua脚本

加锁脚本

解锁脚本

看门口续期lua脚本

源码

tryLock方法

tryAcquireAsync方法

unlock方法

renewExpiration()方法


在一个进程的各个线程间保持数据的同步可以使用Lock、synchronized、CAS、ReentrantLock等,在进程间保持数据的同步就需要使用分布式锁。Redisson就是一种分布式锁。

Redisson是一个基于Redis的Java驻内存数据网格(In-Memory Data Grid),提供了丰富的分布式Java对象和服务,其中包括分布式锁。Redisson的分布式锁实现了java.util.concurrent.locks.Lock接口,可以方便地在分布式环境中实现分布式锁的功能。

Redisson的基本使用

导入依赖

        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.12.0</version>
		</dependency>

编写配置

java 复制代码
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://47.115.217.159:6379");//redis所在的服务器以及端口
        // 创建RedissonClient对象
        return Redisson.create(config);
    }

}

使用

java 复制代码
@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
        
    }
    
}

Redisson的基本原理

Redis中的使用

Redisson借助redis的setnx和expire命令实现。

Redisson 使用了 Redis 的 SET key value [PX milliseconds]|[EX seconds] NX 命令来实现分布式锁,该命令的含义是:当且仅当 key 不存在时,将 key 的值设为 value ,并同时设置 key 的过期时间为 milliseconds 毫秒或者seconds秒。如果成功设置了值和过期时间,返回 OK;如果 key 已经存在,返回 null。

上面这段操作我先get myLock,发现redis中不存在这个key,然后设置myLock的value为hello1,过期时间为100秒,可以看到第一次set的时候返回的是OK,第二次设置的时候返回的是nil(null的意思),然后及时的get,得到设置的值,在100秒之后,再去get,发现已经过期了,返回的是nil。

具体来说,Redisson 实现分布式锁的过程如下:

  1. 使用 SET key value [PX milliseconds]|[EX seconds] NX 命令尝试获取锁,其中 key 是锁的唯一标识,value 可以是任意值(通常用来区分不同的客户端),PX milliseconds 表示设置 key 的过期时间为 milliseconds 毫秒,EX seconds 表示设置key的过期时间为seconds秒, NX 表示仅当 key 不存在时才设置成功。

  2. 如果获取锁成功(SET 命令返回 OK),则表示当前客户端获取到了锁,可以执行临界区代码;如果获取失败(SET 命令返回 nil),则表示锁已经被其他客户端持有,当前客户端需要等待或放弃获取锁。

  3. 在执行临界区代码期间,Redisson 会周期性地使用 EXPIRE key seconds命令来更新锁的过期时间,确保即使临界区代码执行时间较长,锁也不会过期释放。(锁的续期)

  4. 当临界区代码执行完成后,客户端使用 DEL key 命令来释放锁,即使当前客户端不持有锁(例如由于锁的过期时间已到),也可以调用该命令来释放锁。

简单了解一下Lua脚本

在Java中,Redisson使用lua脚本来保证加锁、解锁的原子操作。

想要真正读懂redisson底层的加锁解锁实现,基本的lua脚本还是要了解一下的,这里就简单的介绍一下,本人也了解的不多。

加锁脚本

  • KEYS[1] 锁的名字

  • ARGV[1] 锁自动失效时间(毫秒,默认30s(看门狗续期时长))

  • ARGV[2] value中hash子项的key(uuid+threadId)

java 复制代码
--如果锁不存在
if (redis.call('exists', KEYS[1]) == 0) then
--重入次数初始为0后加一
redis.call('hincrby', KEYS[1], ARGV[2], 1);
--设锁的过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
--返回null-代表加锁成功
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]);
--返回null-代表重入成功
return nil;
--结束符
end;
--返回锁的剩余时间(毫秒)-代表加锁失败
return redis.call('pttl', KEYS[1]);

结论:当且仅当返回nil,才表示加锁成功;

解锁脚本

  • KEYS[1] 锁的名字

  • KEYS[2] 发布订阅的信道(channel=redisson_lock__channel:{lock_name})

  • ARGV[1] 发布订阅中解锁消息

  • ARGV[2] 看门狗续期时间

  • ARGV[3] hash子项的(key=uuid+threadId)、

java 复制代码
--如果锁不存在
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
--返回null-代表解锁成功
return nil;
end;
--重入次数减一
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
--如果重入次数不为0,对锁进行续期(使用看门狗的续期时间,默认续期30s)
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
--返回0-代表锁的重入次数减一,解锁成功
return 0;
--否则重入次数<=0
else
--删除key
redis.call('del', KEYS[1]);
--向channel中发布删除key的消息
redis.call('publish', KEYS[2], ARGV[1]);
--返回1-代表锁被删除,解锁成功
return 1;
end;
return nil;

结论:当且仅当返回1,才表示当前请求真正解锁;

看门口续期lua脚本

如果我们在getLock的时候没有自己设置leaseTime,那么默认的过期时间就是30ms,每隔10毫秒持有Redisson分布式锁的进程会创建一个线程去判断同步代码块是否执行完成,如果没有,就将过期时间设置为30毫秒。如果在创建Redisson分布式锁的时候自己设置了leaseTime,就不会出发看门狗机制。

  • KEYS[1] 锁的名字

  • ARGV[1] 锁自动失效时间

  • ARGV[2] value中hash子项的key(uuid+threadId)

java 复制代码
--自己加的锁存在
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
--续期
redis.call('pexpire', KEYS[1], ARGV[1]);
--1代表续期成功
return 1;
end;
--自己加的锁不存在,后续不需要再续期
return 0;

源码

在IDEA中shift+shift搜索RedissonLock,找到如下方法。

tryLock方法

java 复制代码
// tryLock 是Redisson加锁的核心代码,在这里,我们基本可以了解加锁的整个逻辑流程
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // 获取锁能容忍的最大等待时长
    long time = unit.toMillis(waitTime);
    // 获取当前系统时间
    long current = System.currentTimeMillis();
    // 获取当前线程id
    long threadId = Thread.currentThread().getId();
    
    // 【核心点1】尝试获取锁,若返回值为null,则表示已获取到锁
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    if (ttl == null) {
        return true;
    }
    
   // 剩余等待时长 =   最大等待时长-(当前时间)
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        //等待时间超时
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
    
    current = System.currentTimeMillis();
    
    // 【核心点2】获取锁失败之后订阅解锁消息,这是一个异步任务
    CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    try {
        // 以阻塞的方式获取订阅结果,最大等待时间time,在time时间之内代码一直停在这里
        subscribeFuture.toCompletableFuture().get(time, TimeUnit.MILLISECONDS);
    } catch (ExecutionException | TimeoutException e) {
        // 判断异步任务是否不存在,比如上面的阻塞等待没有获取到订阅结果
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.whenComplete((res, ex) -> {
                // 异步任务出现异常,取消订阅
                if (ex == null) {
                    unsubscribe(res, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
 
    try {
        // 剩余等待时长
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
    
        // 循环获取锁      
        while (true) {
            long currentTime = System.currentTimeMillis();
            // 再次获取锁,成功则返回
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }
 
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
 
            currentTime = System.currentTimeMillis();
            
            // 【核心点3】阻塞等待信号量唤醒或者超时,接收到订阅时唤醒
         // 使用的是Semaphore#tryAcquire()
           // 判断 锁的占有时间(ttl)是否小于等待时间  
            if (ttl >= 0 && ttl < time) {
                commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }
 
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        // 因为是同步操作,所以无论加锁成功或失败,都取消订阅
        unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
    }
}

tryAcquireAsync方法

java 复制代码
/**
 * 异步的方式尝试获取锁
 */
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
     
        // 占有时间等于 -1 表示会一直持有锁,直到业务进行完成,主动解锁(这里就显示出了finally的重要性)
        if (leaseTime != -1) {
            // 【核心点4】这里就是直接使用lua脚本
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
            // lock acquired
            if (ttlRemaining == null) {
                if (leaseTime != -1) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    scheduleExpirationRenewal(threadId);
                }
            }
            return ttlRemaining;
        });
        return new CompletableFutureWrapper<>(f);
    }

核心点4对应的lua脚本

java 复制代码
/**
 * redisson最底层就是lua脚本的直接调用
 * 这里是使用lua脚本进行加锁
 */
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }

unlock方法

java 复制代码
/**
* 解锁逻辑
*/
@Override
public void unlock() {
    try {
        // 以线程阻塞的方式获取结果
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        } else {
            throw e;
        }
    }
}

异步解锁

java 复制代码
// 异步解锁
@Override
public RFuture<Void> unlockAsync(long threadId) {
    // 【核心点5】 调用异步解锁方法--使用lua脚本     
    RFuture<Boolean> future = unlockInnerAsync(threadId);
 
    CompletionStage<Void> f = future.handle((opStatus, e) -> {
        cancelExpirationRenewal(threadId);
 
        if (e != null) {
            throw new CompletionException(e);
        }
        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            throw new CompletionException(cause);
        }
 
        return null;
    });
 
    return new CompletableFutureWrapper<>(f);
}

核心点5所对应的解锁的lua脚本

java 复制代码
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
//Redisson解锁lua脚本
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
return nil;
end; 
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) then 
redis.call('pexpire', KEYS[1], ARGV[2]); 
return 0; 
else redis.call('del', KEYS[1]); 
redis.call('publish', KEYS[2], ARGV[1]); 
return 1; 
end; 
return nil;", 

Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
    }

renewExpiration()方法

java 复制代码
private void renewExpiration() {
        ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    ExpirationEntry ent = (ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            //【核心点6】锁续约的核心代码
                            RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                    if (res) {
                                        RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

核心店6对应的锁续期的lua脚本代码

java 复制代码
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
//lua脚本
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return 1; 
end; 
return 0;", 

Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }
相关推荐
安的列斯凯奇26 分钟前
Redis篇 Redis如何清理过期的key以及对应的解决方法
数据库·redis·缓存
小小虫码43 分钟前
MySQL和Redis的区别
数据库·redis·mysql
飞火流星020276 小时前
docker安装Redis:docker离线安装Redis、docker在线安装Redis、Redis镜像下载、Redis配置、Redis命令
redis·docker·docker安装redis·redis镜像下载·redis基本操作·redis配置
lwprain6 小时前
springboot 2.7.6 security mysql redis jwt配置例子
spring boot·redis·mysql
如风暖阳8 小时前
Redis背景介绍
数据库·redis·缓存
lingllllove9 小时前
Redis脑裂问题详解及解决方案
数据库·redis·缓存
一张假钞9 小时前
Spark的基本概念
大数据·分布式·spark
微光守望者9 小时前
Redis常见命令
数据库·redis·缓存
一张假钞9 小时前
Spark On Yarn External Shuffle Service
大数据·分布式·spark
凌波漫步&9 小时前
通过Redisson构建延时队列并实现注解式消费
redis·电商平台·redisson订单自动取消