Redis分布式锁的原理与面试细节

在这里插入图片描述

这里就讲了下怎么加锁的,很多原理的问题小伙伴们,可用百度下分布式锁,看图中我特别在加锁与删除锁的时候还有俩个指向就特别说下这俩个问题

我们加锁的时候为了防止死锁的问题都在加锁的时候会带上 锁过期时间的问题我们使用Redis提供的设置值的时候跟设置过期时间是原子性的操命令

SETNX EX

加锁时候的原子性问题我们解决了,我们知道分布式锁就是只有一个线程才能抢到锁位,那其他线程怎么处理呢?有些文章可能都只说了一些流程却忘记了很多坑
加锁失败的几个解决办法【这也叫锁冲突的问题】

  1. 直接抛弃,前端提醒用户该次请求失败【这种方式可能并不友好】
  2. 前端重试,或者后端重试
  3. 将请求添加到延时队列中,稍后重试

加锁过程我们处理好了,那么删除锁的时候呢?
删除锁的时候我们既要防止删除是别人锁有要当业务流程执行时间大于加锁的时间问题

删除锁的原子性就我们依靠了Lua脚本来实现删除锁的原子性

Redis锁超时问题呢?
使用过或者了解过Redisson的小伙伴知道Redisson框架实现分布式锁有一个看门狗机制,当业务流程大于加锁时间的时候,看门狗机制为在加锁的时间上在添加10秒

我们就来看看Redisson的加锁实现就可用到Redisson的看门狗机制跟分布式的可重入
【重点主要是依赖lua脚本的原子性,实现加锁和释放锁的功能】

Redisson的用法

使用redisson实现分布式锁的操作步骤,三部曲

第一步: 获取锁 RLock redissonLock = redisson.getLock(lockKey);
第二步: 加锁,实现锁续命功能 redissonLock.lock();
第三步:释放锁 redissonLock.unlock();

重点的地方我都标出来了

redisson.getLock(lockKey) 的逻辑

 @Override
    public RLock getLock(String name) {
        //new 了RedissonLock构造方法
        return new RedissonLock(connectionManager.getCommandExecutor(), name);
}

我们看下RedissonLock构造函数

public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
}

参数:

super(commandExecutor, name); 父类name赋值,后续通过getName()获取

commandExecutor: 执行lua脚本的executor

id 是个UUID, 后面被用来当做 和threadId组成 value值,用作判断加锁和释放锁是否是同一个线程的校验。
internalLockLeaseTime : 取自 Config#lockWatchdogTimeout,默认30秒,这个参数还有另外一个作用,锁续命的执行周期 internalLockLeaseTime/3 = 10秒

redissonLock.lock()的逻辑

 
redissonLock.lock();

//lock方法干了什么
@Override
    public void lock() {
        try {
            //加锁的主要方法
            lockInterruptibly();
        } catch (InterruptedException e) {
            //失败就中断
            Thread.currentThread().interrupt();
        }
 }

继续看 lockInterruptibly方法

@Override
    public void lockInterruptibly() throws InterruptedException {
        lockInterruptibly(-1, null);
}

继续往里面追

	@Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    
	    // 获取当前线程ID
        long threadId = Thread.currentThread().getId();
        
        // 尝试获取锁的剩余时间 
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired  ttl为空,说明没有线程持有该锁,直接返回 让当前线程加锁成功 
        if (ttl == null) {
            return;
        }

        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);
	
	
        // 死循环  
        try {
            while (true) {
            
                // 再此尝试获取锁的剩余时间 ,如果为null, 跳出循环
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }
                // waiting for message   如果ttl >=0 说明 有其他线程持有该锁
                if (ttl >= 0) {
                     // 获取信号量,尝试加锁,设置最大等待时间为ttl
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                
                     // 如果ttl小于0 (-1 ,-2 ) 说明已经过期,直接获取
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

大流程已经梳理完了,我们看下 Long ttl = tryAcquire(leaseTime, unit, threadId); 看门狗机制了

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(leaseTime, unit, threadId));
}
tryAcquireAsync(leaseTime, unit, threadId)
 private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
 

        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        
        // 刚开始  leaseTime 传入的是 -1 ,所以走这个分支
        
        // 1)尝试加锁  待会细看 先把主要的逻辑梳理完
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        
       // 2) 注册监听事件
        ttlRemainingFuture.addListener(new FutureListener<Long>() {
            @Override
            public void operationComplete(Future<Long> future) throws Exception {
                if (!future.isSuccess()) {
                    return;
                }

                Long ttlRemaining = future.getNow();
                // lock acquired
                if (ttlRemaining == null) {
                
	                // 3)获取锁成功的话,给锁延长过期时间 
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }
// 1)尝试加锁  待会细看 先把主要的逻辑梳理完
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                     TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
 <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));
    }

//lua 脚本
//KEYS[1] ---------> getName()
//ARGV[1] ---------> internalLockLeaseTime
//ARGV[2] ---------> getLockName(threadId) 

ARGV[2] ---------> getLockName(threadId) 实现如下

String getLockName(long threadId) {
        return id + ":" + threadId;
}

这个id就是自开始实例化RedissonLock的id ,是个UUID
我们来解释下这段lua脚本【讲的可重入逻辑】

 // 如果 lockKey不存在 ,设置 使用hset设置 lockKey ,field为 uuid:threadId ,value为1 ,并设置过期时间

 //就是这个命令 

 //127.0.0.1:6379> hset lockkey  uuid:threadId 1
 //(integer) 1
 //127.0.0.1:6379> PEXPIRE lockkey internalLockLeaseTime

  "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; " +
       
 // 如果 lockKey 存在和 filed 和 当前线程的uuid:threadId相同  key 加1 ,执行多少次 就加多次  设置过期时间  其实就是如下命令
 //127.0.0.1:6379> HEXISTS lockkey uuid:threadId
 //(integer) 1
 //127.0.0.1:6379> PEXPIRE lockkey  internalLockLeaseTime

   "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; " +
       
 // 最后返回 lockkey的 pttl 
 "return redis.call('pttl', KEYS[1]);"

那继续监听时间中的 scheduleExpirationRenewal(threadId); 逻辑【看门狗续命】

private void scheduleExpirationRenewal(final long threadId) {
        if (expirationRenewalMap.containsKey(getEntryName())) {
            return;
        }

        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        
           // 重点是run方法 
            @Override
            public void run(Timeout timeout) throws Exception {
                
                
               // 又是lua脚本  判断是否存在,存在就调用pexpire 
                RFuture<Boolean> future = 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));
                // 监听事件中又 调用了自己  scheduleExpirationRenewal
                future.addListener(new FutureListener<Boolean>() {
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        
                        if (future.getNow()) {
                            // reschedule itself
                            scheduleExpirationRenewal(threadId);
                        }
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
            task.cancel();
        }
    }

redissonLock.unlock();逻辑

@Override
    public void unlock() {
        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + Thread.currentThread().getId());
        }
        if (opStatus) {
            cancelExpirationRenewal();
        }
    }

重点看 unlockInnerAsync(Thread.currentThread().getId())

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                "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.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

    }

又是lua脚本,核心就是 把value减到为0 ,删除key

KEYS[1] ---------> getName()
KEYS[2] ---------> getChannelName()
ARGV[1] ---------> LockPubSub.unlockMessage
ARGV[2] ---------> internalLockLeaseTime

ARGV[2] ---------> getLockName(threadId)

Redisson的逻辑参考
附上流程图
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值