redisson分布式锁

      接口做幂等的方式很多,我们应用使用分布式锁+插入明细来做幂等。但是发现幂等失效了,最终确认是业务执行尚未结束,还没有插入明细。但是客户端第二个访问就来到了,此时呢,分布式锁的时间也失效了。

      也就是两个问题:1是业务执行为什么很慢,这个就有很多种情况暂不考虑。考虑第二种情况,能不能加长分布式锁的时间。由此仔细看了看redisson的分布式锁。

先来一个redisson的分布式锁测试类


    public void testReentrantLock(String lockName)throws Exception{

        RLock lock = null;
        try {
            lock = getRedisson().getLock(lockName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        lock.lock();
        System.out.println(lock.getName());
       //此处当为业务逻辑
        Thread.sleep(3000);

        lock.unlock();

    }

    public RedissonClient getRedisson() throws IOException {
        Config config  = new Config();
        config.useClusterServers().addNodeAddress("redis://192.168.2.13:9001");
            RedissonClient redisson = Redisson.create(config);
            return redisson;
    }

    public static void main(String[] args)throws Exception {
        DistributedLocks locks = new DistributedLocks();
        locks.testReentrantLock("lock1");

    }

观察测试类:有两个需要关注的入口,getLock 和lock两个方法。

先来看第一个:getLock方法属于Redisson类里的方法。

    public RLock getLock(String name) {
        return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
    }

 new RedissonLock的构造函数,基本上都是一些属性的赋值。先关注几个属性: id,internalLockLeaseTime 还有entryName,commandExcutor

    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.entryName = this.id + ":" + name;
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }

首先,观察到id是ConnectionManager类的一个属性,点进去发现是一个uuid类型,可以明确知道了,这是一个UUID随机值。

然后internalLockLeaseTime 是Config类下的一个lockWatchdogTimeout 。既然是Config类的属性,那么证明这个属性可以配置。从字面上理解,这个就是看门狗的一个超时时间。

还有一个entryName 是id加上name拼接而成,基本可以确认是往redis存入的key,因为name是我们传过去的。至于其他属性,等遇到再分析吧。(往后证明key是我们存入的业务表示,而uuid+线程ID是要给hash结构的field)

接着看lock方法,最终我们走到了lock方法,参数有leaseTime,时间单位,以及一个布尔值,布尔值先不管它。


    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        if (ttl != null) {
            RFuture future = this.subscribe(threadId);
            this.commandExecutor.syncSubscription(future);

            try {
                while(true) {
                    ttl = this.tryAcquire(leaseTime, unit, threadId);
                    if (ttl == null) {
                        return;
                    }

                    if (ttl >= 0L) {
                        try {
                            this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        } catch (InterruptedException var13) {
                            if (interruptibly) {
                                throw var13;
                            }

                            this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        }
                    } else if (interruptibly) {
                        this.getEntry(threadId).getLatch().acquire();
                    } else {
                        this.getEntry(threadId).getLatch().acquireUninterruptibly();
                    }
                }
            } finally {
                this.unsubscribe(future, threadId);
            }
        }
    }

接下来有两个内容:先模拟程序走一遍,进入tryAcquire方法,如果看过AQS一类的源码的话,基本就确认这个是获取锁的方法。那么会根据返回的ttl ,根据字面意思我们知道这是redis里key的剩余生存时间。很显然,当为null的时候表示获取到锁,不为null的话,就需要进一步操作,先不管它。接着往方法里跳,根据tryAcquireAsync的方法名以及返回类型。我们可以知道redisson是用了异步的方式去获取锁,然后外面又是一个get方法保证同步获取返回的ttl结果。

    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
    }
  private  RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            RFuture ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }

进入方法之后,显然有两个分支,一个是当leaseTime为-1的情况,一个是不为-1的情况。

咱们先看不是 -1的时候,进入tryLockInnerAsync方法,进入之后,我们主要关注下 eval的表达式

     RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.commandExecutor.evalWriteAsync(this.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.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

eval表达式主要的意思就是:如果不存在该key,那么就直接hset 赋值。如果存在该key,并且也是该field的话,直接自增(跑不了,这里有利于实现重入锁自增)。所以使用redisson一定要保证redis版本支持eval表达式。

redisson存入redis的锁结构: key:就是我们自己业务命名的字符串。而field呢其实是一个UUID加上冒号再加上一个当前线程ID。这样可以在分布式情况下保证唯一。这样基本上redisson如何加锁的主要流程我们就清楚了。

接下来再返回tryAcquireAsync方法里 leaseTime不是-1的代码块

 private  RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            RFuture ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }

我们可以看到当不是-1时,除了使用tryLockInnerAsync加锁意外,还有要给RFuture对象的一个omComplete方法,很明显我们要关注下 scheduleExpirationRenewal方法。根据方法名我们就知道这个东西和定时任务有关了。不出意外这就是看门狗程序的内容了。那我们进入scheduleExpirationRenewal方法。

private void scheduleExpirationRenewal(long threadId) {
        RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
        RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            this.renewExpiration();
        }

    }

根据代码内容,我们知道参数时当前线程的ID,然后会把线程ID加入到一个Entry里面。具体干啥我也不清楚。最终要的是有一个renewExpiratino方法,接着再进入该方法。看门狗的面目马上就出来了。


    private void renewExpiration() {
        RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                    if (res) {
                                        RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

首先分析第一行,会根据EntryName从一个map里获取一个ExpirationEntry对象,然后建立一个Timeout类型的task。然后可以看出来这个task就是一个任务。每次都会延时 internalLockleaseTime/3 。具体如何实现延时的,目前还没找到源码。每次更新成功之后,有会重复调用自身的renewExpiration方法。这样就能够实现不断续约了。

 

你可能感兴趣的:(源码分析)