Redis分布式锁其实是使用Redis实现的分布式锁,利用了Redis的key过期机制,SETNX机制(即如果不存在对应的key才可以set成功),但如何去实现这样一把分布式锁主要是Redis客户端框架来做的事,事实上锁这个高层API就是由这样的客户端框架暴露出来的,比如jedis,redisson等。接下来我们就分别看看jedis,redisson这样的框架底层如何与redis打交道,然后对上如何暴露简单的锁API。

Jedis分布式锁机制

引入jar包:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

然后来一个使用锁的demo:

public class RedisTest {

    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final int expireTime = 30000;

    private Jedis jedis = new Jedis();

    public void testLock(){
        //加锁
        boolean mylock = this.tryLock("mylock");
        try {
            //如果加锁成功
            if(mylock){
                //具体的业务代码
            }
        } finally {
            // 4.解锁
            this.releaseLock("mylock");
        }

    }

    public boolean tryLock(String lockKey) {
        String threadId = String.valueOf(Thread.currentThread().getId());
        String result = jedis.set(lockKey, threadId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    public boolean releaseLock(String lockKey) {
        String threadId = String.valueOf(Thread.currentThread().getId());
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(threadId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

以上例子中,至于获取锁失败后的重试暂不考虑。

加锁:

可以从上面的demo中看出jedis加锁使用的程序语句是:

jedis.set(lockKey, threadId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)

简单追一下源码便可以知道,其实是把命令:

SET key value NX PX expireTime

发送给了redis,redis判断如果这个key不存在就设置成功,并且设置key的过期时间 expireTime;如果redis判断这个key存在就设置失败。

解锁:

解锁使用的是lua脚本来实现的:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

因为要先判断一下是不是加锁的那个人,当判断是加锁的那个人的时候才可以进行解锁。(其中value记录了一个加锁人才知道的值,这里我们简单处理,记录成了当前线程ID值)。

因为有这样的先判断再执行两个步骤,要保证原子性就必要用使用lua脚本,然后lua脚本会作为一个命令发送给redis,redis在执行lua脚本时会保证原子性,即把它当做一个最小粒子的命令来执行。相反,如果不使用lua脚本,那么必然需要两条命令,两条命令会分别发给redis,分别执行,网络通讯效率低,而且有原子性风险。

总结:

jedis对于分布式锁的支持不太好,并没有封装好分布式锁API对外暴露,只是简单的封装了一下redis命令,如果要项目中依赖了jedis又要使用分布式锁,就只能自己去封装实现分布式锁了,方方面面都需要考虑到。

Redisson分布式锁机制

引入jar包:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.7</version>
</dependency>

先来一个使用demo看看:

public class RedissonLockTest {
    
    public void testLock(){
        // 1. Create config object
        Config config = new Config();
        config.useClusterServers()
                // use "rediss://" for SSL connection
                .addNodeAddress("redis://127.0.0.1:7181")
                .addNodeAddress("redis://127.0.0.1:7182")
                .addNodeAddress("redis://127.0.0.1:7183");

        // 2. Create Redisson instance
        // Sync and Async API
        RedissonClient redisson = Redisson.create(config);
        RLock lock = redisson.getLock("myLock");

        // 3.加锁
        lock.lock();
        try {
            //具体业务代码
        } finally {
            // 4.解锁
            lock.unlock();
        }
    }
}

封装的很好,getLock之后,加锁解锁API和JDK的ReentrantLock一样简洁!接下来我们追源码看看Redisson底层是如何与Redis打交道实现加锁、、锁等待 以及 解锁 的。

获取锁:

redisson.getLock("myLock")追下去,发现其实获取的是一个RedissonLock:

@Override
    public RLock getLock(String name) {
        //这里传入的name就是锁的名称,用于标识一把锁,后面会用到
        return new RedissonLock(commandExecutor, name);
    }

lock.lock()其实调用的RedissonLock.lock():

@Override
    public void lock() {
        try {
            //第一个参数为leaseTime,默认传-1
            lock(-1, null, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        //第一个参数为waitTime,默认传-1
        //使用当成线程ID,肯定是用于标识当前“操作人”,但不同实例中的线程ID可能是会重复的哦,看看后面会怎么来处理
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        //省略其他不相关代码...
    }

 这里只有tryAcquire()方法是我们需要关注的,继续追下去:

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 {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        //省略不相关代码...
    }

从前面可以发现传入的leaseTime默认为-1,所以进else分支语句里面的tryLockInnerAsync():

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        //要发给Redis的lua脚本以及参数
        return evalWriteAsync(getRawName(), 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }

原来就是给Redis发了一段lua脚本嘛,咦,脚本里面的KEYS[1], ARGV[1], ARGV[2] 是什么东东?点进去看看evalWriteAsync()方法的定义:

protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
        //省略不相关代码...
    }

keys就是我们传入的Collections.singletonList(getRawName())嘛,对应的就是KEYS[1],getRawName()获取到的就是前面传入的锁名称,结合lua脚本可以知道:把锁名称作为了hash结构外层那个key

params就是unit.toMillis(leaseTime), getLockName(threadId),而 getLockName(threadId)里面就是返回了 当前连接Redis的ConnectionManager的id 加上 当前线程ID,对应lua脚本里面的ARGV[2],也就是hash结构里面那个key!这样才可以唯一标识一个“操作人”嘛

unit.toMillis(leaseTime)就对应了乱脚本里面的ARGV[1],作为key的过期时间。

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

为了便于表述,先提前说明一下:

  • hash key: hash结构外面的key
  • hash inner key: hash结构里面那个key

这段lua脚本的意思就是:先判断一个指定的锁名称的hash key是否存在,如果不存在的话,给指定的hash inner key进行自增,并给hash key一个过期时间,返回;如果hash key存在的话,看看hash inner key是否存在,如果存在的话,如果存在的话说明是当前持有锁的人来了,进行自增,并刷新hash key的过期时间,返回;如果hash inner key不存在的话,说明不是当前持有锁的人,获取一下这个 hash key的的存活时间,返回。

维持锁:

那锁有过期时间,如果在持有锁期间,锁过期了怎么办呢?redisson开发者肯定想得到,所以有了对应的watch dog机制,具体源码就在tryAcquireAsync中的scheduleExpirationRenewal():

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 {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                if (leaseTime != -1) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    //过期时间刷新机制
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

追下去会发现这样一个核心方法:

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getRawName(), 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.singletonList(getRawName()),
                internalLockLeaseTime, getLockName(threadId));
    }

还是lua脚本,先判断了一下自己是否是锁的持有人,如果是的话,那么就刷新一下hash key的存活时间。

锁等待:

从上面的获取锁的那段lua脚本分析中我们可以知道:对于获取锁失败的线程,会获取一下hash key的存活时间然后返回。返回到了这里:

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        RFuture<RedissonLockEntry> future = subscribe(threadId);
        if (interruptibly) {
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }

        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // 如果ttl 为null,说明是获取锁的线程,直接跳出循环即可
                if (ttl == null) {
                    break;
                }

                // 如果ttl不为空,说明是获取锁失败的线程
                if (ttl >= 0) {
                    //如果ttl大于0,说明锁还没有过期,当前线程会等待指定时间之后再去尝试获取锁
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    //如果ttl小于0,那么说明锁已过期,当前线程会立刻尝试去获取锁
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

然后在while循环中,不断重新尝试获取锁,直到成功。当然轮询的时候会根据拿到的ttl,等待指定时间之后,再尝试去获取锁。

释放锁:

@Override
    public void unlock() {
        try {
            get(unlockAsync(Thread.currentThread().getId()));
        } catch (RedisException e) {
            if (e.getCause() instanceof IllegalMonitorStateException) {
                throw (IllegalMonitorStateException) e.getCause();
            } else {
                throw e;
            }
        }
    }
@Override
    public RFuture<Void> unlockAsync(long threadId) {
        RPromise<Void> result = new RedissonPromise<>();
        RFuture<Boolean> future = unlockInnerAsync(threadId);

        //省略不相关代码...
    }

查看最普通的锁的释放过程:RedissonLock.unlockAsync()

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), 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(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

lua脚本解释:如果不是当前持有锁的人,那么立刻返回;如果是,那么对hash inner key 进行减一操作,然后查看结果值,如果结果值大于0,那么刷新hash key的过期时间;如果结果值小于等于0,那么就删除hash key,并发布锁释放的消息。

总结:

redisson才真正去封装出了分布式锁,底层通过lua脚本与redis打交道,上层暴露出极其简单的类似ReentranLock的锁API。

分布式锁就是利用这样子的一个结构:

lockName: {    
        connectionManagerId+currentThreadId : 1
}

lockName就是自定义的锁名称,connectionManagerId就是当前连接的ID,currentThreadId是当前线程ID,1是当前锁没有重入过,如果重入一次,会变为2

分布式锁的整体实现逻辑可以概括为:

获取锁:先判断一个指定的锁名称的 hash key是否存在,如果不存在的话,给指定的hash inner key进行自增,并给hash key一个过期时间,获取锁成功,返回;如果存在的话,看看hash inner key是否存在,如果存在的话,如果存在的话说明是当前持有锁的人来了,进行锁重入:对hash inner key进行自增,并刷新hash key的过期时间,返回;如果hash inner key不存在的话,说明不是当前持有锁的人,获取锁失败,获取一下这个 hash key的的存活时间,返回。

维持锁:先判断了一下自己是否是锁的持有人,如果是的话,那么就刷新一下hash key的存活时间。

等待锁:获取锁失败之后,在while循环中,不断重新尝试获取锁,直到成功。当然轮询的时候会根据拿到的ttl,等待指定时间之后再轮询。

释放锁:如果不是当前持有锁的人,那么立刻返回;如果是,那么对hash inner key 进行减一操作,然后查看结果值,如果结果值大于0,那么刷新hash key的过期时间;如果结果值小于等于0,那么就删除hash key,并发布锁释放的消息。