分布式锁—3.Redisson的公平锁一

大纲

1.Redisson公平锁RedissonFairLock概述

2.公平锁源码之加锁和排队

3.公平锁源码之可重入加锁

4.公平锁源码之新旧版本对比

5.公平锁源码之队列重排

6.公平锁源码之释放锁

7.公平锁源码之按顺序依次加锁

1.Redisson公平锁RedissonFairLock概述

(1)非公平和公平的可重入锁

(2)Redisson公平锁的简单使用

(3)Redisson公平锁的初始化

(1)非公平和公平的可重入锁

一.非公平可重入锁

锁被释放后,排队获取锁的线程会重新无序获取锁,没有任何顺序性可言。

二.公平可重入锁

锁被释放后,排队获取锁的线程会按照请求获取锁时候的顺序去获取锁。公平锁可以保证线程获取锁的顺序,与其请求获取锁的顺序是一样的。也就是谁先申请获取到这把锁,谁就可以先获取到这把锁。公平可重入锁会把各个线程的加锁请求进行排队处理,保证先申请获取锁的线程,可以优先获取锁,从而实现所谓的公平性。

三.可重入的非公平锁和公平锁不同点

可重入的非公平锁和公平锁,在整体的技术实现框架上都是一样的。唯一的不同点就是加锁和解锁的逻辑不一样。非公平锁的加锁逻辑,比较简单。公平锁的加锁逻辑,要加入排队机制,保证各个线程排队能按顺序获取锁。

(2)Redisson公平锁的简单使用

Redisson的可重入锁RedissonLock指的是非公平可重入锁,Redisson的公平锁RedissonFairLock指的是公平可重入锁。

Redisson的公平可重入锁实现了java.util.concurrent.locks.Lock接口,保证了当多个线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒之后才会继续分配下一个线程。

RedissonFairLock是RedissonLock的子类。RedissonFairLock的锁实现框架,和RedissonLock基本一样。而在获取锁和释放锁的lua脚本中,RedissonFairLock的逻辑才有所区别。

复制代码
//1.最常见的使用方法
RedissonClient redisson = Redisson.create(config);
RLock fairLock = redisson.getFairLock("myLock");
fairLock.lock();

//2.10秒钟以后自动解锁,无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);

//3.尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
fairLock.unlock();

//4.Redisson为公平的可重入锁提供了异步执行的相关方法
RLock fairLock = redisson.getFairLock("myLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);

(3)Redisson公平锁的初始化

复制代码
public class RedissonDemo {
    public static void main(String[] args) throws Exception {
        ...
        //创建RedissonClient实例
        RedissonClient redisson = Redisson.create(config);
        
        //获取公平的可重入锁
        RLock fairLock = redisson.getFairLock("myLock");
        fairLock.lock();//加锁
        fairLock.unlock();//释放锁
    }
}

public class Redisson implements RedissonClient {
    //Redis的连接管理器,封装了一个Config实例
    protected final ConnectionManager connectionManager;
    //Redis的命令执行器,封装了一个ConnectionManager实例
    protected final CommandAsyncExecutor commandExecutor;
    ...
    protected Redisson(Config config) {
        this.config = config;
        Config configCopy = new Config(config);
        //初始化Redis的连接管理器
        connectionManager = ConfigSupport.createConnectionManager(configCopy);
        ...  
        //初始化Redis的命令执行器
        commandExecutor = new CommandSyncService(connectionManager, objectBuilder);
        ...
    }
    
    public RLock getFairLock(String name) {
        return new RedissonFairLock(commandExecutor, name);
    }
    ...
}

public class RedissonFairLock extends RedissonLock implements RLock {
    private final long threadWaitTime;
    private final CommandAsyncExecutor commandExecutor;
    ...
    public RedissonFairLock(CommandAsyncExecutor commandExecutor, String name) {
        this(commandExecutor, name, 60000*5);
    }
    
    public RedissonFairLock(CommandAsyncExecutor commandExecutor, String name, long threadWaitTime) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.threadWaitTime = threadWaitTime;
         ...
    }
    ...
}

public class RedissonLock extends RedissonBaseLock {
    protected long internalLockLeaseTime;
    final CommandAsyncExecutor commandExecutor;
    ...
    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        //与WatchDog有关的internalLockLeaseTime
        //通过命令执行器CommandExecutor可以获取连接管理器ConnectionManager
        //通过连接管理器ConnectionManager可以获取Redis的配置信息类Config
        //通过Redis的配置信息类Config可以获取lockWatchdogTimeout超时时间
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        ...
    }
    ...
}

public abstract class RedissonBaseLock extends RedissonExpirable implements RLock {
    ...
    protected long internalLockLeaseTime;
    final String id;
    final String entryName;
    final CommandAsyncExecutor commandExecutor;
    
    public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();//获取UUID
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.entryName = id + ":" + name;
    }
    ...
}

abstract class RedissonExpirable extends RedissonObject implements RExpirable {
    RedissonExpirable(CommandAsyncExecutor connectionManager, String name) {
        super(connectionManager, name);
    }
    ...
}

public abstract class RedissonObject implements RObject {
    protected final CommandAsyncExecutor commandExecutor;
    protected String name;
    protected final Codec codec;
    
    public RedissonObject(CommandAsyncExecutor commandExecutor, String name) {
        this(commandExecutor.getConnectionManager().getCodec(), commandExecutor, name);
    }
    
    public RedissonObject(Codec codec, CommandAsyncExecutor commandExecutor, String name) {
        this.codec = codec;
        this.commandExecutor = commandExecutor;
        if (name == null) {
            throw new NullPointerException("name can't be null");
        }
        setName(name);
    }
    ...
}

public class ConfigSupport {
    ...
    //创建Redis的连接管理器
    public static ConnectionManager createConnectionManager(Config configCopy) {
        //生成UUID
        UUID id = UUID.randomUUID();
        ...
        if (configCopy.getClusterServersConfig() != null) {
            validate(configCopy.getClusterServersConfig());
            //返回ClusterConnectionManager实例
            return new ClusterConnectionManager(configCopy.getClusterServersConfig(), configCopy, id);
        }
        ...
    }
    ...
}

public class ClusterConnectionManager extends MasterSlaveConnectionManager {
    public ClusterConnectionManager(ClusterServersConfig cfg, Config config, UUID id) {
        super(config, id);
        ...
    }
    ...
}

public class MasterSlaveConnectionManager implements ConnectionManager {
    protected final String id;//初始化时为UUID
    private final Config cfg;
    protected Codec codec;
    ...
    protected MasterSlaveConnectionManager(Config cfg, UUID id) {
        this.id = id.toString();//传入的是UUID
        ...
        this.cfg = cfg;
        this.codec = cfg.getCodec();
        ...
    }
    
    public String getId() {
        return id;
    }
    
    public Codec getCodec() {
        return codec;
    }
    ...
}

2.公平锁源码之加锁和排队

(1)加锁时的执行流程

(2)获取公平锁的lua脚本相关参数说明

(3)lua脚本步骤一:进入while循环移除队列和有序集合中等待超时的线程

(4)lua脚本步骤二:判断当前线程能否获取锁

(5)lua脚本步骤三:执行获取锁的操作

(6)lua脚本步骤四:判断锁是否已经被当前线程持有(可重入锁)

(7)lua脚本步骤五:判断当前获取锁失败的线程是否已经在队列中排队

(8)lua脚本步骤六:对获取锁失败的线程进行排队

(9)获取锁失败的第一个线程执行lua脚本的流程

(10)获取锁失败的第二个线程执行lua脚本的流程

(1)加锁时的执行流程

使用Redisson的公平锁RedissonFairLock进行加锁时:首先调用的是RedissonLock的lock()方法,然后会调用RedissonLock的tryAcquire()方法,接着会调用RedissonLock的tryAcquireAsync()方法。

在RedissonLock的tryAcquireAsync()方法中,会调用一个可以被RedissonLock子类重载的tryLockInnerAsync()方法。对于非公平锁,执行到这会调用RedissonLock的tryLockInnerAsync()方法。对于公平锁,执行到这会调用RedissonFairLock的tryLockInnerAsync()方法。

在RedissonFairLock的tryLockInnerAsync()方法中,便执行具体的lua脚本。

复制代码
public class RedissonDemo {
    public static void main(String[] args) throws Exception {
        ...
        //创建RedissonClient实例
        RedissonClient redisson = Redisson.create(config);
        
        //获取公平的可重入锁
        RLock fairLock = redisson.getFairLock("myLock");
        fairLock.lock();//加锁
        fairLock.unlock();//释放锁
    }
}

public class RedissonLock extends RedissonBaseLock {
    ...
    //不带参数的加锁
    public void lock() {
        try {
            lock(-1, null, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
    
    //带参数的加锁
    public void lock(long leaseTime, TimeUnit unit) {
        try {
            lock(leaseTime, unit, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
    
    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        //加锁成功
        if (ttl == null) {
            return;
        }
        //加锁失败
        ...
    }
    
    private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }
    
    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        if (leaseTime != -1) {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            //非公平锁,接下来调用的是RedissonLock.tryLockInnerAsync()方法
            //公平锁,接下来调用的是RedissonFairLock.tryLockInnerAsync()方法
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        //对RFuture<Long>类型的ttlRemainingFuture添加回调监听
        CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
            //tryLockInnerAsync()里的加锁lua脚本异步执行完毕,会回调如下方法逻辑:
            //加锁成功
            if (ttlRemaining == null) {
                if (leaseTime != -1) {
                    //如果传入的leaseTime不是-1,也就是指定锁的过期时间,那么就不创建定时调度任务
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    //创建定时调度任务
                    scheduleExpirationRenewal(threadId);
                }
            }
            return ttlRemaining;
        });
        return new CompletableFutureWrapper<>(f);
    }
    ...
}

public class RedissonFairLock extends RedissonLock implements RLock {
    private final long threadWaitTime;//线程可以等待锁的时间
    private final CommandAsyncExecutor commandExecutor;
    private final String threadsQueueName;
    private final String timeoutSetName;
    
    public RedissonFairLock(CommandAsyncExecutor commandExecutor, String name) {
        this(commandExecutor, name, 60000*5);//传入60秒*5=5分钟
    }
    
    public RedissonFairLock(CommandAsyncExecutor commandExecutor, String name, long threadWaitTime) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.threadWaitTime = threadWaitTime;
        threadsQueueName = prefixName("redisson_lock_queue", name);
        timeoutSetName = prefixName("redisson_lock_timeout", name);
    }
    ...
    @Override
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        long wait = threadWaitTime;
        if (waitTime != -1) {
            //将传入的指定的获取锁等待时间赋值给wait变量
            wait = unit.toMillis(waitTime);
        }  
        ...
        if (command == RedisCommands.EVAL_LONG) {
            return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                //步骤一:remove stale threads,移除等待超时的线程
                "while true do " +
                    //获取队列中的第一个元素
                    //KEYS[2]是一个用来对线程排队的队列的名字
                    "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
                    "if firstThreadId2 == false then " +
                        "break;" +
                    "end;" +
                    //获取队列中第一个元素对应的分数,也就是排第一的线程的过期时间
                    //KEYS[3]是一个用来对线程排序的有序集合的名字
                    "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +
                    //如果排第一的线程的过期时间小于当前时间,说明该线程等待超时了都还没获取到锁,所以要移除
                    //ARGV[4]是当前时间
                    "if timeout <= tonumber(ARGV[4]) then " +
                        //remove the item from the queue and timeout set NOTE we do not alter any other timeout
                        //从有序集合 + 队列中移除这个线程
                        "redis.call('zrem', KEYS[3], firstThreadId2);" +
                        "redis.call('lpop', KEYS[2]);" +
                    "else " +
                        "break;" +
                    "end;" +
                "end;" +

                //check if the lock can be acquired now
                //步骤二:判断当前线程现在能否尝试获取锁,以下两种情况可以通过判断去进行尝试获取锁
                //情况一:锁不存在 + 队列也不存在;KEYS[1]是锁的名字;KEYS[2]是对线程排队的队列;
                //情况二:锁不存在 + 队列存在 + 队列的第一个元素就是当前线程;ARGV[2]是当前线程的UUID + ThreadID;
                "if (redis.call('exists', KEYS[1]) == 0) " +
                    "and ((redis.call('exists', KEYS[2]) == 0) " +
                        "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
                    //步骤三:当前线程执行获取锁的操作
                    //remove this thread from the queue and timeout set
                    //弹出队列的第一个元素 + 从有序集合中删除UUID:ThreadID对应的元素
                    "redis.call('lpop', KEYS[2]);" +
                    "redis.call('zrem', KEYS[3], ARGV[2]);" +


                    //decrease timeouts for all waiting in the queue
                    //递减有序集合中每个线程的分数,也就是递减每个线程获取锁时的已经等待时间
                    //zrange返回有序集合KEYS[3]中指定区间内(0,-1)的成员,也就是全部成员
                    "local keys = redis.call('zrange', KEYS[3], 0, -1);" +
                    "for i = 1, #keys, 1 do " +
                        //对有序集合KEYS[3]的成员keys[i]的score减去:tonumber(ARGV[3])
                        //ARGV[3]就是线程获取锁时可以等待的时间,默认是5分钟
                        "redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);" +
                    "end;" +

                    //acquire the lock and set the TTL for the lease
                    //hset设置Hash值进行加锁操作 + pexpire设置锁key的过期时间 + 最后返回nil表示加锁成功
                    "redis.call('hset', KEYS[1], ARGV[2], 1);" +
                    "redis.call('pexpire', KEYS[1], ARGV[1]);" +
                    "return nil;" +
                "end;" +

                //check if the lock is already held, and this is a re-entry(可重入锁)
                //步骤四:判断锁是否已经被当前线程持有,KEYS[1]是锁的名字,ARGV[2]是当前线程的UUID + ThreadID;
                "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;" +

                //the lock cannot be acquired, check if the thread is already in the queue
                //步骤五:判断当前获取锁失败的线程是否已经在队列中排队
                //KEYS[3]是对线程排序的有序集合,ARGV[2]是当前线程的UUID + ThreadID;
                "local timeout = redis.call('zscore', KEYS[3], ARGV[2]);" +
                "if timeout ~= false then " +
                    //the real timeout is the timeout of the prior thread in the queue, 
                    //but this is approximately correct, and avoids having to traverse the queue
                    //如果当前获取锁失败的线程已经在队列中排队
                    //那么就返回该线程等待获取锁时,还剩多少时间就超时了,外部代码拿到这个时间会阻塞等待这个时间
                    //ARGV[3]是当前线程获取锁时可以等待的时间,ARGV[4]是当前时间
                    "return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);" +
                "end;" +

                //add the thread to the queue at the end, and set its timeout in the timeout set to the timeout of
                //the prior thread in the queue (or the timeout of the lock if the queue is empty) plus the threadWaitTime
                //步骤六:对获取锁失败的线程进行排队处理
                "local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
                "local ttl;" +
                //如果在队列中排队的最后一个元素不是当前线程
                "if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
                    //lastThreadId是在队列中排最后的线程,ARGV[2]是当前线程的UUID+线程ID,ARGV[4]是当前时间
                    //因为拥有最大过期时间的线程在队列中是排最后的
                    //所以可通过队列中的最后一个元素的过期时间,计算当前线程的过期时间
                    //从而保证新加入队列和有序集合的线程的过期时间是最大的
                    //下面这一行会计算出:还有多少时间,当前队列中排最后的线程就会过期,外部代码拿到这个时间会阻塞等待这个时间
                    //这样后一个加入队列的线程,会阻塞等待前一个加入队列的线程的过期时间
                    "ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
                "else " +
                    //下面这一行会计算出:还有多少时间,锁就会过期,外部代码拿到这个时间会阻塞等待这个时间
                    "ttl = redis.call('pttl', KEYS[1]);" +
                "end;" +
                //计算当前线程在排队等待锁时的过期时间
                "local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
                //把当前线程作为一个元素插入有序集合,并设置元素分数为该线程在排队等待锁时的过期时间
                //然后再把当前线程作为一个元素插入队列尾部
                "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
                    "redis.call('rpush', KEYS[2], ARGV[2]);" +
                "end;" +
                "return ttl;",
                Arrays.asList(getRawName(), threadsQueueName, timeoutSetName),
                unit.toMillis(leaseTime),
                getLockName(threadId),
                wait,
                currentTime
            );
        }
        ...
    }
    ...
}

(2)获取公平锁的lua脚本相关参数说明

KEYS[1]是getRawName(),它是一个Hash数据结构的key,也就是锁的名字,比如"myLock"。

KEYS[2]是threadsQueueName,它是一个用来对线程排队的队列的名字,多个客户端线程申请获取锁时,会到这个队列里进行排队。比如"redisson_lock_queue:{myLock}"。

KEYS[3]是timeoutSetName,它是一个用来对线程排序的有序集合的名字,这个有序集合可以自动按照每个数据指定的分数进行排序。比如"redisson_lock_timeout:{myLock}"。

ARGV[1]是leaseTime,代表锁的过期时间。如果leaseTime没有指定,默认就是internalLockLeaseTime = 30秒。

ARGV[2]是getLockName(threadId),代表客户端UUID + 线程ID。

ARGV[3]是threadWaitTime,代表线程可以等待的时间(默认5分钟)。

ARGV[4]是currentTime,代表当前时间。

(3)lua脚本步骤一:进入while循环移除队列和有序集合中等待超时的线程

while循环中首先执行命令:"lindex redisson_lock_queue:{myLock} 0",也就是获取"redisson_lock_queue:{myLock}"这个队列中的第一个元素。一开始该队列是空的,所以什么都获取不到,firstThreadId2为false。此时就会break掉,退出while循环。

如果获取到队列中的第一个元素,那么就会执行zscore命令:从有序集合中获取该元素对应的分数,也就是该元素对应线程的过期时间。如果过期时间比当前时间小,那么就要从队列和有序集合中移除该元素。否则,也会break掉,退出while循环。

复制代码
//步骤一:remove stale threads,移除等待超时的线程
"while true do " +
    //获取队列中的第一个元素
    //KEYS[2]是一个用来对线程排队的队列的名字
    "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
    "if firstThreadId2 == false then " +
        "break;" +
    "end;" +
    //获取队列中第一个元素对应的分数,也就是排第一的线程的过期时间
    //KEYS[3]是一个用来对线程排序的有序集合的名字
    "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +
    //如果排第一的线程的过期时间小于当前时间,说明该线程等待锁超时了都还没获取到锁,所以要移除
    //ARGV[4]是当前时间
    "if timeout <= tonumber(ARGV[4]) then " +
        //remove the item from the queue and timeout set NOTE we do not alter any other timeout
        //从有序集合+队列中移除这个线程
        "redis.call('zrem', KEYS[3], firstThreadId2);" +
        "redis.call('lpop', KEYS[2]);" +
    "else " +
        "break;" +
    "end;" +
"end;" +

(4)lua脚本步骤二:判断当前线程能否获取锁

判断条件一:

首先执行命令"exists myLock",判断锁是否存在。一开始没有线程加过锁,所以判断条件肯定是成立的,该条件为true。

判断条件二:

接着执行命令"exists redisson_lock_queue:{myLock}",看队列是否存在。一开始也没有这个队列,所以这个条件也肯定成立,该条件为true。

判断条件三:

如果有这个队列,则判断队列存在的条件不成立,执行"或"后面的判断。也就是执行命令"lindex redisson_lock_queue:{myLock} 0",判断队列的第一个元素是否是当前线程的UUID + ThreadID。

复制代码
//check if the lock can be acquired now
//步骤二:判断当前线程现在能否尝试获取锁,以下两种情况可以通过判断去进行尝试获取锁
//情况一:锁不存在 + 队列也不存在;KEYS[1]是锁的名字;
//情况二:锁不存在 + 队列存在 + 队列的第一个元素就是当前线程;ARGV[2]是当前线程的UUID + ThreadID;
"if (redis.call('exists', KEYS[1]) == 0) " +
    "and ((redis.call('exists', KEYS[2]) == 0) " +
        "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
    ...
"end;" +

总结当前线程现在可以尝试获取锁的情况如下:

**情况一:**锁不存在 + 队列也不存在

**情况二:**锁不存在 + 队列存在 + 队列的第一个元素就是当前线程

(5)lua脚本步骤三:执行获取锁的操作

当判断现在能否尝试获取锁的条件通过后,便会执行如下操作:

**步骤一:**执行命令"lpop redisson_lock_queue:{myLock}",弹出队列第一个元素。一开始该队列是空的,所以该命令不会进行处理。接着执行命令"zrem redisson_lock_timeout:{myLock} UUID1:ThreadID1",也就是从有序集合中删除UUID1:ThreadID1对应的元素。一开始该有序集合也是空的,所以该命令不会进行处理。

**步骤二:**执行命令"hset myLock UUID1:ThreadID1 1",进行加锁操作。在设置key为myLock的Hash值中,field为UUID1:ThreadID1的value值为1。接着执行命令"pexpire myLock 30000",设置锁key的过期时间为30秒。

最后返回nil,这样在外层代码中,就会认为加锁成功。于是就会创建一个WatchDog看门狗定时调度任务,10秒后对锁进行检查。如果检查发现当前线程还持有这个锁,那么就重置锁key的过期时间为30秒,并且重新创建一个WatchDog看门狗定时调度任务在10秒后继续进行检查。

复制代码
//check if the lock can be acquired now
//步骤二:判断当前线程现在能否尝试获取锁,以下两种情况可以通过判断去进行尝试获取锁
//情况一:锁不存在 + 队列也不存在;KEYS[1]是锁的名字;KEYS[2]是对线程排队的队列;
//情况二:锁不存在 + 队列存在 + 队列的第一个元素就是当前线程;ARGV[2]是当前线程的UUID + ThreadID;
"if (redis.call('exists', KEYS[1]) == 0) " +
    "and ((redis.call('exists', KEYS[2]) == 0) " +
        "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
    //步骤三:当前线程执行获取锁的操作
    //remove this thread from the queue and timeout set
    //弹出队列的第一个元素 + 从有序集合中删除UUID:ThreadID对应的元素
    "redis.call('lpop', KEYS[2]);" +
    "redis.call('zrem', KEYS[3], ARGV[2]);" +

    //decrease timeouts for all waiting in the queue
    //递减有序集合中每个线程的分数,也就是递减每个线程获取锁时的已经等待时间
    //zrange返回有序集合KEYS[3]中指定区间内(0,-1)的成员,也就是全部成员
    "local keys = redis.call('zrange', KEYS[3], 0, -1);" +
    "for i = 1, #keys, 1 do " +
        //对有序集合KEYS[3]的成员keys[i]的score减去:tonumber(ARGV[3])
        //ARGV[3]就是线程获取锁时可以等待的时间,默认是5分钟
        "redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);" +
    "end;" +

    //acquire the lock and set the TTL for the lease
    //hset设置Hash值进行加锁操作 + pexpire设置锁key的过期时间 + 最后返回nil表示加锁成功
    "redis.call('hset', KEYS[1], ARGV[2], 1);" +
    "redis.call('pexpire', KEYS[1], ARGV[1]);" +
    "return nil;" +
"end;" +

(6)lua脚本步骤四:判断锁是否已经被当前线程持有(可重入锁)

此时会执行命令"hexists myLock UUID:ThreadID"。如果判断条件通过,则说明是持有锁的线程对锁进行了重入。于是会执行命令"hincrby myLock UUID:ThreadID 1",对key为锁名的Hash值中,field为UUID + 线程ID的value值累加1。并且执行命令"pexpire myLock 300000"重置锁key的过期时间。最后返回nil,表示重入加锁成功。

复制代码
//check if the lock is already held, and this is a re-entry(可重入锁)
//步骤四:判断锁是否已经被当前线程持有,KEYS[1]是锁的名字,ARGV[2]是当前线程的UUID + ThreadID;
"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;" +

(7)lua脚本步骤五:判断当前获取锁失败的线程是否已经在队列中排队

通过执行命令"zscore redisson_lock_timeout:{myLock} UUID:ThreadID",获取当前线程在有序集合中的对应的分数,也就是过期时间。如果获取成功则返回:当前线程等待获取锁的超时时间还剩多少,外部代码拿到这个时间会阻塞等待这个时间。

复制代码
//the lock cannot be acquired, check if the thread is already in the queue
//步骤五:判断当前获取锁失败的线程是否已经在队列中排队
//KEYS[3]是对线程排序的有序集合,ARGV[2]是当前线程的UUID+ThreadID;
"local timeout = redis.call('zscore', KEYS[3], ARGV[2]);" +
"if timeout ~= false then " +
    //the real timeout is the timeout of the prior thread in the queue, 
    //but this is approximately correct, and avoids having to traverse the queue
    //如果当前获取锁失败的线程已经在队列中排队
    //那么就返回该线程等待获取锁时,还剩多少时间就超时了,外部代码拿到这个时间会阻塞等待这个时间
    //ARGV[3]是当前线程获取锁时可以等待的时间,ARGV[4]是当前时间
    "return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);" +
"end;" +

(8)lua脚本步骤六:对获取锁失败的线程进行排队

首先获取队列中的最后一个元素。因为拥有最大过期时间的线程在队列中是排最后的,所以可通过队列中的最后一个元素的过期时间,计算当前线程的过期时间。从而保证新加入队列和有序集合的线程的过期时间是最大的。然后获取锁或者队列中排最后的线程剩余的存活时间,接着计算当前线程在排队等待锁时的过期时间。

然后把当前线程作为一个元素插入有序集合,并设置有序集合中该元素的分数为该线程在排队等待锁时的过期时间,接着再把当前线程作为一个元素插入队列尾部。

最后返回锁或者队列中排第一的线程剩余的存活时间ttl给外层代码。如果外层代码拿到的返回值是非null,那么客户端会进入一个while循环。在while循环会每阻塞等待ttl时间再尝试去进行加锁,重新执行lua脚本。

如果队列里没有元素,那么第一个加入队列的线程,会阻塞等待锁的过期时间。如果队列里有元素,那么后一个加入队列的线程,会阻塞等待前一个加入队列的线程的过期时间。

复制代码
//步骤六:对获取锁失败的线程进行排队处理
"local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
"local ttl;" +
//如果在队列中排队的最后一个元素不是当前线程
"if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
    //lastThreadId是在队列中排最后的线程,ARGV[2]是当前线程的UUID + 线程ID,ARGV[4]是当前时间
    //因为拥有最大过期时间的线程在队列中是排最后的
    //所以可通过队列中的最后一个元素的过期时间,计算当前线程的过期时间
    //从而保证新加入队列和有序集合的线程的过期时间是最大的
    //下面这一行会计算出:还有多少时间,当前队列中排最后的线程就会过期,外部代码拿到这个时间会阻塞等待这个时间
    //这样后一个加入队列的线程,会阻塞等待前一个加入队列的线程的过期时间
    "ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
"else " +
    //下面这一行会计算出:还有多少时间,锁就会过期,外部代码拿到这个时间会阻塞等待这个时间
    "ttl = redis.call('pttl', KEYS[1]);" +
"end;" +
//计算当前线程在排队等待锁时的过期时间
"local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
//把当前线程作为一个元素插入有序集合,并设置元素分数为该线程在排队等待锁时的过期时间
//然后再把当前线程作为一个元素插入队列尾部
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
    "redis.call('rpush', KEYS[2], ARGV[2]);" +
"end;" +
"return ttl;",

(9)获取锁失败的第一个线程执行lua脚本的流程

公平锁的核心在于申请加锁时,加锁失败的各个客户端会排队。之后锁被释放时,会依次获取锁,从而实现公平性。

假设此时第一个客户端线程已加锁成功,第二个客户端线程也来尝试加锁,那么会进行如下排队处理。

**步骤一:**进入while循环,移除等待超时的线程。执行命令"lindex redisson_lock_queue:{myLock} 0",获取队列排第一元素。由于此时队列还是空的,所以获取到的是false,于是退出while循环。

**步骤二:**判断当前线程现在能否尝试获取锁。因为执行命令"exists myLock",发现锁已经存在了,于是判断不通过。

**步骤三:**判断锁是否已经被当前线程持有,由于第二个客户端线程的UUID + 线程ID必然不等于第一个客户端线程。所以此时执行命令"hexists myLock UUID2:ThreadID2",发现不存在。所以此处的可重入锁的判断条件也不成立。

**步骤四:**判断当前获取锁失败的线程是否已经在队列中排队。由于当前线程是第一个获取锁失败的线程,所以判断不通过。

**步骤五:**接下来进行排队处理。

复制代码
//对获取锁失败的线程进行排队处理
"local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
"local ttl;" +
//如果在队列中排队的最后一个元素不是当前线程
"if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
    //lastThreadId是在队列中排最后的线程,ARGV[2]是当前线程的UUID+线程ID,ARGV[4]是当前时间
    //因为拥有最大过期时间的线程在队列中是排最后的
    //所以可通过队列中的最后一个元素的过期时间,计算当前线程的过期时间
    //从而保证新加入队列和有序集合的线程的过期时间是最大的
    //下面这一行会计算出:还有多少时间,当前队列中排最后的线程就会过期,外部代码拿到这个时间会阻塞等待这个时间  
    //这样后一个加入队列的线程,会阻塞等待前一个加入队列的线程的过期时间
    "ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
"else " +
    //下面这一行会计算出:还有多少时间,锁就会过期,外部代码拿到这个时间会阻塞等待这个时间
    "ttl = redis.call('pttl', KEYS[1]);" +
"end;" +
//计算当前线程在排队等待锁时的过期时间
"local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
//把当前线程作为一个元素插入有序集合,并设置元素分数为该线程在排队等待锁时的过期时间
//然后再把当前线程作为一个元素插入队列尾部
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
    "redis.call('rpush', KEYS[2], ARGV[2]);" +
"end;" +
"return ttl;"

首先执行命令"lindex redisson_lock_queue:{myLock} 0"。也就是从队列中获取最后一个元素,由于此时队列是空,所以获取不到元素。然后执行命令"ttl = pttl myLock",获取锁剩余的存活时间。

接着计算当前线程在排队等待锁时的过期时间。假设myLock剩余的存活时间ttl为20秒,那么timeout = ttl + 5分钟 + 当前时间 = 20秒 + 5分钟 + 10:00:00 = 10:05:20;

然后执行命令"zadd redisson_lock_timeout:{myLock} 10:05:20 UUID2:ThreadID2",这行命令的意思是,在有序集合中插入一个元素。元素值是UUID2:ThreadID2,元素对应的分数是10:05:20。分数会用时间的Long型时间戳来表示,时间越靠后,时间戳就越大。有序集合Sorted Set会自动根据插入的元素分数从小到大进行排序。

接着执行命令"rpush redisson_lock_queue:{myLock} UUID2:TheadID2",这行命令的意思是,将UUID2:ThreadID2插入到队列的尾部。

最后返回ttl给外层代码,也就是返回myLock剩余的存活时间。如果外层代码拿到的ttl是非null,那么客户端会进入一个while循环。在while循环会每阻塞等待ttl时间就尝试进行加锁,重新执行lua脚本。

(10)获取锁失败的第二个线程执行lua脚本的流程

如果此时有第三个客户端线程也来尝试加锁,那么会进行如下排队处理。

**步骤一:**进入while循环,移除等待超时的线程。执行命令"lindex redisson_lock_queue:{myLock} 0",获取队列排第一元素。此时获取到UUID2:ThreadID2,代表着第二个客户端线程正在队列里排队。

继续执行命令"zscore redisson_lock_timeout:{myLock} UUID2:ThreadID2",从有序集合中获取UUID2:ThreadID2对应的分数,timeout = 10:05:20。

假设当前时间是10:00:25,那么timeout <= 10:00:25的这个条件不成立,于是退出while循环。

**步骤二:**判断当前线程现在能否尝试获取锁,发现不能通过。因为执行命令"exists myLock"时,发现锁已经存在。

**步骤三:**判断锁是否已经被当前线程持有。由于第三个客户端线程的UUID + 线程ID必然不等于第一个客户端线程。所以此时执行命令"hexists myLock UUID3:ThreadID3",发现不存在。所以此处的可重入锁的判断条件也不成立。

**步骤四:**判断当前获取锁失败的线程是否已经在队列中排队。由于当前线程是第二个获取锁失败的线程,所以判断不通过。

**步骤五:**接下来进行排队处理。

复制代码
//对获取锁失败的线程进行排队处理
"local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
"local ttl;" +
//如果在队列中排队的最后一个元素不是当前线程
"if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
    //lastThreadId是在队列中排最后的线程,ARGV[2]是当前线程的UUID + 线程ID,ARGV[4]是当前时间
    //因为拥有最大过期时间的线程在队列中是排最后的
    //所以可通过队列中的最后一个元素的过期时间,计算当前线程的过期时间
    //从而保证新加入队列和有序集合的线程的过期时间是最大的
    //下面这一行会计算出:还有多少时间,当前队列中排最后的线程就会过期,外部代码拿到这个时间会阻塞等待这个时间  
    //这样后一个加入队列的线程,会阻塞等待前一个加入队列的线程的过期时间
    "ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
"else " +
    //下面这一行会计算出:还有多少时间,锁就会过期,外部代码拿到这个时间会阻塞等待这个时间
    "ttl = redis.call('pttl', KEYS[1]);" +
"end;" +
//计算当前线程在排队等待锁时的过期时间
"local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
//把当前线程作为一个元素插入有序集合,并设置元素分数为该线程在排队等待锁时的过期时间
//然后再把当前线程作为一个元素插入队列尾部
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
    "redis.call('rpush', KEYS[2], ARGV[2]);" +
"end;" +
"return ttl;"

首先执行命令"lindex redisson_lock_queue:{myLock} 0",获取到队列中的最后一个元素UUID2:ThreadID2。

然后判断条件是否成立:lastThreadId不为false + lastThreadId不是自己。由于此时的ARGV[2] = UUID3:ThreadID3,所以判断条件成立。即在队列里排队的最后一个元素并不是当前尝试获取锁的客户端线程。

于是执行:"zscore redisson_lock_timeout:{myLock} UUID2:ThreadID2" - 当前时间,也就是获取在队列中排最后的线程还有多少时间就会过期,从而得到ttl。

接着根据ttl计算当前线程在排队等待锁时的过期时间timeout,然后执行zadd和rpush命令对当前线程进行入队和排队,最后返回ttl。

3.公平锁源码之可重入加锁

持有公平锁的客户端重复进行lock.lock(),执行加锁lua脚本的流程如下:

**步骤一:**进入while循环,移除等待超时的线程。执行命令"lindex redisson_lock_queue:{myLock} 0",获取队列排第一元素。此时获取到UUID2:ThreadID2,代表着第二个客户端线程正在队列里排队。

继续执行命令"zscore redisson_lock_timeout:{myLock} UUID2:ThreadID2",从有序集合中获取UUID2:ThreadID2对应的分数,timeout = 10:05:20。

假设当前时间是10:00:25,那么timeout <= 10:00:25的这个条件不成立,于是退出while循环。

**步骤二:**判断当前线程现在能否尝试获取锁,发现不能通过。因为执行命令"exists myLock"时,发现锁已经存在。

**步骤三:**判断锁是否已经被当前线程持有。由于当前线程的UUID + 线程ID等于持有锁的线程。即此时执行命令"hexists myLock UUID:ThreadID"发现key是存在的,所以此处的可重入锁的判断条件成立。

于是会执行命令"hincrby myLock UUID:ThreadID 1",对key为锁名的Hash值中,key为UUID + 线程ID的Hash值累加1。并且执行命令"pexpire myLock 300000"重置锁key的过期时间。最后返回nil,表示重入加锁成功。

复制代码
//check if the lock is already held, and this is a re-entry(可重入锁)
//步骤四:判断锁是否已经被当前线程持有,KEYS[1]是锁的名字,ARGV[2]是当前线程的UUID+ThreadID;
"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;" +
相关推荐
毛线裤夹腿毛1 小时前
【rabbitmq基础】
网络·分布式·rabbitmq
ChinaRainbowSea2 小时前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·分布式·后端·rabbitmq·ruby·java-rabbitmq
LUCIAZZZ4 小时前
说一下分布式组件时钟一致性的解决方案
java·网络·分布式·计算机网络·操作系统·springboot·系统设计
掘金-我是哪吒6 小时前
分布式微服务系统架构第97集:JVM底层原理
jvm·分布式·微服务·架构·系统架构
掘金-我是哪吒8 小时前
分布式微服务系统架构第96集:大型跨境电商JVM调优,MongoDB、Elasticsearch (ES)、Cassandra
jvm·分布式·mongodb·微服务·系统架构
信徒_8 小时前
Kafka 如何保证消息可靠性?
数据库·分布式·kafka
寒9929 小时前
如何保证RabbitMQ消息的可靠传输?
java·分布式·rabbitmq
IT成长日记10 小时前
【Hadoop入门】Hadoop生态圈概述:核心组件与应用场景概述
大数据·hadoop·分布式
三次握手四次挥手10 小时前
Apache Kafka全栈技术解析
分布式·kafka·apache
小样vvv11 小时前
【面试篇】Kafka
分布式·kafka