redis分布式锁使用和源码解析

        java对并发问题一般会采用上锁的方式来控制并发,可以用sychronized或者ReentrantLock或者cas的方式来上锁,但是在分布式系统下,这些方法就会失效,一般情况下会采用分布式锁的方式来对数据进行上锁(可以用redis,也可以用zk),下面是redis分布式锁的使用方式和源码解析

        Redisson分布式锁的工具包

    public  <T> void lockData(String key, String errorMsg, Long timeSeconds, Consumer<String> consumer) {
        //为空则构建
        RLock lock = redisson.getLock("lock:" + key);
        if(timeSeconds == null){
            lock.lock();
        }
        else{
            lock.lock(timeSeconds, TimeUnit.SECONDS);
        }
        LOG.info("上锁:"+"lock:" + key+"成功");
        try {
            consumer.accept(key);
        }catch (Exception e){
            StringWriter stringWriter= new StringWriter();
            PrintWriter writer= new PrintWriter(stringWriter);
            e.printStackTrace(writer);
            LOG.error(errorMsg+",错误如下:\n" + stringWriter.getBuffer().toString());
            throw new ApiException(e.getMessage());
        }finally {
            lock.unlock();
            LOG.info("解锁:"+"lock:" + key+"成功");
        }
    }


    public <T> void tryLockData(Consumer<T> consumer, T data, String key,String errMsg) {
        //多个人同时编辑一条变更数据的时候,需要手动加锁
        RLock lock = redisson.getLock(key);
        boolean b = lock.tryLock();
        if (!b) {
            throw new ApiException(errMsg);
        }
        try {
            consumer.accept(data);
        }
        catch (Exception e){
            StringWriter stringWriter= new StringWriter();
            PrintWriter writer= new PrintWriter(stringWriter);
            e.printStackTrace(writer);
            LOG.error(key+",错误如下:\n" + stringWriter.getBuffer().toString());
            throw new ApiException(e.getMessage());
        }finally {
            lock.unlock();
        }
    }

       Redisson源码解析

        Redisson封装了 redis的一些方法并且对其进行了一些扩展,下面是对Redisson.lock()分布式锁相关的核心源码解析,看整体逻辑从上往下看,整体理完之后从下往上看(越下面的图代表进的越深)

    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // 获取到锁就直接返回
        if (ttl == null) {
            return;
        }
        //下面是没获取到锁的逻辑
        //如果没有获取到分布式锁,那么会去订阅同一个key的channel,等到时候持有锁的线程释放锁之后会唤醒这些阻塞的线程
        //这里只是做了一个订阅,其他啥都没干
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
                //不想进入阻塞,再一次的尝试获取锁
                ttl = tryAcquire(leaseTime, unit, threadId);
                //运气好获取到了,就跳出抢占锁的逻辑
                if (ttl == null) {
                    break;
                }

                //如果还是获取不到,getLatch()会返回一个信号量(值为零),然后通过Aqs的逻辑进行阻塞(LockSupport.park())
                if (ttl >= 0) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            //最后解除对redisChannel的订阅
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

        尝试获取到锁(tryAcquire)

                为Lua脚本添加监听器,上看门狗续命

                  tryAcquireAsync()方法:返回值是future,就是tryLockInnerAsync中的逻辑,因为是异步的,所以会添加一个监听器监听,如果获取到了锁,那么会对锁续命(额外添加一个新线程去定时轮询业务代码是否执行完)

    private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
        //lock方法并不会走这个逻辑(写死的-1)
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        //这行代码会用Lua语句对key进行塞值(上锁),默认超时时间是30秒
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        //上面获取锁的代码返回的是一个future,异步执行的,这边加了一个监听器,会监听业务代码执行完没
        ttlRemainingFuture.addListener(new FutureListener<Long>() {
            @Override
            public void operationComplete(Future<Long> future) throws Exception {
                //没有获取到就直接return
                if (!future.isSuccess()) {
                    return;
                }
                Long ttlRemaining = future.getNow();
                // lock acquired
                //这里代表锁已经获取到了
                if (ttlRemaining == null) {
                    //异步加一个看门狗续命,为了防止这把锁超时或者系统宕机导致一直锁删不掉
                    //这里有一个定时任务,定时时长设置为锁时长的1/3,比如锁如果是30秒,那么10秒就会扫描一下,然后如果业务代码没执行完,会把锁的时间再次设置为30秒
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

       加锁逻辑,Lua脚本解析

        tryLockInnerAsync,里面的Lua脚本翻译成中文就是

                if:如果第一次进入,代表没有锁,那么调用hset设置这个key,然后时间设置成你传进来的时间(默认是30秒)返回null

                if:如果进入了,并且进入的还是自己的那个线程(锁重入),那么就会++,并且将时间重置为传进来的那个时间,返回null

                else:前面两种都代表获取锁成功,除了那两种情况之外,就代表锁获取失败,并且会返回锁的剩余时间;

                 keys[1] = getName()锁key

                argv[1] = internalLockLeaseTime;超时时间

                argv[2] = getLockName(threadId)锁key对应的value

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

     解锁逻辑

    public void unlock() {
               //执行lua脚本解锁
        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();
        }}

        redissonLock.unlock()

                if锁已经不存在了,那么通过channel发布抢占锁的消息,return 1;

                if:锁还存在,并且尝试解锁,但是上锁的线程并不是自己,所以解锁失败,return null

                if:重入锁--,并且重置锁的超时时间 return 0

                else:已经没有锁重入了,那么直接删除锁,并且发布消息return 1

                    通过lua脚本得知,只要解锁成功便会发布消息,返回1

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

    }

              收到解锁的消息之后,那些尝试获取信号量的线程又该如何解锁

    protected void onMessage(RedissonLockEntry value, Long message) {
        //收到消息之后,会判断是否是解锁逻辑
        if (message.equals(unlockMessage)) {
            //会获取刚开始的信号量,然后去release(),释放阻塞,又会接着第一张图的循环获取资源,到此闭环
            value.getLatch().release();

            while (true) {
                Runnable runnableToExecute = null;
                synchronized (value) {
                    Runnable runnable = value.getListeners().poll();
                    if (runnable != null) {
                        if (value.getLatch().tryAcquire()) {
                            runnableToExecute = runnable;
                        } else {
                            value.addListener(runnable);
                        }
                    }
                }
                
                if (runnableToExecute != null) {
                    runnableToExecute.run();
                } else {
                    return;
                }
            }
        }
    }

        梳理逻辑:

                入口是tryAcquireAsync(),调用tryAcquire()尝试获取锁,如果获取到了,那么会加一个看门狗的续命锁,每隔设置时间的1/3时间会定时扫描一次,观察业务代码是否结束,如果没有,则会重新设置锁时间;如果获取失败则会返回锁的剩余时间,紧接着会去订阅redis里面的channel,然后进入死循环,通过调用java里面的信号量进入阻塞态,释放锁的时候会去发布解锁通知,那些阻塞的线程又会被唤醒紧接着通过死循环再去尝试获取资源

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值