Redis-分布式锁

风雨同行  金牌会员 | 2025-3-7 13:11:30 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 978|帖子 978|积分 2934

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
为什么必要分布式锁

分布式锁是解决分布式系统中并发控制标题的关键机制。在单体应用中,我们可以使用Java的synchronized或ReentrantLock来实现线程同步,但这些机制只在单个JVM内有用。
当应用扩展为分布式架构后,多个服务实例大概同时访问和修改共享资源.
传统的单机锁无法跨进程协调资源访问,这就必要分布式锁来解决。"
核心场景举例

我认为分布式锁主要应用在以下几个典范场景:

  • 防止重复使用
    比方支付系统中,防止同一订单被重复处理。
  • 包管数据一致性
    最典范的就是库存管理,防止超卖标题。在我负责的电商项目中,秒杀活动期间假如没有分布式锁,多个服务实例同时读取库存并扣减,会导致数据不一致。
  • 分布式任务调理
    确保定时任务只被一个服务实例执行,避免重复执行。
  • 限流控制
    在分布式环境下控制对共享资源的并发访问量。"
技能原理简述

一个有用的分布式锁必要满足几个关键特性:


  • 互斥性:恣意时候只有一个客户端能持有锁
  • 避免死锁:纵然客户端瓦解,锁也能主动开释
  • 高可用:锁服务自己不应成为系统瓶颈
  • 高性能:获取和开释锁的使用应该是高效的
    常见的实现方式有基于Redis、Zookeeper或数据库的方案,各有优缺点。在我的项目中,我主要使用Redis实现分布式锁,因为它性能高、实现相对简单。"
项目中必要注意的优化和思考

写项目的时候有一些必要注意的标题:


  • 锁的粒度:锁粒度过粗会影响并发性能,过细会增加复杂度。我们通常按业务资源ID设计锁粒度,如商品ID。
  • 锁的超时时间:必要根据业务处理时间合理设置,过短大概导致业务未完成锁就开释,过长则大概在客户端异常时长时间壅闭其他哀求。
  • 锁的可重入性:在某些场景下必要支持同一客户端多次获取同一把锁。
    性能考量:分布式锁虽然解决了并发标题,但也引入了网络开销,必要在一致性和性能间取得平衡。
    厥后引入了Redisson框架,它提供了更美满的分布式锁实现,包罗主动续期(看门狗机制)、可重入锁、读写锁等高级特性,进一步提拔了系统的可靠性。"
小结

总的来说,分布式锁是分布式系统中不可或缺的组件,它解决了单机锁无法解决的跨进程资源协调标题。选择合适的分布式锁实现,必要根据业务场景、一致性要求和性能需求综合考虑。在实际应用中,我们不但要关注锁的功能实现,还要考虑异常环境处理、性能优化和可用性保障。"
分布式锁的本质

分布式锁的本质是在分布式环境下实现的互斥协调机制。与单机锁差别,分布式锁必要解决的核心标题是:如安在没有共享内存的环境下,协调多个分布式节点对共享资源的访问。
从技能实现角度看,分布式锁依赖于一个所有节点都能访问的共享协调点(如Redis、Zookeeper或数据库),通过原子使用和一致的协议来确保在恣意时候只有一个客户端可以或许获取锁。
一个美满的分布式锁应具备四个关键特性:


  • 互斥性:恣意时候只有一个客户端能持有锁
  • 防死锁:纵然客户端瓦解,锁也能主动开释
  • 高可用:锁服务的可用性不应成为系统瓶颈
  • 一致性:所有节点对锁状态有一致的认知
分布式锁的实现也受CAP理论约束,差别实现方式在一致性、可用性和分区容忍性之间有差别权衡。
比方,基于Redis的分布式锁通常偏向AP,而基于Zookeeper的实现偏向CP。
在我的实践履历中,分布式锁的选择必要根据业务场景、一致性要求和性能需求综合考虑。
比方,在我负责的xx系统中,对于xx类核心数据,我们使用了Redisson实现的Redis分布式锁,它通过’看门狗’机制解决了锁超时标题;而对于一些非核心数据,我们选择了更轻量的实现方案。
分布式锁的本质挑衅在于,它必要在分布式系统固有的网络延迟、分区和节点故障等标题存在的环境下,依然可以或许提供可靠的互斥包管。这本质上是一个分布式共识标题,也是为什么完美的分布式锁实现黑白常有挑衅性的。"
Redis分布式锁的实现原理?

Redis分布式锁的实现原理是使用Redis的单线程模型和原子使用特性,通过在Redis中创建一个键值对来表现锁,确保在恣意时候只有一个客户端可以或许成功设置这个键值对,从而实现分布式环境下的互斥控制。
最基本的实现方式是使用SET lock_key unique_value NX PX timeout下令,它将获取锁和设置逾期时间归并为一个原子使用。


  • unique_value通常是客户端的唯一标识,用于安全开释锁;
  • NX表现只在键不存在时设置;
  • PX timeout设置锁的逾期时间,防止客户端瓦解导致的死锁。
    开释锁时,必要使用Lua脚本确保只开释自己持有的锁:
  1. if redis.call("get", KEYS[1]) == ARGV[1] then
  2.     return redis.call("del", KEYS[1])
  3. else
  4.     return 0
  5. end
复制代码
这种基本实现存在一些挑衅:


  • 锁逾期标题:假如业务执行时间超过锁的逾期时间,大概导致其他客户端提前获取到锁。解决方案是实现锁续期机制(看门狗),定期延伸锁的逾期时间。
  • 主从复制延迟:在Redis主从架构中,假如主节点宕机,从节点提拔为主节点前大概未完成锁的同步,导致锁丢失。
    为了进步可靠性,Redis的作者提出了Redlock算法,它使用多个独立的Redis节点来实现更可靠的分布式锁。Redlock的核心头脑是,只有在大多数Redis节点上成功获取锁,才认为获取锁成功,这样纵然部分节点故障,整体锁服务仍然可用。
    我们使用Redisson框架实现Redis分布式锁,它提供了看门狗机制、可重入锁、读写锁等高级特性,大大简化了分布式锁的使用。对于高并发的秒杀场景,我们还结合了Redis的Lua脚本功能,实现了更复杂的原子使用,如库存检查和扣减的原子性。
    Redis分布式锁虽然实现简单、性能高,但在极端环境下(如网络分区)大概无法包管完全的互斥性。
    对于对一致性要求极高的场景,大概必要考虑基于Zookeeper或etcd的分布式锁实现。"
什么是Redlock算法

单节点Redis的分布式锁在Redis节点故障时大概导致锁失效。为了进步可靠性Redis提出了Redlock算法,它使用多个独立的Redis节点来实现更可靠的分布式锁。
工作流程


  • 获取当前时间:记录开始获取锁的时间
  • 依次尝试从N个独立的Redis实例获取锁:使用类似的键名、值和逾期时间
  • 计算获取锁消耗的时间:当前时间减去开始时间
  • 检查锁是否有用:

    • 假如成功获取了超过半数(N/2+1)的Redis实例上的锁
    • 且获取锁消耗的总时间小于锁的有用时间
      则认为成功获取了分布式锁

  • 开释锁:无论是否获取成功,都尝试开释所有Redis实例上的锁
Redlock算法在肯定程度上进步了分布式锁的可靠性,但仍然存在一些标题:


  • 锁的逾期时间:假如业务执行时间超过锁的逾期时间,大概导致其他客户端提前获取到锁。
  • 主从复制延迟:在Redis主从架构中,假如主节点宕机,从节点提拔为主节点前大概未完成锁的同步,导致锁丢失。
  • 网络延迟:在分布式环境中,差别节点之间的网络延迟大概导致锁的竞争和冲突。
    Redisson框架实现了Redlock算法,并提供了更美满的分布式锁功能,如看门狗机制、可重入锁、读写锁等。
实现Redis分布式锁的方式

Redis实现分布式锁有多种方式,每种方式都有其特点和实用场景。


  • 第一种是基于SETNX下令的简单实现。这是最底子的方式,通过SETNX的原子性确保只有一个客户端能设置成功。但它存在显着缺陷:没有逾期时间机制,假如客户端瓦解,锁将永远存在。虽然可以用EXPIRE设置逾期时间,但这不是原子使用,两个下令之间客户端大概瓦解导致死锁。
  • 第二种是使用SET下令的扩展选项,这是现在最常用的方式。通过SET lock_key unique_value NX PX timeout一个下令同时完成加锁和设置逾期时间,解决了原子性标题。开释锁时,必要使用Lua脚本确保只开释自己的锁。这种方式实现简单、性能高,适合大多数场景,但仍存在锁逾期和单点故障标题。
  • 第三种是基于Lua脚本的加强实现,通过Lua脚本的原子执行特性,实现更复杂的锁语义,如可重入锁、锁续期等。
    比方,可以使用哈希结构记录线程标识和重入次数,实现可重入特性;通过定期执行脚本延伸锁逾期时间,解决锁逾期标题。
  • 第四种是Redlock算法,它使用多个独立的Redis节点,只有在大多数节点上获取锁成功才认为获取锁成功。这种方式进步了可靠性,纵然部分Redis节点故障,整体锁服务仍然可用。但实现复杂,性能相对较低,适合对锁可靠性要求极高的场景。
  • 第五种是使用Redisson框架,它提供了丰富的分布式锁实现,包罗可重入锁、公平锁、读写锁等,并通过看门狗机制主动延伸锁的逾期时间。Redisson大大简化了分布式锁的使用,适合企业级应用开辟。
    在实际项目中,我通常根据业务需求选择合适的实现方式。对于简单场景,SET下令扩展选项充足;对于复杂场景,我会使用Redisson;对于对可靠性要求极高的场景,会考虑Redlock或结合其他分布式协调服务。
分布式锁实现的要点

从核心特性来看,一个美满的分布式锁必须具备几个关键特性:
互斥性, 防死锁, 高可用, 高性能.
其次在在实现细节上,必要注意以下几点:
锁的粒度设计,超时时间设置,锁的获取计谋,异常处理机制.
从高级特性方面考虑的话:
锁续期机制,监控与告警,降级计谋
在实际项目中,我通常使用Redisson框架,它已经很好地解决了这些标题,包罗可重入性、主动续期和读写锁等高级特性,简化了分布式锁的实现和维护。"
分布式锁完全可靠吗?

分布式锁不是完全可靠的,它存在几个经典的可靠性标题:

  • 时钟漂移标题。分布式系统中差别节点的时钟大概存在弊端,这会影响基于超时的锁开释机制,大概导致多个客户端同时认为持有锁的环境。
  • 网络分区标题。当网络分区发生时,大概出现’脑裂’征象:客户端A获取锁后与锁服务器断开毗连,锁逾期开释,客户端B获取同一把锁,当网络恢复时,A和B同时认为自己持有锁。
  • 主从复制延迟标题。在Redis主从架构中,假如主节点宕机,从节点提拔为主节点前大概未完成锁的同步,导致锁丢失。
  • 锁逾期标题。假如业务执行时间超过锁的逾期时间,大概导致其他客户端提前获取到锁。
    对于一样寻常业务场景,Redis或Zookeeper的分布式锁已经充足可靠,可以通过看门狗机制、合理的超时设置和重试计谋来进步可靠性。
    于金融生意业务等核心场景,我会采用更严格的方案,如使用Redlock算法、结合数据库事故提供额外保障,或设计使用的幂等性,确保纵然锁失效也不会导致数据不一致。
如何安全地开释Redis分布式锁?为什么必要这样做?

最开始我们也是简单地用DEL下令删除锁,厥后遇到了并发标题才深入研究这块。
首先说为什么要安全开释。假设这样一个场景:


  • 线程A获取了锁,设置了10秒逾期时间
  • 但A执行业务时GC停顿了12秒
  • 这时锁已经逾期了,被线程B获取了
  • A从GC中恢复后,直接用DEL删除锁
  • 效果把B的锁给删了,导致C也能获取锁
    这就是最典范的误删标题。以是我们厥后改用了Lua脚本来开释锁:
  1. if redis.call("get",KEYS[1]) == ARGV[1] then
  2.     return redis.call("del",KEYS[1])
  3. else
  4.     return 0
  5. end
复制代码
这个脚本会先检查锁是否照旧自己的(通过之前设置的唯一标识),是才删除。用Lua脚本是为了包管这个过程的原子性。
但是厥后我们发现,纵然这样照旧不敷。因为在业务执行期间,锁逾期了就会被其他线程获取,导致并发执行。
以是我们又引入了看门狗机制。
看门狗实在就是一个主动续期的配景线程。
它会每隔一段时间(好比10秒)检查锁是否照旧自己的,假如是就续期。
这样只要持有锁的客户端还在世,锁就不会逾期。
现在我们的完整方案是:


  • 加锁时设置唯一标识(UUID+线程ID)
  • 启动看门狗定时续期
  • 用Lua脚本安全开释
  • 同时做好监控,及时发现超长耗时任务
分布式锁如何解决锁逾期标题?

第一是合理设置逾期时间。这个要根据业务的实际执行时间来定,好比我们的业务一样寻常是毫秒级的,我们会设置锁的逾期时间为30秒,留出充足的冗余来应对各种异常环境。
第二是使用看门狗机制。
获取锁时,先设置一个相对较短的逾期时间,好比30秒
同时启动一个配景线程(看门狗)
看门狗每隔10秒检查一次,假如发现锁还在使用就主动续期
假如客户端瓦解了,看门狗也就停了,锁自然逾期,这样不会产生死锁
  1. try {
  2.     // 获取锁
  3.     lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
  4.     // 执行业务逻辑
  5. } catch (Exception e) {
  6.     // 异常处理
  7. } finally {
  8.     // 释放锁
  9.     lock.unlock();
  10. }
复制代码
纵然有了这些机制,在Redis主从架构下照旧大概出现标题。因为假如主节点在复制数据到从节点前瓦解了,这时候从节点被提拔为主节点,之前加的锁就丢失了。
以对于一些强一致性要求的场景,我们会考虑:
要么使用Redis Cluster多主节点
要么切换到Zookeeper这样的CP系统
请设计一个可重入的分布式锁

首先说下为什么要可重入。
在实际业务中,我们经常会遇到同一个线程多次获取同一把锁的场景。
好比一个方法获取了锁,它调用的子方法也必要这个锁。假如不支持可重入,就会导致死锁。
实现可重入的核心思路是:


  • 记录锁的持有者信息
  • 记录重入次数
  • 只有持有者才能重入和开释锁
    具体实现上,我们使用Redis的Hash结构:
  • key是锁的名称
  • field是客户端标识(好比UUID+线程ID)
  • value是重入次数
    加锁过程是这样的:
  • 假如锁不存在,创建hash并设置重入次数为1
  • 假如锁存在且是当前客户端的,重入次数加1
  • 假如是其他客户端的锁,获取失败
    解锁时:
  • 先验证是否是当前客户端的锁
  • 将重入次数减1
  • 假如重入次数变成0,删除整个锁
固然,这只是基本实现。在生产环境还必要考虑:


  • 结合看门狗机制处理锁逾期
  • 异常环境的处理
  • 性能优化等
使用Redis实现一个分布式锁,包罗获取锁和开释锁的逻辑

  1. public class RedisDistributedLock {
  2.     private StringRedisTemplate redisTemplate;
  3.     private static final long DEFAULT_EXPIRE = 30; // 默认30秒过期
  4.     private static final long DEFAULT_WAIT = 3;   // 默认等待3秒
  5.    
  6.     // 获取锁
  7.     public boolean tryLock(String key, String value, long timeout) {
  8.         try {
  9.             // SET key value NX EX 30
  10.             Boolean result = redisTemplate.opsForValue()
  11.                 .setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
  12.             return Boolean.TRUE.equals(result);
  13.         } catch (Exception e) {
  14.             // 记录日志
  15.             return false;
  16.         }
  17.     }
  18.    
  19.     // 释放锁
  20.     public boolean releaseLock(String key, String value) {
  21.         // 使用Lua脚本保证原子性
  22.         String script =
  23.             "if redis.call('get',KEYS[1]) == ARGV[1] then " +
  24.                 "return redis.call('del',KEYS[1]) " +
  25.             "else " +
  26.                 "return 0 " +
  27.             "end";
  28.             
  29.         try {
  30.             Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
  31.                 Collections.singletonList(key), value);
  32.             return Long.valueOf(1).equals(result);
  33.         } catch (Exception e) {
  34.             // 记录日志
  35.             return false;
  36.         }
  37.     }
  38.    
  39.     // 实际使用示例
  40.     public void doBusinessWithLock() {
  41.         String key = "order:1";
  42.         String value = UUID.randomUUID().toString();
  43.         
  44.         try {
  45.             if (tryLock(key, value, DEFAULT_EXPIRE)) {
  46.                 // 获取锁成功,执行业务逻辑
  47.                 doBusiness();
  48.             } else {
  49.                 // 获取锁失败的处理
  50.                 throw new RuntimeException("获取锁失败");
  51.             }
  52.         } finally {
  53.             // 释放锁
  54.             releaseLock(key, value);
  55.         }
  56.     }
  57. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

风雨同行

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表