redis入门第九课:实战之分布式锁

当资源在分布式环境下需要共享时,比如秒杀等场景,减库存等。redis的客户端redission提供了一种基于LUA脚本的分布式锁实现方案来解决分布式环境下资源共享问题。

1.redission使用

public class RedissonUtil {
    
    public RedissonClient getConnetion(){
        Config config = new Config();
        //  单台服务器
        config.useSingleServer().setAddress("172.0.0.1:6379");
        return Redisson.create(config);
    }
    
    /**
     * 获取锁
     * @param key
     * @return
     */
    public boolean lock(String key){
        RLock lock = getConnetion().getLock("payOrder");
        //
        try {
            //最多等待100s,上锁10s后自动释放锁
            if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
                //获取锁成功
                System.out.println("获取锁成功了!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

2. 原理分析

来看看redission是如何实现的:
tryLock(long waitTime, long leaseTime, TimeUnit unit)源码如下:

@Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        final long threadId = Thread.currentThread().getId();
        //申请锁,返回剩余过期时间
        Long ttl = tryAcquire(leaseTime, unit);
        // 无竞争...
        if (ttl == null) {
            return true;
        }
        
        //有竞争...
    }

2.1 先申请锁,然后返回锁剩余时间

  • 将异步执行的结果以同步的形式返回
    Redisson 实现的执行 Redis 命令都是异步的,但是它在异步的基础上提供了以同步的方式获得执行结果的封装。
 private Long tryAcquire(long leaseTime, TimeUnit unit) {
        return get(tryAcquireAsync(leaseTime, unit, Thread.currentThread().getId()));
    }
  • 异步获取锁
    分布式锁要确保未来的一段时间内锁一定能够被释放,因此要对锁设置超时释放的时间,在我们没有指定该时间的情况下,Redisson 默认指定为30秒
private  RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
//1.如果用户设置的有最大等待时间,则直接执行LUA脚本获取锁
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
 // 2.如果用户没有设置最大等待时间,用默认的锁超时时间去获取锁
        RFuture ttlRemainingFuture = tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.addListener(new FutureListener() {
            @Override
            public void operationComplete(Future future) throws Exception {
                if (!future.isSuccess()) {
                    return;
                }

                Long ttlRemaining = future.getNow();
                // lock acquired
                if (ttlRemaining == null) {
 // 3.锁过期时间刷新任务调度
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }
  • 使用 EVAL 命令执行 Lua 脚本获取锁
  RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand 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.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

lua脚本中有三个参数:

KEYS[1]:就是传入的加锁的key,比如:RLock lock = redisson.getLock("myLock");
ARGV[2]:代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1
ARGV[1]:代表的就是锁key的默认生存时间,默认30秒

执行分析:

  • 先判断key是否存在。执行命令:exists key。如果不存在,继续向下执行:
  • 如果不存在,执行HSET key field value命令,比如:
hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

结果如下:
myLock:{
    "8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}

这样设置之后,就表示客户端8743c9c0-0795-4907-87fd-6c719a6b4586:1对myLock这个锁完成了加锁操作。

  • 执行过期时间设置,比如:
PEXPIRE key milliseconds
PEXPIRE myLock 100000
  • 此时另一个客户端来对key进行加锁操作,又执行了一段Lua脚本,会发生什么问题呢?
    第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。
  • 接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
    执行命令判断ash里面field是否存在:
HEXISTS key field

如果不存在,则返回key的剩余生存时间。如果存在,则继续一下处理:

  • 如果存在,redission增加了锁的可重入性,通过一下命令:
增加 key 指定的哈希集中指定字段的数值
HINCRBY key field increment
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

结果如下:
myLock:{
    "8743c9c0-0795-4907-87fd-6c719a6b4586:1":2
}
  • 执行过期时间设置,比如:
PEXPIRE key milliseconds
PEXPIRE myLock 100000

执行lua脚本流程如下:


图片.png

2.2 无竞争

无竞争是直接获取到锁的。

 if (ttl == null) {
            return true;
        }

2.3 有竞争

通过tryAcquire发现锁被其它线程申请时,需要进入等待竞争逻辑中。

time -= (System.currentTimeMillis() - current);
        if (time <= 0) {
            return false;
        }
        
        current = System.currentTimeMillis();
        final RFuture subscribeFuture = subscribe(threadId);
        if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.addListener(new FutureListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        if (subscribeFuture.isSuccess()) {
                            unsubscribe(subscribeFuture, threadId);
                        }
                    }
                });
            }
            return false;
        }

        try {
            time -= (System.currentTimeMillis() - current);
            if (time <= 0) {
                return false;
            }
        
            while (true) {
                long currentTime = System.currentTimeMillis();
                ttl = tryAcquire(leaseTime, unit);
                // lock acquired
                if (ttl == null) {
                    return true;
                }

                time -= (System.currentTimeMillis() - currentTime);
                if (time <= 0) {
                    return false;
                }

                // waiting for message
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }

                time -= (System.currentTimeMillis() - currentTime);
                if (time <= 0) {
                    return false;
                }
            }
        } finally {
            unsubscribe(subscribeFuture, threadId);
        }

流程如下:


图片.png

4. 分布式锁释放

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

然后呢,另外的客户端2就可以尝试完成加锁了。

这就是所谓的 分布式锁的开源Redisson框架的实现机制。

一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

5. Redis分布式锁的缺点

其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。

但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。

此时就会导致多个客户端对一个分布式锁完成了加锁。

这时系统在业务语义上一定会出现问题, 导致各种脏数据的产生 。

所以这个就是redis cluster,或者是redis master-slave架构的 主从异步复制 导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。这也是我们项目没有使用redis作为分布式锁的方案的主要原因。

你可能感兴趣的:(redis入门第九课:实战之分布式锁)