乐趣区

关于后端:Redis-中是如何实现分布式锁的

喷泉丑陋是因为它有压力,瀑布壮观是因为它没有进路,水能穿石是因为它永远在保持。

分布式锁常见的三种实现形式:

  1. 数据库乐观锁;
  2. 基于 Redis 的分布式锁;
  3. 基于 ZooKeeper 的分布式锁。

要点

Redis 要实现分布式锁,以下条件应该失去满足
互斥性

  • 在任意时刻,只有一个客户端能持有锁。

不能死锁

  • 客户端在持有锁的期间解体而没有被动解锁,也能保障后续其余客户端能加锁。

容错性

  • 只有大部分的 Redis 节点失常运行,客户端就能够加锁和解锁。

实现

能够间接通过 set key value px milliseconds nx 命令实现加锁,通过 Lua 脚本实现解锁。

// 获取锁(unique_value 能够是 UUID 等)SET resource_name unique_value NX PX  30000
// 开释锁(lua 脚本中,肯定要比拟 value,避免误会锁)if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

代码解释

  • set 命令要用 set key value px milliseconds nx,代替 setnx + expire 须要分两次执行命令的形式,保障了原子性,
  • value 要具备唯一性,能够应用 UUID.randomUUID().toString() 办法生成,用来标识这把锁是属于哪个申请加的,在解锁的时候就能够有根据;
  • 开释锁时要验证 value 值,避免误会锁;
  • 通过 Lua 脚本来防止 Check And Set 模型的并发问题,因为在开释锁的时候因为波及到多个 Redis 操作(利用了 eval 命令执行 Lua 脚本的原子性);

加锁代码剖析
首先,set()退出了 NX 参数,能够保障如果已有 key 存在,则函数不会调用胜利,也就是只有一个客户端能持有锁,满足互斥性。其次,因为咱们对锁设置了过期工夫,即便锁的持有者后续产生解体而没有解锁,锁也会因为到了过期工夫而主动解锁(即 key 被删除),不会产生死锁。最初,因为咱们将 value 赋值为 requestId,用来标识这把锁是属于哪个申请加的,那么在客户端在解锁的时候就能够进行校验是否是同一个客户端。
解锁代码剖析
将 Lua 代码传到 jedis.eval()办法里,并使参数 KEYS[1]赋值为 lockKey,ARGV[1]赋值为 requestId。在执行的时候,首先会获取锁对应的 value 值,查看是否与 requestId 相等,如果相等则解锁(删除 key)。
存在的危险
如果存储锁对应 key 的那个节点挂了的话,就可能存在失落锁的危险,导致呈现多个客户端持有锁的状况,这样就不能实现资源的独享了。

  1. 客户端 A 从 master 获取到锁
  2. 在 master 将锁同步到 slave 之前,master 宕掉了(Redis 的主从同步通常是异步的)。
    主从切换,slave 节点被升级为 master 节点
  3. 客户端 B 获得了同一个资源被客户端 A 曾经获取到的另外一个锁。导致存在同一时刻存不止一个线程获取到锁的状况。

redlock 算法呈现

这个场景是假如有一个 redis cluster,有 5 个 redis master 实例。而后执行如下步骤获取一把锁:

  1. 获取以后工夫戳,单位是毫秒;
  2. 跟下面相似,轮流尝试在每个 master 节点上创立锁,过期工夫较短,个别就几十毫秒;
  3. 尝试在大多数节点上建设一个锁,比方 5 个节点就要求是 3 个节点 n / 2 + 1;
  4. 客户端计算建设好锁的工夫,如果建设锁的工夫小于超时工夫,就算建设胜利了;
  5. 要是锁建设失败了,那么就顺次之前建设过的锁删除;
  6. 只有他人建设了一把分布式锁,你就得一直轮询去尝试获取锁。


Redis 官网给出了以上两种基于 Redis 实现分布式锁的办法,具体阐明能够查看:

https://redis.io/topics/distlock。

Redisson 实现

Redisson 是一个在 Redis 的根底上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的 Java 罕用对象,还实现了可重入锁(Reentrant Lock)、偏心锁(Fair Lock、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)等,还提供了许多分布式服务。

Redisson 提供了应用 Redis 的最简略和最便捷的办法。Redisson 的主旨是促成使用者对 Redis 的关注拆散(Separation of Concern),从而让使用者可能将精力更集中地放在解决业务逻辑上。

Redisson 分布式重入锁用法

Redisson 反对单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例:

// 1. 结构 redisson 实现分布式锁必要的 Config
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);
// 2. 结构 RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 3. 获取锁对象实例(无奈保障是按线程的程序获取到)RLock rLock = redissonClient.getLock(lockKey);
try {
    /**
     * 4. 尝试获取锁
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     * leaseTime   锁的持有工夫, 超过这个工夫锁会主动生效(值应设置为大于业务解决的工夫,确保在锁有效期内业务能解决完)*/
    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
    if (res) {// 胜利取得锁,在这里解决业务}
} catch (Exception e) {throw new RuntimeException("aquire lock fail");
} finally{
    // 无论如何, 最初都要解锁
    rLock.unlock();}

加锁流程图

解锁流程图

咱们能够看到,RedissonLock 是可重入的,并且思考了失败重试,能够设置锁的最大等待时间,在实现上也做了一些优化,缩小了有效的锁申请,晋升了资源的利用率。

须要特地留神的是,RedissonLock 同样没有解决 节点挂掉的时候,存在失落锁的危险的问题。而现实情况是有一些场景无奈容忍的,所以 Redisson 提供了实现了 redlock 算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是须要额定的为 RedissonRedLock 搭建 Redis 环境。

所以,如果业务场景能够容忍这种小概率的谬误,则举荐应用 RedissonLock,如果无奈容忍,则举荐应用 RedissonRedLock。

退出移动版