前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >redission 锁机制

redission 锁机制

原创
作者头像
六个核弹
发布2023-04-26 19:44:38
4570
发布2023-04-26 19:44:38
举报

前段时间,有小伙伴问我,redission锁的原理,看门狗的作用,和一些实际开发中的场景,当时并没有给他比较完整的解答,后来我查了资料对redission做了一个总结,在这里分享给小伙伴们

redission 锁的实现原理

以下是redission锁的使用示例:

代码语言:javascript
复制
        RLock lock = redisson.getLock ("key");
        lock.lock ();
        try {
          //  do something
        } catch (Exception e) {
           // do something
        }finally {
            lock.unlock ();
        }

我们看一下RLock实现类RedissonLock的lock方法代码:

代码语言:javascript
复制
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        ......
}

追溯tryAcquire方法,最终能找到tryLockInnerAsync这个方法:

代码语言:javascript
复制
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

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

不难看出,这个方法是通过lua 脚本进行上锁操作,它首先通过 exists判断当前的key是否存在,如果不存在,则使用 hincrby命令创建一个新的哈希表,其中哈希表的key 也就是 ARVG[2] 的值的源码:

代码语言:javascript
复制
protected String getLockName(long threadId) {
        return id + ":" + threadId;
    }

它是当前redission 的id +":"+当前线程的id,再通过hincrbypexpire初始化当前线程持有的锁。如果当前key存在,则通过hexists判断哈希表中是否有当前线程的锁,如果有则通过hincrby指令给哈希表中的值加一,然后通过pexpire重置锁过期时间。最后一个return是如果不是当前线程持有锁,则返回当前锁剩余有效时间。不难看出RedissonLock是支持重入的,只要当前线程持有了该锁,下次获取锁的时候通过hincrby进行加一操作。那它是怎么释放锁的呢?我们直接看org.redisson.RedissonLock#unlockInnerAsync方法:

代码语言:javascript
复制
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "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(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

这个段lua脚本也是先判断当前线程的锁是否存在,不存在直接返回,存在则对哈希表中线程的值减一;如果值大于1 证明该锁被重入,不应该释放,重置过期时间返回,否则删除该锁,然后 调用publish指令通知其他订阅了该key的线程重新抢占。参考我以往博客的lua脚本:

代码语言:javascript
复制
StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"setnx\",KEYS[1],ARGV[1])==1 ");
        sb.append("then ");
        sb.append(" redis.call(\"pexpire\",KEYS[1],KEYS[2]) ");
        sb.append(" return 1 ");
        sb.append("else ");
        sb.append(" return 0 ");
        sb.append("end ");
        String script = sb.toString();
        RedisCallback<Boolean> callback = (connection) -> connection.eval(script.getBytes(),
            ReturnType.BOOLEAN, 1, key.getBytes(Charset.forName("UTF-8")),
            value.getBytes(Charset.forName("UTF-8")));
        Boolean execute = redisTemplate.execute(callback);

我这个分布式锁是使用redisTemplate 实现的,不支持重入,对比redission就简陋很多。

看门狗的原理

在明白看门狗的原理的之前,我们要先了解为什么要看门狗?它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。如果没有看门狗,就会导致业务代码没跑完,锁已经释放的情况,可能你会说那不给锁过期时间不就行了,那如果某个线程释放锁失败,会把整个业务场景锁死,造成生产事故;而有看门狗的情况解锁失败也只会死锁续期的那一段时间,造成的影响远比不设过期时间的情况要小。在之前提到的tryAcquireAsync()方法中如果没有传入过期时间,就会调用org.redisson.RedissonLock#scheduleExpirationRenewal方法创建看门狗:

代码语言:javascript
复制
private 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);
            renewExpiration();
        }
    }

再看 org.redisson.RedissonLock#renewExpiration

代码语言:javascript
复制
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);
}

不难看出,看门狗本质就是一个schedule线程,它的执行时间间隔是internalLockLeaseTime / 3 也就是锁设置的过期时间的三分之一。

一些疑问

  1. 如果忘记释放锁,看门狗会给我的锁无限续期吗?

门狗线程的执行逻辑是获取持有当前锁的线程id,然后续期。如果线程id没有被从当前锁的map中剔除,就会一直续期。而剔除线程id的方法是org.redisson.RedissonLock#cancelExpirationRenewal,很不幸,这个方法只有在解锁的时候被调用。

  1. redission 还有哪些锁,分别是为了解决什么问题

这是一个比较大的问题,在这里只对redission 锁及作用进行简单介绍:

  • RedissonRedLock:红锁,用于redis多节点部署架构,它是同时对多个redis节点进行上锁,过半节点上锁成功才算加锁成功,主要是为了防止单节点挂掉导致锁失效
  • RedissonMultiLock:联锁,将多个RLock对象关联为一个联锁,提供一个锁合并的功能。
  • RedissonSemaphore:信号量,同 JUC中的信号量,底层指令是decrbyincrby

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • redission 锁的实现原理
  • 看门狗的原理
  • 一些疑问
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档