Advertisement

30 | 如何使用Redis实现分布式锁?

阅读量:

目录

单机上的锁和分布式锁的联系与区别

分布式锁的两个要求

基于单个 Redis 节点实现分布式锁

上锁

解锁

上锁、判断、解锁如何保证原子性?

setnx

风险点1 :未释放锁

解决: 锁变量设置一个过期时间

风险点2 :B释放了A得锁

解决:设置锁得值要能区分来自不同客户端或不同线程的锁操作

lua脚本释放锁

执行lua脚本

基于多个 Redis 节点实现高可靠的分布式锁

分布式锁算法 Redlock

总结


单机上的锁和分布式锁的联系与区别

单个计算节点上的锁定机制:当一个线程执行加法互斥操作时。其实质就是在检测当前互斥标志是否已被占用。如果当前标志值是零,则会将其设置为一以表明成功获得了互斥权限;否则系统会返回一个错误状态信息说明加法互斥失败的原因是有其他线程已经占据了该资源。而释放互斥标志的操作则是将当前标志值归零以便其他线程也能进行访问

换个锁得值可以自定义,然后判断之

分布式锁:分布式锁也可以通过一个变量来实现。在加锁过程中需要判断锁变量的值,在释放操作中将锁变量设置为0以表明客户端不再持有该资源。

此外,分布式锁变量需要由一个共享存储系统来维护。

使用分布式锁得三部曲:

  1. 读取
  2. 判断
  3. 设置共享存储系统中的锁变量值

分布式锁的两个要求

  • 在实现分布式锁的过程中,则需确保这些 lock 操作保持原子性。
    • 共享存储系统中存储了所有相关的 lock 变量;如果共享存储系统出现故障或无法正常工作,则可能导致客户端无法执行相应的 lock 操作。因此,在设计分布式 lock 机制时,则需特别关注和优化共享存储系统的稳定性和可靠性。

基于单个Redis节点实现分布式锁

Redis 通过键值对来存储锁变量,并进一步接收来自不同客户端发送的加锁和释放锁的操作请求。

上锁

同时发起加锁请求的客户端包括A和C。由于Redis采用了单线程机制接收 request,并不具备并发处理能力,即便这两个 client 同时向Redis发送了相应的 add lock command,因此Redis将这些 request依次执行。

单线程为什么那么快呢?

解锁

当客户端A拥有锁时,锁变量lock_key被设置为1。在客户端A完成对锁的解锁操作后,Redis将执行相应的操作以释放该锁。

lock_key 的值置为 0,表明已经没有客户端持有锁了。

上锁、判断、解锁如何保证原子性?

setnx

实际上,在大多数情况下"setnx"命令不仅仅指的是Redis提供的那条命令。"通常所说的setnx"这一用法可能源于对Redis数据库结构的理解偏差。

在Redis中使用set命令时通常会设置nx参数以避免数据持久化的问题。该命令目前提供了多种可选参数设置。

SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]

具体用法参考 SET | Redis

可以用 SETNX 和 DEL 命令组合来实现加锁和释放锁操作

复制代码
  
    
 // 加锁
    
 SETNX lock_key 1
    
 // 业务逻辑
    
 DO THINGS
    
 // 释放锁
    
 DEL lock_key
    
    
    
    
    代码解读

风险点1 :未释放锁

运行了SETNX命令后随后加锁,在操作共享数据时出现了故障,并未能成功释放锁。

解决: 锁变量设置一个过期时间

风险点2 :B释放了A得锁

在发起 SETNX 命令进行加锁后,在这种情况下,
当客户端 B 执行 DEL 命令释放 lock 时,
则会导致客户端 A 的 lock 被错误地释放。
若客户端 C 同时也在尝试发起加锁请求,
则能够成功地获取到该 lock 并开始操作共享的数据。

解决:设置锁得值要能区分来自不同客户端或不同线程的锁操作

fileloader解析文件就是这么使用分布式锁得

lua脚本释放锁

Lua脚本(unlock.script)负责编写并实现释放锁操作的伪代码描述中提到:KEYS[1]代表lock_key这一变量存储位置;而ARGV[1]则对应于当前客户端的一个唯一标识符;这些变量参数均源自我们在执行该Lua脚本时所使用的输入参数集合中。

复制代码
  
    
 //释放锁 比较unique_value是否相等,避免误释放
    
 if redis.call("get",KEYS[1]) == ARGV[1] then
    
     return redis.call("del",KEYS[1])
    
 else
    
     return 0
    
 end
    
    
    
    
    代码解读

执行lua脚本

复制代码
    redis-cli --eval unlock.script lock_key , unique_value
    
    代码解读

Redis在处理锁相关操作时会依次进行以下步骤:首先包含读取锁变量、接着判断其值、最后删除该变量。而当Redis解析Lua脚本时,则能够以一种确保整个 lock release 过程具有严格原子性的方法来完成这些操作

-- update 2023年6月6日10:07:04

java

复制代码
 public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {

    
     String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
    
         "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    
     List<String> keys = new ArrayList<>();
    
     List<String> values = new ArrayList<>();
    
     keys.add(key);
    
     values.add(UniqueId);
    
     values.add(String.valueOf(seconds));
    
     Object result = jedis.eval(lua_scripts, keys, values);
    
     //判断是否成功
    
     return result.equals(1L);
    
 }
    
    
    
    
    代码解读

基于多个Redis节点实现高可靠的分布式锁

当Redis实例出现故障性崩溃时,其上的锁机制会失效.这会导致客户端无法执行相应的锁操作,并对业务流程造成干扰.

分布式锁算法 Redlock

Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。

Redlock算法的核心理念是通过让客户端与多个独立运行的Redis实例依次进行加锁请求。当客户端能够与超过一半数量的Redis实例成功执行加锁操作时,则判定该客户端已实现分布式锁定;否则,则无法实现锁定。在这种情况下(即若有任何一个Redis实例发生故障),由于其他实例仍可保存这些锁定变量,在这种情况下(故障发生后),确保即使其中任何一个Redis实例出现故障时(故障影响下),依然能维持键值的一致性,并保证链式反应中的问题得到解决。

第一步是,客户端获取当前时间。

第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作。

该处的锁定操作与单实例上的锁定操作具有相似性。
该系统采用 SET 命令,并附加 NX、EX/PX 选项。
同时配合客户端独有的唯一标识符。
然而,在某些情况下,
例如某个 Redis 实例出现故障,
为了确保在该情形下 Redlock 算法仍能正常运行,
我们需要设置相应的超时参数。

当客户端尝试对一个 Redis 实例进行加锁请求但持续未完成(完成目标)一段时间后(即未成功),此时客户端将转向下一个 Redis 实例继续发起加锁请求)。建议将加锁操作的超时时间设置为与其有效时间窗口相匹配(通常设置为几十到一百多毫秒)。

第三步是,在客户端完成对所有 Redis 实例进行加锁操作之后,需要统计整个加锁过程的总时间消耗。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

  1. 条件一:客户端通过多数节点(即至少N/2+1个)的Redis实例成功地获得了锁资源;
  2. 条件二:客户端获得锁资源的总耗时未超出其有效时间范围。

第四阶段:当这两个前提条件得到满足时,必须重新评估这把锁的有效时间其有效时间将被确定为最初有效时间减去客户端获取该锁所需的总耗时。

关于Redlock存在争议,在个人博客平台探讨了基于Redis实现分布式锁的安全性问题(上篇)?访问http://zhangtielei.com/posts/blog-redlock-reasoning.html查看详细讨论

总结

K大评论

通过采用 SET lock_key unique_val EX $second NX 命令来确保加锁操作具有原子性,并设定锁的有效期。

2、锁的过期时间要提前评估好,要大于操作共享资源的时间

3、在每个线程加锁的过程中设定随机数值,在释放锁时检查所设定的值是否与最初设定的一致,并以防止他人误用自己已锁定的资源。

4、当执行锁解除操作时,在虚拟内存管理单元(VMMU)中运行 Lua 脚本以确保操作的原子性,默认策略仅通过设置过期时间来触发自动锁释放机制。

基于Redlock协议的多节点系统,在加锁过程中绝大多数节点均完成操作。只有当获取锁所需的时间不超过其有效期限时才认为是成功的。这种思路似乎与Zk协议有着相似之处。

Redlock 在解封锁定机制时必须确保对全部节点进行操作(即便某个节点尝试加锁失败),因为在完成一次成功的加锁操作后可能存在服务端已经完成后续操作的情形;若因网络延迟导致客户端未能收到成功确认的网络包反馈,则必须解除所有节点所保留的潜在锁定状态以确保系统的稳定运行

在使用 Redlock 技术时需确保机器时钟不会出现跳变,并由运维团队负责维护这一技术的应用。具体而言,在运行多个节点的系统中需要具备一定的专业技能才能确保系统的稳定性和可靠性。例如,在一个包含三个节点的系统中(如图所示),假设线程 A 成功地对其中两个节点施加了锁(即实现了对这些节点的互斥访问)。然而,在这种情况下如果有一个节点出现了时钟跳变,则会导致该锁提前失效(这一观点在 Redis 社区和分布式系统专家中存在广泛的争议)。同时,在另外两个节点上也完成了加锁操作后发现 Redlock 完全失效(这一现象往往被认为是Redis作者和分布式系统专家争论的重要点之一)

如果为了追求效率而采用单一 Redis 节点的分布式锁机制,则该方案的一个不足之处在于偶尔可能导致锁失效;然而其优点在于结构简单且效率较高。

如果业务对最终结果有较高的精度要求,则Redlock是一个值得推荐采用的解决方案;然而,在具体实施过程中,请注意其虽然功能强大但资源消耗较大。

全部评论 (0)

还没有任何评论哟~