敖丙思维导图--分布式锁的实现比较

在这里插入图片描述

基于数据库

  1. 基于表主键唯一做分布式锁
  2. 基于表字段版本号做分布式锁 (基于MVCC机制,对数据库连接的开销无法忍受的。)
  3. 基于数据库排他锁做分布式锁 (select xxx for update)

基于 Zookeeper

独占锁和读写锁

  1. 独占锁

1、多个客户端竞争创建 lock 临时节点
2、其中某个客户端成功创建 lock 节点,其他客户端对 lock 节点设置 watcher
3、持有锁的客户端删除 lock 节点或该客户端崩溃,由 Zookeeper 删除 lock 节点
4、其他客户端获得 lock节点被删除的通知
5、重复上述4个步骤,直至无客户端在等待获取锁了
在这里插入图片描述

  1. 读写锁
    一开始,所有的客户端都会创建自己的锁节点。客户端从 Zookeeper 端获取 /share_lock 下所有的子节点。
    读锁节点(满足其中一个即可)
    1、自己创建的节点序号排在所有其他子节点前面
    2、自己创建的节点前面无写锁节点
    写锁节点
    自己创建的锁节点是否排在其他子节点前面

1、所有客户端创建自己的锁节点
2、从 Zookeeper 端获取 /share_lock 下所有的子节点
3、判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则对自己关心的锁节点设置 watcher
4、持有锁的客户端删除自己的锁节点,某个客户端收到该节点被删除的通知,并获取锁
5、重复步骤4,直至无客户端在等待获取锁了
在这里插入图片描述

为什么不用zk来做分布式锁

CAP 理论告诉我们,一个分布式系统不可能同时满足以下三种

  • 一致性(C:Consistency)
  • 可用性(A:Available)
  • 分区容错性(P:Partition Tolerance)

zk本身更多是偏向于支持CP的模式,分布式锁需要对高可用有一定支持性。

当分布式锁加锁过程中,出现未知异常,需要主动释放锁,通常会采用try…finally的模版方式来做实现。
对于每次加锁都应该设置超时机制,否则会一直处于抢占资源的状态不做释放。尤其是高并发场景中,如果加锁过程中出现了某些不可预料的情况,导致锁没有正常释放,一直在redis中存储且没有设置过期时间,那么会一直占用redis的资源。

基于 Redis

setnx():该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。
getset()命令:该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。

双重防死锁,使用setNx + getSet两个原子性方法。

  1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
  2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
  3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
  4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
  5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
public void closeOrderTaskV3(){
        log.info("关闭订单定时任务启动");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout","5000"));
        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
        if(setnxResult != null && setnxResult.intValue() == 1){
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }else{
            //未获取到锁,继续判断,判断时间戳,看是否可以重置并获取到锁
            String lockValueStr = RedisShardedPoolUtil.get(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            if(lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)){
                String getSetResult = RedisShardedPoolUtil.getSet(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
                //再次用当前时间戳getset。
                //返回给定的key的旧值,->旧值判断,是否可以获取锁
                //当key没有旧值时,即key不存在时,返回nil ->获取锁
                //这里我们set了一个新的value值,获取旧的值。
                if(getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr,getSetResult))){
                    //真正获取到锁
                    closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }else{
                    log.info("没有获取到分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }
            }else{
                log.info("没有获取到分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
        }
        log.info("关闭订单定时任务结束");
    }

当然,我们可以最便捷的使用Redisson实现分布式锁。

Redis实现的分布式锁为什么设置过期时间

1.网络抖动
进程A中的一个线程获取到了锁,然后执行finally中的释放锁的代码时,由程序到Redis的网络不好了,所以释放锁失败。
2.服务端宕机
进程A获取到了锁,Redis服务器宕机了,所以锁没有释放。

解锁的步骤有2步:需要先判断自己是否能够删除锁(是否是当前线程加的锁),然后再执行删除锁,这个过程由于是2个步骤,所以需要保证原子性才行,否则就会出问题。

Redisson

https://www.jianshu.com/p/47fd7f86c848
对于加锁过程中,业务调用接口长时间堵塞(但是并没有抛出异常,例如说查询数据的数据量非常大,处理时间很久),堵塞时间远远超过了加锁的时常,那么这个时候应该设计一种自动给锁进行延期的机制。 这里可以参考一下redisson框架的设计思路,内部采用了watch dog机制来做这块的优化。

加锁

当我们调用lock方法,定位到lockInterruptibly。在这里,完成了加锁的逻辑。

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    
    //当前线程ID
    long threadId = Thread.currentThread().getId();
    //尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // 如果ttl为空,则证明获取锁成功
    if (ttl == null) {
        return;
    }
    //如果获取锁失败,则订阅到对应这个锁的channel
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    commandExecutor.syncSubscription(future);

    try {
        while (true) {
            //再次尝试获取锁
            ttl = tryAcquire(leaseTime, unit, threadId);
            //ttl为空,说明成功获取锁,返回
            if (ttl == null) {
                break;
            }
            //ttl大于0 则等待ttl时间后继续尝试获取
            if (ttl >= 0) {
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                getEntry(threadId).getLatch().acquire();
            }
        }
    } finally {
        //取消对channel的订阅
        unsubscribe(future, threadId);
    }
    //get(lockAsync(leaseTime, unit));
}
获取锁
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {

    //如果带有过期时间,则按照普通方式获取锁
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    
    //先按照30秒的过期时间来执行获取锁的方法
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(
        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        
    //如果还持有这个锁,则开启定时任务不断刷新该锁的过期时间
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit,     
                            long threadId, RedisStrictCommand<T> command) {

        //过期时间
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  //如果锁不存在,则通过hset设置它的值,并设置过期时间
                  "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; " +
                  //如果锁已存在,并且锁的是当前线程,则通过hincrby给数值递增1
                  "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; " +
                  //如果锁已存在,但并非本线程,则返回过期时间ttl
                  "return redis.call('pttl', KEYS[1]);",
        Collections.<Object>singletonList(getName()), 
                internalLockLeaseTime, getLockName(threadId));
    }
  • 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
  • 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
  • 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间,加锁失败

上面的两种实现方式都是针对单机Redis实例而进行的。如果我们有多个Redis实例,请参阅Redlock算法

RedLock

RedLock 的思想是使用多台 Redis Master ,节点完全独立,节点间不需要进行数据同步,因为 Master-Slave 架构一旦 Master 发生故障时数据没有复制到 Slave,被选为 Master 的 Slave 就丢掉了锁,另一个客户端就可以再次拿到锁。锁通过 setNX(原子操作) 命令设置,在有效时间内当获得锁的数量大于 (n/2+1) 代表成功,失败后需要向所有节点发送释放锁的消息。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值