• Redisson


    简介

    关于 Redisson 的具体介绍可点击 这里,简单来说就是将 JUC 和 Redis 结合起来,使其可以实现多机器多线程同步的功能,Redisson 有很多组件,这篇主要介绍可重入锁 —— ReentantLock。

    环境准备

    添加 Maven 依赖

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

    添加配置类

    @Configuration
    public class MyRedissonConfig {
        @Bean(destroyMethod = "shutdown")
        RedissonClient redisson() throws IOException {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://192.168.56.10:6379");
            return Redisson.create(config);
        }
    }
    

    基本使用代码如下:

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        //获取Lock锁,设置锁的名称
        RLock lock = redisson.getLock("my-lock");
        //开启
        lock.lock();
        try {
            System.out.println("上锁:" + Thread.currentThread().getId());
            //模拟业务处理20秒
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("解锁:" + Thread.currentThread().getId());
            //释放
            lock.unlock();
        }
        return "hello";
    }
    

    分析

    当我们发送 /hello 请求后等待 20 秒得到响应结果,会在 Redis 中存储锁的信息(如下图所示),期间,其它用户发送 /hello 请求时会被阻塞,只有前一个请求结束后释放锁,当前请求才会进入。

    思考1:如果在业务处理过程中程序突然终止,锁没有得到释放,是否会一直阻塞下去?

    经过实验,在业务处理的20秒中,将服务手动停止,刷新 Redis 中 my-lock 的信息,发现 TTL 不断的减小,直到失效,再发送其它请求能够正常执行,这说明,即使不释放锁,锁的有效时间到了也会自动释放。源码如下:

    //获取当前线程id
    long threadId = Thread.currentThread().getId();
    //获取此线程的锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    //如果获取不到,则说明锁已经释放了,直接返回
    if (ttl == null) {
        return;
    }
    while (true) {
        ttl = tryAcquire(leaseTime, unit, threadId);
        //和上面一样,判断是否能获取到锁
        if (ttl == null) {
            break;
        }
        ...
    }
    

    思考2:过期时间是多少?如果我们的业务处理时间超过了过期时间,岂不是还没处理完就把锁的信息给删了?

    正常启动服务访问 /hello,刷新 my-lock 的信息,我们发现,TTL 每次减少到 20 就再次变为 30,直到业务处理完成,my-lock 被删除。查找相关源代码如下:

    while (true) {
        //尝试获取锁
        ttl = tryAcquire(leaseTime, unit, threadId);
        //如果获取不到,说明执行该线程执行结束,就终止循环
        if (ttl == null) {
            break;
        }
    
        //如果获取到了就继续循环
        if (ttl >= 0) {
            try {
                future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                if (interruptibly) {
                    throw e;
                }
                future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            }
        } else {
            if (interruptibly) {
                future.getNow().getLatch().acquire();
            } else {
                future.getNow().getLatch().acquireUninterruptibly();
            }
        }
    }
    

    继续深入源码可以看到,如果不指定锁的时间,就默认为 30 秒,它有一个好听的名字:看门狗

    private long lockWatchdogTimeout = 30 * 1000;
    

    只要占领锁,就会启动一个定时任务:每隔一段时间重新给锁设置过期时间

    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), 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.<Object>singletonList(getName()), 
            internalLockLeaseTime, getLockName(threadId));
        //internalLockLeaseTime就是看门狗的时间
    }
    

    每隔多长久刷新一下呢?

    //获取看门狗的时间,赋值给自己
    this.internalLockLeaseTime = xxx.getLockWatchdogTimeout();
    public long getLockWatchdogTimeout() {
        return lockWatchdogTimeout;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ...
        }
        //使用的时候除3,也就是10秒刷新一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    

    思考三:如何自定义过期时间?

    lock() 方法还有一个重载方法,可以传入过期时间和单位

    void lock(long leaseTime, TimeUnit unit);
    

    我们将之前的代码修改,设置为 15 秒,重启服务再测试

    lock.lock(15, TimeUnit.SECONDS);
    

    访问 /hello,刷新 Redis 中 my-lock 的信息会发现,TTL 从 15 减到 0,然后锁信息过期,并不会出现之前的 10秒一刷新,查看源码:

    private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        //如果传入了过期时间,则直接执行tryLockInnerAsync里面的Lua脚本
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        //没有传入过期时间,执行下面的逻辑
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            //有异常,直接返回
            if (e != null) {
                return;
            }
            if (ttlRemaining == null) {
                //刷新过期时间
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }
    

    总结

    1、Reentrant Lock 对其它线程是阻塞的

    2、为了解决死锁的问题,Redisson 内部提供了一个监控锁的看门狗,只要 Redisson 实例没被关闭就不断延长锁的有效时间,默认情况下,看门狗的检查锁的超时时间是 30 秒,检查时间是 10 秒(超时时间的三分之一),可以通过 setLockWatchdogTimeout 设置(只适用于未指定锁的时间的情况)

    3、如果指定锁的时间,到达指定时间会自动解锁,因此设置的时间必须大于业务正常执行的时间,否则,业务没执行完,锁就会被释放

    4、推荐使用指定时间的方式,省掉了续期操作,但需要合理设置过期时间,不能使锁过早释放

  • 相关阅读:
    《Linux内核设计与实现》读书笔记 第十八章 调试
    《Linux内核设计与实现》读书笔记 第五章 系统调用
    [题解] LuoguP5488 差分与前缀和
    [题解] LuoguP4655 [CEOI2017]Building Bridges
    [题解] Tenka1 Programmer Contest 2019 E
    [题解] LuoguP4284 [SHOI2014]概率充电器
    长链剖分学习笔记
    [题解] LuoguP4292 [WC2010]重建计划
    [题解] LuoguP6197 [EER1]礼物
    [题解] LuoguP3980 [NOI2008]志愿者招募
  • 原文地址:https://www.cnblogs.com/songjilong/p/12891787.html
Copyright © 2020-2023  润新知