Redis-04分布式锁

@toc:

  • 分布式锁的使用场景?
  • 分布式锁的设计要点?
  • 如何用Redis实现分布式锁?

➤ 什么情况需要分布式锁:

多个Client操作一个资源并保证互斥性, 进程内的锁机制无法实现

➤ 设计要点:

  • 互斥性: A/B Client 同时只能有一个抢占到锁;
  • 锁超时: 如果抢到锁的Client挂掉, 锁仍能在超时后被解锁;
  • 高可用: Redis节点宕机仍能保证上述特性;

➤ 方案1:

  • 加锁: SET k random_val NX PX 3000,
  • 加锁失败: 失败后休眠一段时间再尝试
  • 放锁: 释放前先Get, 如果值等于自己的 random_val, 才可以删除K // 避免线程1醒来后, 已经超时的情况, 另一个线程2已经SET了锁, 线程1误删Key的情况
    • 释放前为什么要先get, 考虑如下情况, A获取到了锁, 但A处理过程耗时很长(A的锁超时了,但A还在正常运行), A的锁过期并被清理, 线程B抢占到锁, 这时候A处理完任务开始尝试 Del自己的锁…
  • 问题: SET NX PX这种方式也有潜在的问题, 因为是在单Redis上加锁, 如果master上获取了锁, 但slave还没来得及同步锁, master就宕机了, slave成为新master, 这时候其他Client是可以抢到锁的;

➤ 方案2: // 不推荐

  • 加锁: SETNX k <timestamp + timeout>, 成功则获得锁;
  • 超时: 加速的指令可以看到K没有设置过期, 如果获得锁的Client挂掉或者休眠, 需要其他线程来删除K, 具体做法: 其他没抢到锁的客户端, Get K发现已经超时, 则发起 GETSET k <timestamp + timeout> , GETSET会返回K之前的值(上个K的过期时间), 考虑多个线程同时去GETSET的情况:
    • GETSET 返回的时间比当前时间更小, 获取锁成功;
    • 如果已经有另个线程GETSET了Key, GETSET 返回的时间比当前时间更大, 获取锁失败, 继续等待;
  • 放锁: 删除Key
  • 问题: SETNX 获得锁的线程A因为其他原因阻塞很久, 直到锁已经超时并被其他客户端 GETSET掉, 这时候线程A恢复并del了k

➤ RedLock方案:

  • RedLock为了解决什么: 上面提到的分布式锁是基于单Redis实例, RedLock是在多Redis实例上实现的分布式锁(更可靠)
  • RedLock还可保证当大多数Redis节点可用, 就可以正常加锁放锁
  • 加锁(无锁状态下抢占到锁): 客户端向所有Redis 都发起 SET k client_random_val NX PX 3000, 这个例子里锁的总超时是3秒, 假设这一阶段消耗了x秒, 那么锁的实际可用时间=3-x, 如果大多数(N/2 + 1)实例都返回成功, 并且锁的有效时间大于0, 才算是获取锁成功;
  • 测锁(尝试去加锁,有没有其他Client已经获取了锁): @todo
  • 加锁失败: 如果无法在多数实例上获取到锁, 则加锁失败, Client应向已经成功的实例发起 DEL k, 为了防止过多重试, 客户端获取锁失败需要稍等一会才能再次发起锁请求;
  • 解锁: 向所有Redis发起 DEL …
  • 问题: 假设5个实例, Client从3个实例都取得了锁, 但这时候某个实例重启了且丢失了锁, 解决方法是:
    • redis开启fsync, 同步到磁盘, 这样重启的时候可以从磁盘备份恢复数据(但一般情况下锁超时时间远小于重启耗时)
    • Client尝试抢锁, 如果发现有实例宕机, 则等待一段时间(大于锁的TTL)

➤ @ref: