技术分享 | Redis 之分布式锁

2023-01-05 13:52:42 浏览数 (1)

作者:贲绍华

爱可生研发中心工程师,负责项目的需求与维护工作。其他身份:柯基铲屎官。

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


引言:之前的一篇《缓存穿透 - Redis Module之布隆过滤器》中,介绍了布隆过滤器的使用,本篇主要通过实际业务场景来讲述Redis中关于分布式锁与Red lock的相关内容。

一、什么是分布式锁

分布式锁指的就是分布式系统下使用的锁(说了好像等于没说),在分布式系统中,常常需要协调组件间的动作。

如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

二、场景案例

举个例子,假设ATM机A、ATM机B同时对同一账户入账,它们是两个独立且相同的业务系统。由于余额是要在现有金额上进行增加的,中间的前后操作会出现时差。

则A或B增加余额时都需要先获取互斥锁,锁住需要操作的资源,增加余额后释放锁。

三、使用Redis实现分布式锁

3.1 带TTL的key

在Redis中创建一个key,这个key有一个失效时间(TTL),以保证锁最终会被自动释放掉(这个对应上边脑图的活性A)

即:

代码语言:sql复制
get->不存在,获取成功->set->ttl->del

当客户端释放资源(解锁)时,会删除掉这个key。

3.2 setNX

使用3.1的方式存在一个问题,那就是每次都得先get一下看看这个key是否存在,插入之后还需要再设置TTL。

非常的繁琐且get->set->ttl操作并不是原子性的,需要额外处理类似get不存在但set又存在、锁被其他客户端释放掉的场景。

Redis提供了setNX命令(如果不存在就插入,并设置key的超时时间。如果存在则什么也不做)

使用命令:

代码语言:sql复制
set key value px milliseconds nx
3.3 setNX Lua

在3.2中还存在一个问题,例如:

  1. ATM-A 获取锁成功
  2. ATM-A操作中,持续阻塞
  3. 设置key过期了,锁被自动释放
  4. ATM-B获取到锁
  5. ATM-A操作此时恢复处理,操作完成开始释放锁
  6. ATM-B持有的锁被ATM-A释放掉了

为了解决这个问题,释放锁之前需要对比value值(需要保证值的唯一性),释放锁的时候只有key存在并且存储的值和当前客户端指定的值一样才能删除成功。

同样的,get->delete操作并不是原子性的,需要使用lua脚本来同时完成:

代码语言:sql复制
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
3.4 锁的续期

锁的过期时间是插入key时直接设置的一个大概时间区间,实际业务执行过程中不能精确预估具体的执行的时间。会出现客户端正在处理时key TTL过期导致的被提前释放问题。

解决方式:可以让获得锁的客户端开启一个守护进程,用于给快要过期的key增加超时时间。当业务执行完成时,再主动关闭该守护进程。

3.5 高可用问题

单节点Redis实例会导致获取锁失败,业务直接停摆。但想通过增加slave节点解决这个问题其实是行不通的,因为Redis的主从同步通常是异步的。

会出现以下的问题:

  1. ATM-A从Redis master节点获取到锁
  2. 在master将锁同步到slave之前,master宕机
  3. slave节点被晋级为master节点
  4. ATM-B获取到了锁,ATM-A业务还在进行中,导致安全失效

四、Redis Module - RedLock

RedLock正是为了防止单点故障而设计的基于Redis的分布式锁实现。

它是由N(大于等于3的基数个)个Redis master节点组成的,节点与节点之间不使用复制或任何隐式协调系统。

当客户端需要获取锁时,会尝试顺序从N个实例中获取,在所有实例中使用相同的key与value。

  • 官方文档:https://redis.io/docs/manual/patterns/distributed-locks
  • 使用方式:https://github.com/wujunwei/redlock/blob/master/README.md
4.1 获取锁:

当N / 2 1个节点获取到锁时则成功得到锁(因为如果小于一半判断为成功的话,有可能出现多个客户端都成功获取锁的情况, 从而使锁失效)

4.2 释放锁:

客户端应该向所有Redis节点发起释放锁的操作。即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点

4.3 延迟重启:

一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。

这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

4.4 优点:
  • 有效防止单点故障
4.5 缺点:
  • 需要维护多台Redis Master,使用起来相当笨重
  • RedLock算法对时钟依赖性强,若集群中的某个节点发生时钟异常问题,可能会因此而引发锁安全性问题
  • 如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关

0 人点赞