Redisson实现分布式锁(key续期原理)

730 阅读6分钟

概述

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和****最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

在SpringBoot中引入依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.12.0</version>
</dependency>
package luochuang.news.news.conf;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @Description
* @Author 张国伟
* @Data 2022/8/27 21:00
*/
@Configuration
public class RedissonConfig {
    
    @Bean(destroyMethod = "shutdown")
    RedissonClient redissonClient ()
    {
        Config config = new Config();
        //单节点模式
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

redisson实现分布式锁(以及自动续期源码分析)

JUC可重入锁

又名递归锁。是指在同一线程在外层访问方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提:锁对象是用一个对象),不会因为之前已经获取过还没释放而阻塞.

synchronized和ReentrantLock都是可重入锁,优点是可一定程度避免死锁。

一个线程中的多个流程可以获取同一把锁,持有这把同步锁的可以再次进入。自己可以获取自己的内部锁。

为什么在这里引入可重入锁?可以看到ReentrantLock实现了Lock,当然Redisson也实现了Lock所以如果对Lock有了解的同仁操作起来Redisson还是相当方便

首先我们来看一段代码

  //获取锁
        RLock lock =redisson.getLock("gw-lock");
        //获取到锁后加锁
        lock.lock();//阻塞式等待,自动尝试获取锁
        try {
            System.err.println("执行业务。。。。。。。。。。。。。。。"+Thread.currentThread().getId());
            Thread.sleep(30000);
        } catch (Exception e) {

        } finally {
            System.out.println("解锁........."+Thread.currentThread().getId());
            lock.unlock();
        }

其实这里看起来代码相对redis原生setNX简洁起来很多,那我们就来剖析下其中的奥妙。

1、首先第一步先使用lock.lock()获取到锁

2、执行业务

3、在finally里释放锁。其实就是这么回事

在这里着重说一下lock.lock()机制,我在下面写的Thread.sleep(30000);模拟的是业务执行时间,大家都知道30S时间对服务端来说已经是一个特别特别久的时间,那么这个时候还没释放锁,其他线程进来的话是拿不到锁的,那redisson怎么保证锁的过期时间呢?怎么保证锁在业务时间执行较长的过程中保证不过期?我们来看下源码

我们可以看到这里有一个tryAcquire方法有三个参数,leaseTime:锁的过期时间,threadId:当前线程ID其实lock.lock()在加锁的时候是有参数

lock.lock(10,TimeUnit.SECONDS);
//过期时间为10S(并且不会自动续期),如果不指定默认过期时间为30S

有了这行代码我们继续来看.

 if (leaseTime != -1) {
    return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
 }

如果我们不传递参数会执行一个tryLockInnerAsync的方法。

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

可以看到如果指定的话,就会执行commandExecutor.evalWriteAsync可以看到是一个LUA脚本,lua脚本是原子性的,所以一下会执行完,不拖泥带水,并且没有体现key的续期过程.

 RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        });

如果不设置过期时间可以看到同样执行了tryLockInnerAsync方法,但是这时候注意有一个commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()参数

这个是commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()的值,如果不设置时间默认就是30*1000,可以看到返回了一个RFuture<Long>对象,这个对象是JUC里的Future可以获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等

我们看到这里有scheduleExpirationRenewal这样一个方法参数为线程ID

这里来看下renewExpiration()

 private void renewExpiration() {
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }
        
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
                
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }

可以看到这里有一个定时任务,这个定时任务就是定时续期,那么在多久续期?

在类构造环节会对internalLockLeaseTime进行赋值,默认为30*1000也就是30秒,然后/3也就是平均在10秒左右续期. 我们来看下具体操作

这里通过TTL看到默认是30S

当我刷新到快10S左右的时候

重新续期到30S。这里我们做个总结

1、锁的自动续期,如果业务时间超长,运行期间自动给续上新的30S。不必担心业务时间长,锁会自动过期

2、加锁的业务只要完成运行,就不会给当前锁进行续期,及时不手动解锁,锁默认在30S以后自动删除

3、如果我们传递了锁的超时时间,就会发送lua脚本,进行占锁,不会自动续期。

4、如果我们没有指定锁的过期时间,就使用30*1000(internalLockLeaseTime),只要占锁成功,就会启动一个定时任务,重新给锁设置过期时间。

5、lock.lock(10,TimeUnit.SECONDS);省去了续期操作。

lock.tryLock(100,10,TimeUnit.SECONDS);
//这里还有个tryLock方法,多了一个参数,这里的100指的是最多等待占锁时间
//他返回的是一个boolean如果是true就说明获取到锁
//实战推荐tryLock的方式,当然结合AOP的方式更佳

读写锁

public String wirteValue()
    {
        RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");
        //加锁写锁
        writeLock.writeLock().lock();
        //执行业务
        try {
            Thread.sleep(2000);
            redisTemplate.opsForValue().set("123","abc");
            System.out.println("业务执行...");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放锁
            writeLock.writeLock().unlock();
        }
        return "";
    }
 RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");
        //拿到读锁
        writeLock.readLock().lock();
        try {
            Thread.sleep(30000);
            System.out.println("执行业务");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放锁
            writeLock.readLock().unlock();
        }

简单总结

修改数据期间也就是写锁,是一个排他锁(互斥锁).读锁是一个共享锁

写锁没释放就必须等待

读、读: 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁都会加锁成功。

写、读:等待写锁的释放

写、写:阻塞式等待,等待前一个写完

读、写:有读锁,写也需要等待。

只要有写的存在,都必须等待