Redission实现分布式锁

 

目录

1. redis实现分布式锁的问题

2. Redission

2.1 什么是redission

 2.2 redission实现分布式锁

 2.2.1 引入依赖

2.2.2 客户端实现

2.3 redission可重入锁原理        

2.3.1 流程分析

 2.3.2 源码实现

2.4 锁重试和WatchDog机制

2.4.1 跟踪源码探寻锁重试

2.4.2 WatchDog机制处理锁续约


1. redis实现分布式锁的问题

重入问题:重入问题就是获得锁的线程可以再次进入到相同的锁的代码块中,重入锁的意义就是在于防止死锁。

不可重试:指的是目前分布式锁只能尝试一次,合理的情况应该是:当线程获得锁失败后,他应该能再次尝试获得锁。

超时释放:在加锁的时候,设置了过期时间防止死锁,但如果出现线程阻塞的情况就会出现锁不住的情况,这就是之前出现的误删锁的问题。

主从一致性:如果redis提供了主从集群,当向集群中写数据时,主机需要异步的将数据同步给从机,而如果在同步的时候,主机宕机,就会出现死锁问题。

2. Redission

2.1 什么是redission

redission时一个在redis基础上实现JAVA驻内存数据网络,不仅提供了一系列的分布式java对象以及各种分布式锁的实现。

 2.2 redission实现分布式锁

 2.2.1 引入依赖

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

2.2.2 客户端实现

配置类

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis:/ip address:6379")
            .setPassword("password");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

获取和释放锁

@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();
        }
        
    } 
}

2.3 redission可重入锁原理        

        在Lock中,借助了底层的一个voaltile的一个state变量俩记录重入状态,当没有人获取这把锁,此时state=0,有线程得到锁后state=1,如果这个线程再次申请这把锁的时候,此时state+1,释放锁时state-1。对于synchronized而言,在c语言代码中有一个count,原理和state是一样的,重入+1,释放-1,知道减少到零,表示此时锁没有线程持有。

        在redission中也是支持锁重入的,他采用了hash结构来存储锁,其中大key标识这把锁是否存在,小key表示这把锁当前被谁持有。

hash结构:

  • key:锁的名称
  • value:
    • field:当前持有者的id(id+":"+线程id,小key)
    • value:state(count)状态变量

2.3.1 流程分析

 2.3.2 源码实现

        LUA脚本实现锁重入逻辑。KEYS[1]:锁名称,ARGV[1]:锁失效时间,ARGV[2]:线程标识

            "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]);"

2.4 锁重试和WatchDog机制

2.4.1 跟踪源码探寻锁重试

        redission获取锁的方法trylock(),这个方法代码量较多,并且里面也调用了多个函数,在这里进行分段分析

trylock()方法

        三个参数waitTime(就是重试等待时间),leaseTime(过期时间),TimeUnit(时间单位),这里前两个事比较重要的参数,最后一个参数相信大家都明白是干什么的。

       1、对参数进行解析,全部转换成毫秒单位,方便后面进行判断,调用tryAcurire方法获取剩余有效期,返回null的话就是已经获得了锁,返回具体值说明获取锁失败,进入重试机制

    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);

        2、这里就是上述说的判断,判断完成后获取剩余时间,判断是否还有剩余重试等待时间,如果没有直接返回false,获取锁失败(这里还调用了一个获取失败的函数,应该是发送通知,后面会提到)

        // lock acquired
        if (ttl == null) {
            return true;
        }
        
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

         3、如果还有重试等待时间,进行消息订阅,订阅释放锁的信号,并调用get方法进行消息接收,get方法中传入监听时间(重试等待时间),在监听时间内未接收到释放锁信号,此时会报TimeoutException异常,此时重试等待时间耗尽,返回false,获取锁失败,退出重试机制。

        这一段代码是放置过于频繁的重试获取锁,因为你刚刚获取锁失败,立即重试,失败的概率还是很大,因此设置一个消息订阅,当其他线程将锁释放后,再进行重试。

        current = System.currentTimeMillis();
        CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        try {
            subscribeFuture.get(time, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
                    "Unable to acquire subscription lock after " + time + "ms. " +
                            "Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
                subscribeFuture.whenComplete((res, ex) -> {
                    if (ex == null) {
                        unsubscribe(res, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            return false;
        } catch (ExecutionException e) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        4、如果在监听过程中收到了释放锁的信号,此时继续判断是否还有重试等待时间,没有就返回false,获取锁失败,有就继续

        try {
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

        5、这是最后一段,也是锁重试的核心一段。此时如果还有重试等待时间,就进入循环,进行重试获取锁,返回ttl如果为null获取锁成功,如果不为null,此时要判断是否还有重试等待时间,没有就返回false,重试获取锁失败。如果还有重试等待时间,判断这时的重试等待时间和过期时间那个大,选择大的那个进行消息等待,如果接收到锁释放消息,计算此时剩余的重试等待时间,判断是否还有时间剩余,如果没有,获取锁失败,如果有返回while(true)循环,重复执行上述逻辑。

  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;
                }

                // waiting for message
                currentTime = System.currentTimeMillis();
                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);
        }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
    }

2.4.2 WatchDog机制处理锁续约

        在进行获取锁时,往往会给过期时间,如果传入过期时间此时就会触发WatchDog机制(看门狗机制)。分析tryAcquire方法源码

tryAcquire方法

        如果没有传入过期时间(leaseTime),就会进入到else部分默认internalLockLeaseTime,一般是30s,继续往下走。

        到下一个if,判断ttlRemaining==null,这个就是判断是否获取锁成功,成功返回true,不成功返回false,成功进一步判断是否出入过期时间(leaseTime),传入了就不处理,没有传入就会触发刷新有效期机制,进入方法scheduleExpirationRenewal。

    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        if (leaseTime > 0) {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
        ttlRemainingFuture = new CompletableFutureWrapper<>(s);

        CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
            // lock acquired
            if (ttlRemaining == null) {
                if (leaseTime > 0) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    scheduleExpirationRenewal(threadId);
                }
            }
            return ttlRemaining;
        });
        return new CompletableFutureWrapper<>(f);
    }

scheduleExpirationRenewal方法

        进入这个方法后,会new一个entry,将这个entry添加到一个静态map中,key的值就是锁定名字,如果存在就返回上一次的值,不存在返回null。就是说第一次进入oldEntry==null,之后进入oldEntry就是静态map返回的值。如果是第一次,进入else,给entry添加线程id,并进入renewExpiration方法,实现刷新有效期。

 protected void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            try {
                renewExpiration();
            } finally {
                if (Thread.currentThread().isInterrupted()) {
                    cancelExpirationRenewal(threadId);
                }
            }
        }
    }

renewExpiration方法

        这个方法是续约的核心方法,进入方法后,获取map中的entry,如果为空直接返回,不为空,开启一个延迟任务。newTimeout有两个参数,一个是任务本身,就是任务逻辑,第二个参数是延迟时间。

        任务逻辑是调用renewExpirationAsync方法,这个方法就是调用lua脚本实现过期时间刷新。之后递归调用renewExpiration方法,实现不断刷新过期时间。

        延迟时间一般是过期时间的三分之一,也就是说,每过三分之一的过期时间就刷新一次过期时间。

    private void renewExpiration() {
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }
        
        Timeout task = getServiceManager().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;
                }
                
                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock {} expiration", getRawName(), e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    } else {
                        cancelExpirationRenewal(null);
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }

renewExpirationAsync方法

调用lua脚本刷新过期时间。

    protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return 0;",
                Collections.singletonList(getRawName()),
                internalLockLeaseTime, getLockName(threadId));
    }

 

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值