redis 分布式锁深度剖析

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: redis 分布式锁

redis 分布式锁的实现主要是基于 redis 的 setnx 命令(setnx 命令解释见:http://doc.redisfans.com/string/setnx.html),我们来看一下 setnx 命令的作用:

redis-setnx.png

 

1、redis 分布式锁的基本实现

redis 加锁命令:

SETNX resource_name my_random_value PX 30000

这个命令的作用是在只有这个 key 不存在的时候才会设置这个 key 的值(NX 选项的作用),超时时间设为 30000 毫秒(PX 选项的作用) 这个 key 的值设为 “my_random_value”。这个值必须在所有获取锁请求的客户端里保持唯一。

SETNX 值保持唯一的是为了确保安全的释放锁,避免误删其他客户端得到的锁。举个例子,一个客户端拿到了锁,被某个操作阻塞了很长时间,过了超时时间后自动释放了这个锁,然后这个客户端之后又尝试删除这个其实已经被其他客户端拿到的锁。所以单纯的用 DEL 指令有可能造成一个客户端删除了其他客户端的锁,通过校验这个值保证每个客户端都用一个随机字符串’签名’了,这样每个锁就只能被获得锁的客户端删除了。

既然释放锁时既需要校验这个值又需要删除锁,那么就需要保证原子性,redis 支持原子地执行一个 lua 脚本,所以我们通过 lua 脚本实现原子操作。代码如下:

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

2、业务逻辑执行时间超出锁的超时限制导致两个客户端同时持有锁的问题

如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。

不难发现正常情况下锁操作完后都会被手动释放,常见的解决方案是调大锁的超时时间,之后若再出现超时带来的并发问题,人工介入修正数据。这也不是一个完美的方案,因为但业务逻辑执行时间是不可控的,所以还是可能出现超时,当前线程的逻辑没有执行完,其它线程乘虚而入。并且如果锁超时时间设置过长,当持有锁的客户端宕机,释放锁就得依靠 redis 的超时时间,这将导致业务在一个超时时间周期内不可用。

基本上,如果在执行计算期间发现锁快要超时了,客户端可以给 redis 服务实例发送一个 Lua 脚本让 redis 服务端延长锁的时间,只要这个锁的 key 还存在而且值还等于客户端设置的那个值。 客户端应当只有在失效时间内无法延长锁时再去重新获取锁(基本上这个和获取锁的算法是差不多的)。

当锁超时时间快到期且逻辑未执行完,延长锁超时时间的伪代码:

if  redis.call("get",KEYS[1]) == ARGV[1] then 
        redis.call("set",KEYS[1],ex=3000)
else 
        getDLock();//重新获取锁

3、redis 的单点故障主从切换带来的两个客户端同时持有锁的问题

生产中 redis 一般是主从模式,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。

不过这种不安全也仅仅是在主从发生 failover 的情况下才会产生,而且持续时间极短,业务系统多数情况下可以容忍。

4、RedLock 算法

如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,可以考虑 redlock。 Redlock 算法是由 Antirez 发明的,它的流程比较复杂,不过已经有了很多开源的 library 做了良好的封装,用户可以拿来即用,比如 redlock-py。

import redlock
addrs = [{
  "host": "localhost",
  "port": 6379,
  "db": 0
}, {
  "host": "localhost",
  "port": 6479,
  "db": 0
}, {
  "host": "localhost",
  "port": 6579,
  "db": 0
}]
dlm = redlock.Redlock(addrs)
success = dlm.lock("user-lck-laoqian", 5000)
if success:
    print 'lock success'
    dlm.unlock('user-lck-laoqian')
else:
    print 'lock failed'

RedLock 算法的核心原理:

使用 N 个完全独立、没有主从关系的 Redis master 节点以保证他们大多数情况下都不会同时宕机,N 一般为奇数。一个客户端需要做如下操作来获取锁:

1. 获取当前时间(单位是毫秒)。

2. 轮流用相同的 key 和随机值在 N 个节点上请求锁,在这一步里,客户端在每个 master 上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是 10 秒钟,那每个节点锁请求的超时时间可能是 5-50 毫秒的范围,这个可以防止一个客户端在某个宕掉的 master 节点上阻塞过长时间,如果一个 master 节点不可用了,我们应该尽快尝试下一个 master 节点。

3. 客户端计算第二步中获取锁所花的时间,只有当客户端在大多数 master 节点上成功获取了锁((N/2) +1),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。

4. 如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。

5. 如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1) 还是因为总消耗时间超过了锁释放时间,客户端都会到每个 master 节点上释放锁,即便是那些他认为没有获取成功的锁。

5、知识扩展

5.1 为什么 lua 脚本结合 redis 命令可以实现原子性

Redis 提供了非常丰富的指令集,但是用户依然不满足,希望可以自定义扩充若干指令来完成一些特定领域的问题。Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。

 

redis-lua 交互 --.png

5.2 redis 可重入分布式锁

要实现可重入锁,方法很简单,当加锁失败时判断锁的值是不是跟当前线程设置值相同,伪代码如下:

if setnx == 0 
      if get(key) == my_random_value 
            //重入 
      else 
           //不可重入 
else 
      //获取了锁,等价于可重入

参考文档:


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
14天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
42 5
|
17天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
34 8
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
57 16
|
27天前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
38 5
|
2月前
|
NoSQL Redis 数据库
计数器 分布式锁 redis实现
【10月更文挑战第5天】
51 1
|
2月前
|
NoSQL 算法 关系型数据库
Redis分布式锁
【10月更文挑战第1天】分布式锁用于在多进程环境中保护共享资源,防止并发冲突。通常借助外部系统如Redis或Zookeeper实现。通过`SETNX`命令加锁,并设置过期时间防止死锁。为避免误删他人锁,加锁时附带唯一标识,解锁前验证。面对锁提前过期的问题,可使用守护线程自动续期。在Redis集群中,需考虑主从同步延迟导致的锁丢失问题,Redlock算法可提高锁的可靠性。
83 4
|
2月前
|
缓存 NoSQL 算法
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!
|
NoSQL Redis 数据库
用redis实现分布式锁时容易踩的5个坑
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 近有不少小伙伴投入短视频赛道,也出现不少第三方数据商,为大家提供抖音爬虫数据。 小伙伴们有没有好奇过,这些数据是如何获取的,普通技术小白能否也拥有自己的抖音爬虫呢? 本文会全面解密抖音爬虫的幕后原理,不需要任何编程知识,还请耐心阅读。
用redis实现分布式锁时容易踩的5个坑
|
NoSQL Java 关系型数据库
浅谈Redis实现分布式锁
浅谈Redis实现分布式锁