解析redisson的lock

多线程下的数据一致性问题一直都是热点问题,既要考虑到数据的一致,又要考虑实现的效率,在分布式情况下,这又要成为一种新的难题。分布式锁和我们java基础中学习到的synchronized略有不同,synchronized中我们的锁是个对象,当前系统部署在不同的服务实例上,单纯使用synchronized或者lock 已经无法满足对库存一致性的判断。本次主要讲解基于rediss 实现的分布式锁

普通实现原理

说到大家熟悉的rediss分布式锁 ,大部分人都会想到:setnx+过期时间

- 获取锁(client_id 可以是UUID等)
SET lock_name client_id NX PX 30000

或者 setnx+lua

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

这种实现的方式又以下几点好处:

1.set命令要用set key value px milliseconds nx;
自动释放,避免死锁

2.释放锁时要验证vaule
避免误解锁,同时也要注意value的唯一性

Reentrant Lock

redisson已经有对 Reentrant lock算法封装,接下来对其用法进行简单介绍 参考官方wiki
首先导入 pom文件

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

简单的用法展示

RLock lock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
    isLock = isLock.tryLock();
    // 5s拿不到锁, 就认为获取锁失败。5s即5s是锁失效时间。
    isLock = isLock.tryLock(5, 5, TimeUnit.SECONDS);
    if (isLock) {
        //业务逻辑
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId。

protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
    return id + ":" + threadId;
}
获取锁

获取锁的api是 lock.tryLock()或者lock.tryLock(waitTime, leaseTime, TimeUnit),两者实现原理是一样,只不过前者获取锁的默认租约时间(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // 获取锁时向redis实例发送的命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              // 首先分布式锁的KEY不能存在,如果确实不存在,
              // 那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间
              "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; " +
              // 如果分布式锁的KEY已经存在,并且value也匹配
              // 表示是当前线程持有的锁,那么可重入次数++,并且更新失效时间
              // 执行命令 incrby lockkey uuid:threadId 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; " +
              // 最后就是都不满足的情况  获取分布式锁的KEY的失效时间毫秒数
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

加锁完成后的redis 数据结构大致 如下

lockkey :{
    "uuid:threadId": 1
}

参数说明

KEYS[1] 代表的是你加锁的那个key,比如说:RLock lock = redisson.getLock(“myLock”);这里你自己设置了加锁的那个锁key就是“myLock”。

ARGV[1] 代表的就是锁key的续约时间,默认30秒。

ARGV[2] 代表的是加锁的set的vaule,类似于下面这样 uuid:threadId(本质其实就是uuid+线程id)
释放锁

释放锁的api为 lock.unlock()

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    // 向实例都执行如下命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 如果分布式锁KEY不存在,那么向channel发布一条消息
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            // 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            // 如果就是当前线程占有分布式锁,那么将重入次数减1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            // 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
                // 重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}
Redlock

其实写到这里,我个人以为rediss的分布式锁 已经实现的很完美了,但还是有其他声音对这个方案提出了异议。Redis通过sentinel保证高可用,如果在某个时间进行主备切换,很有可能在预备slave 上还没有master节点的锁。具体流程如下

1.Redis的master节点上拿到了锁;

2.但是这个加锁的key还没有同步到slave节点;

3.master故障,发生故障转移,slave节点升级为master节点;

最终导致 导致锁丢失。

在这个背景下,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。

我理解的算法大致如下
假设有N个Redis 节点。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。
这里重点就是 完全互相独立

演示一下简单操作

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// 这里的lock1  lock2   lock3 就是从各个redis节点获取的 锁
boolean isLock;
try {
    isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(30000);
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    System.out.println("");
    redLock.unlock();
}

最大的变化就是RedLock 的初始化RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); 这里选择的是三个节点, 可以选择多个。

实现原理

RedLock 其实是RedissonMultiLock的子类,tryLock其实 是RedissonMultiLock的tryLock方法,源码如下:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // 允许的失败数量
    //  RedissonMultiLock 0 , RedLock 是少于半数 
    int failedLocksLimit = failedLocksLimit();
    List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
    // 实现要点之遍历所有节点通过EVAL命令执行lua加锁
    // locks就是初始化的传入的redis节点
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        RLock lock = iterator.next();
        boolean lockAcquired;
        try {
            // 尝试对某一个节点
            lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
        } catch (RedisConnectionClosedException|RedisResponseTimeoutException e) {
            // 如果抛出这类异常,为了防止加锁成功,但是响应失败,需要解锁
            unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception e) {
            // 抛出异常表示获取锁失败
            lockAcquired = false;
        }

        if (lockAcquired) {
            // 成功获取锁集合
            acquiredLocks.add(lock);
        } else {
            // 如果达到了允许加锁失败节点限制,那么break,即此次Redlock加锁失败
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }               
        }
    }
    return true;
}

以redis 的以哨兵 模式架构为例。假设有三个独立的哨兵模式集群,如果要获取分布式锁,那么需要向这3个sentinel集群通过EVAL命令执行LUA脚本,需要过半数,即至少2个sentinel集群响应成功,才算成功的以Redlock算法获取到分布式锁。这样就算某一个集群失败了,其他两个成功也一样可以加锁成功。
详细步骤如下
1.获取当前时间戳 startTIme

2.从注册的实例上,使用相同的key和 uuid:threadId 获取锁(和重入锁的获取方式一直)。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。

3.锁是否成功状态 -> nowTime - startTIme =获取锁的时间 。当获得锁的时间小于有效时间,则判断得锁成功,成功个数 大于等于 N/2+1, 返回加锁成功

4.如果加锁失败。那则执行解锁操作

理解误区

1.Redlock 理解误区
才开始的想法是 Redlock 向redis集群或者哨兵模式发送lua脚本, 通过hash 算法还是过固定到某一个redis槽点上。那发三次 不都是发到同一个槽点上面了吗?过后 才发现, 完全独立!!!其实 是向三个不同的redis 集群发送lua脚本,利用多个独立的redis节点确保锁的丢失问题。 算是一种典型的空间换时间的方法吧,毕竟在实际生产中,多台完全独立的redis集群,成本还是挺大的。但是在一次业务开发中,我发现RedissonMultiLock的另一个用途,比如一次购买多件商品, 在减库存的操作时,对每个商品加锁,业务实现挺复杂的,这个时候RedissonMultiLock的重要性 就体现出来了。

2.失效时间设置
如果业务执行时间超过了失效时间,那么锁就会被自动释放,目前 我找到的方案时启动一个线程,自动续约,比如每隔十秒,查询某个锁的状态, 如果是持有状态, 那么自动续约。很明显这又提升了分布式锁的复杂度。这个问题就留给大家帮我解答了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值