【Redis】6、Redisson 分布式锁的简单使用(可重入、重试机制...)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 【Redis】6、Redisson 分布式锁的简单使用(可重入、重试机制...)


零、自己通过 set nx ex 实现的分布式锁存在的问题

✏️ 不可重入 同一个线程无法多次获取同一把锁

✏️ 不可重试 获取锁只尝试一次就返回 false,没有重试机制

✏️ 超时释放 锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患

一、Redisson 介绍

✏️ Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)

✏️ 它提供了一系列分布式的 Java 常用对象

✏️ 提供了许多分布式服务,其中包含了各种分布式锁的实现

官网地址 https://redisson.org

GitHub 地址 https://github.com/redisson/redisson

二、Redisson 基本使用(改造业务)

(1) 依赖

<dependency>
       <groupId>org.redisson</groupId>
       <artifactId>redisson</artifactId>
       <version>3.13.6</version>
   </dependency>

(2) 配置 Redisson 客户端

@Configuration
@SuppressWarnings("all")
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.88.130:6379")
                .setPassword("root");
        return Redisson.create(config);
    }
}

✏️ 配置 redis 地址也可以使用 config.useClusterServers() 添加集群地址

✏️ useSingleServer() 是添加单点地址

(3) 使用 Redisson 的可重入锁

@Test
 public void tesRedisson() throws InterruptedException {
     // 获取可重入锁, 指定锁的名称
     RLock lock = redissonClient.getLock("anLock");
     // 尝试获取锁
     // 参数1:获取锁的最大等待时间(期间会多次重试获取锁)
     // 参数2:锁自动释放时间
     // 参数3:时间单位
     boolean isGetLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
     if (isGetLock) {
         try {
             System.out.println("执行业务");
         } finally {
             lock.unlock();
         }
     }
 }

三、Redisson 可重入锁原理

✏️ 通过 Redis 的 Hash 数据结构实现

✏️ 存入线程标识和 counter【记录锁被重入的次数(被使用的次数),被使用(获取)一次就递增1,被释放则 counter 减1】

✏️ 当 counter 为 0 的时候,整个锁会被释放

@Resource
    private RedissonClient redissonClient;
    @SuppressWarnings("all")
    @Test
    public void tRedisson() {
        RLock lock = redissonClient.getLock("lock");
        boolean getLock = lock.tryLock();
        if (!getLock) {
            System.out.println("tRedisson getLock Failed");
            return;
        }
        try {
            System.out.println("tRedisson getLock Success");
            m(lock);
        } finally {
            System.out.println("tRedisson unlock");
            lock.unlock();
        }
    }
    public void m(RLock lock) {
        boolean getLock = lock.tryLock();
        if (!getLock) {
            System.out.println("m() getLock Failed");
            return;
        }
        try {
            System.out.println("m() getLock Success");
        } finally {
            System.out.println("m() unlock");
            lock.unlock();
        }
    }


✏️ 没有直接使用 hset

✏️ hincrby 命令:当 key 不存在的时候会自动创建 key

✏️ 获取锁成功:返回 nil;获取锁失败:返回当前锁的存活时间

✏️ pttl毫秒为单位返回某个 key 的存活时间

✏️ redis.call('publish', KEYS[2], ARGV[1]); 发布释放锁的消息通知

四、Redisson 可重试原理

下面是 Redisson 的 tryLock() 方法的源码:

@Override
 public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
     // 把超时等待时间转换为毫秒
     long time = unit.toMillis(waitTime);
     // 获取当前时间(毫秒)
     long current = System.currentTimeMillis();
     // 获取线程 ID
     long threadId = Thread.currentThread().getId();
     // tryAcquire 尝试获取锁(获取锁成功返回 null, 获取锁失败返回剩余ttl)
     Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
     // lock acquired
     if (ttl == null) { // 成功获取锁
         return true;
     }
     
     // time 是超时释放时间(单位:毫秒)
     // time = time - (System.currentTimeMillis() - current)
     time -= System.currentTimeMillis() - current; // 刷新超时释放时间
     if (time <= 0) { // 超时释放时间到了
         acquireFailed(waitTime, unit, threadId);
         return false; // 获取锁失败
     }
     
     // 获取 当前时间毫秒值
     current = System.currentTimeMillis();
     // 等待释放锁的通知(订阅释放锁的信号)
     RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
  // 等到超时释放时间结束还没有收到释放锁的通知的话, 返回 false
     // 获取锁失败 
     if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
         if (!subscribeFuture.cancel(false)) { // 取消订阅
             subscribeFuture.onComplete((res, e) -> {
                 if (e == null) {
                     unsubscribe(subscribeFuture, threadId);
                 }
             });
         }
     
         acquireFailed(waitTime, unit, threadId);
         return false;
     }
     
     // 接收到释放锁的信号
     try {
      // 判断释放到超时时间
         time -= System.currentTimeMillis() - current;
         if (time <= 0) {
             acquireFailed(waitTime, unit, threadId);
             return false; // 获取锁失败
         }
     
         while (true) { // 反复尝试
             long currentTime = System.currentTimeMillis();
             ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
             // lock acquired
             if (ttl == null) { // 获取锁成功
                 return true;
             }
             time -= System.currentTimeMillis() - currentTime;
             if (time <= 0) {
                 acquireFailed(waitTime, unit, threadId);
                 return false;
             }
             // waiting for message
             // 代码来到这里:没有获取到锁, 超时时间有剩余
             // 等待释放锁的信号
             currentTime = System.currentTimeMillis();
             if (ttl >= 0 && ttl < time) {
                 subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
             } else {
                 subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
             }
             time -= System.currentTimeMillis() - currentTime;
             if (time <= 0) {
                 acquireFailed(waitTime, unit, threadId);
                 return false;
             }
         }
     } finally {
         unsubscribe(subscribeFuture, threadId);
     }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
 }
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
      // 判断超时释放时间是否是 -1
      if (leaseTime != -1) {
          return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
      }
      // 代码来到这里表示超时释放时间是 -1
      // tryLockInnerAsync 异步的方法
      RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                  commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                  TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
      ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
          if (e != null) {
              return;
          }
          // lock acquired
          if (ttlRemaining) {
              scheduleExpirationRenewal(threadId);
          }
      });
      return ttlRemainingFuture;
  }

tryLockInnerAsync()、tryAcquire(): ① 尝试获取锁。获取锁成功,返回 nil;获取锁失败,返回超时是否时间(lessTime)

② 该方法是异步的

① 获取锁失败后,不会立刻重新尝试获取锁

② 会等待释放锁的通知

五、Redisson 超时释放(锁的 ttl)

好😵懵逼啊,随便看看得了

📝 可重入:利用hash结构记录线程id和重入次数

📝 可重试:利用信号量和 PubSub 功能实现等待、唤醒,获取锁失败的重试机制

📝 超时续约:利用 watchDog,每隔一段时间(releaseTime / 3),重置超时时间

六、主从一致(连锁 MultiLock)

✏️主节点支持写请求

✏️从节点支持读请求

✏️主节点写入的数据要同步到从节点

✏️假如向主节点写入了数据,主节点还没有来得及向从节点同步数据自己就宕机了,此时 Redis 的哨兵机制会从剩下的从节点中选一个从节点作为新的主节点

✏️ 该新的主节点是没有上个主节点中的 lock = thread1 数据的

✏️ 锁失效,可能存在线程安全问题


✏️其中有任何一个节点宕机都无法拿到锁

七、锁总结

🎄不可重入 Redis 分布式锁:

原理: 利用 setnx 的互斥性;利用 ex 避免死锁;释放锁时判断线程标

缺陷: 不可重入、无法重试、锁超时失效

🎄可重入的 Redis 分布式锁:

原理: 利用 Hash 结构,记录线程标识和重入次数;利用 watchDog 延续锁过期时间;利用信号量控制锁重试等待

缺陷: redis宕机引起锁失效问题

🎄 Redisson 的 multiLock:

原理: 多个独立的 Redis 节点,必须在所有节点都获取重入锁,才算获取锁成功

缺陷: 运维成本高、实现复杂

相关文章
|
3月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
317 2
|
3月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
253 6
|
4月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
2月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
206 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
2月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
6月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
1月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
|
2月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
189 1
Redis专题-实战篇二-商户查询缓存
|
1月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。
|
6月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
951 0

热门文章

最新文章

下一篇
oss云网关配置