Bootstrap

Redisson学习教程(B站诸葛)

在这里插入图片描述


  • 弱智级别
package org.example.controller;

public class IndexController {
    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_storck")
    public String deductStock() {
        String lockKey = "product:1001";


        //再redis里排队的只有第一个请求能执行成功
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "zhuge");
        //相当于jedis.setnx(k,v)  如果这个值不存在,我就设置这个key
        //result代表执行成功,原来没有这个key,如果这个值是false 代表这个key存在
        if (!result) {
            return "error";
        }
        if (stock > 0) {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功 ");
        } else {
            System.out.println("扣减失败,库存不足 ");
        }
        stringRedisTemplate.delete("lockKey");
    }
}

在这里插入图片描述
如果扣减库存的失效,那么就出问题了,下面就无法执行,锁无法释放,导致报错,这个商品id就一直被锁着 了

  stringRedisTemplate.delete("lockKey");

因为服务器扣减库存抛异常,改进一版

package org.example.controller;

public class IndexController {
    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_storck")
    public String deductStock() {
        String lockKey = "product:1001";


        //再redis里排队的只有第一个请求能执行成功
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "zhuge");
        //相当于jedis.setnx(k,v)  如果这个值不存在,我就设置这个key
        //result代表执行成功,原来没有这个key,如果这个值是false 代表这个key存在
        if (!result) {
            return "error";
        }
        
        try{     
		    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功 ");
            } else {
                System.out.println("扣减失败,库存不足 ");
            }
        }finally {
            stringRedisTemplate.delete("lockKey");
        }
    }
}

在关键的代码块加了

try{} finally {}

  • 改进二版:
  • 如果这个服务器宕机了,在扣减库存的时候,这个时候你应该怎么办?????finally 无法执行,是不是又导致死锁了。
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "zhuge");
        stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
        if (!result) {
            return "error";
        }

解决方案: 加过期的时间


第三版:如果枷锁之后,系统宕机, 还是导致死锁,所以要保证原子性:

//        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "zhuge");
//        stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "zhuge", 10, TimeUnit.SECONDS);

原子性:保证了同时执行,一行代码,注意方法的参数;

  • 高并发下出现的问题:假如执行这个请求执行完成1500ms , 过期时间1000ms, 也就是说第一个请求还没执行完成,就已经过期了,然而此时第二个请求过来了,加入执行时间800ms 扣减库存占用500ms, 第二个请求本来是加锁了,但是第一个因为提前过期而没有手动释放锁,导致你现在释放了第二个请求过来的锁。这样加的锁就无效了。然后导致超卖问题,就是你库存100卖了1000的货。

问题的根本原因:我加的锁被别人删除了, 怎么解决???

package org.example.controller;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class IndexController {
    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_storck")
    public String deductStock() {
        String lockKey = "product:1001";
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", clientId, 10, TimeUnit.SECONDS);

        if (!result) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功 ");
            } else {
                System.out.println("扣减失败,库存不足 ");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get("lockKey"))) {
                stringRedisTemplate.delete("lockKey");
            }
        }
    }
}

在这里插入图片描述
他在加锁哪里,对锁进行了一个id的匹配


关于锁续命:
天空一声雷响,主角登场,文章属于纸上谈兵,目前没有实战

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

1、单机模式useSingleServer,可以变

@Configuration
public class RedissonConfig {
 
    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

也可以

package org.example;

import jdk.nashorn.internal.runtime.regexp.joni.Config;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
        System.out.println("程序启动");
    }

    @Bean
    public RedissonClient redissonClient() {
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
                .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

改进版

    public String deductStock() {
//        String lockKey = "product:1001";
//        String clientId = UUID.randomUUID().toString();
//        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", clientId, 10, TimeUnit.SECONDS);
//
//        if (!result) {
//            return "error";
//        }
        RLock redissonLock = redisson.getLock("lockKey");
        redissonLock.lock();

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功 ");
            } else {
                System.out.println("扣减失败,库存不足 ");
            }
        } finally {
            redissonLock.unlock();
/*            if (clientId.equals(stringRedisTemplate.opsForValue().get("lockKey"))) {
                stringRedisTemplate.delete("lockKey");
            }*/
        }
    }

视频最后的效果

package com.example.demo.controller;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;

public class IndexController {
    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_storck")
    public String deductStock() {
        RLock redissonLock = redisson.getLock("lockKey");
        redissonLock.lock();
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功 ");
            } else {
                System.out.println("扣减失败,库存不足 ");
            }
        } finally {
            redissonLock.unlock();
        }
        
        return "deductStock";
    }
}


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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

分别对应依赖

    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
  • 为什么要用分布式锁,用一般的锁不行吗,分布式锁的应用场景是什么?使用 分布式锁 的主要原因是要在多个不同的 机器进程 之间进行资源的共享和同步。在分布式系统中,应用可能在多个节点上运行,而普通的 本地锁(如 Java 中的 synchronizedReentrantLock)只能在同一个 JVM(Java Virtual Machine)进程内有效,无法跨进程、跨机器共享和同步。

1. 为什么普通锁不行

普通锁(如 synchronizedReentrantLock)是 局部锁,只能在同一个 进程线程 内部工作。当应用部署在多个机器或容器上时,普通锁就无法保证跨节点的同步与协调。比如:

  • 如果多个服务实例或容器在不同的机器上运行,它们之间无法共享本地的锁。
  • 如果不同服务实例同时访问某个共享资源(如数据库、缓存、文件等),就可能导致资源竞争,产生 并发冲突数据不一致

这时就需要 分布式锁 来保证多个分布式系统节点在操作共享资源时,能够遵循某种约定进行同步,避免数据竞争。

2. 分布式锁的应用场景

分布式锁主要用于以下几个典型的场景:

1. 多个服务实例竞争共享资源

在微服务架构或分布式系统中,通常会有多个服务实例运行在不同的机器上。当这些服务需要对一个共享资源(如数据库中的某个数据、缓存、文件等)进行操作时,需要避免同时进行修改,防止数据不一致或者产生竞态条件。这时分布式锁就可以确保在同一时刻只有一个服务实例在访问这个共享资源。

场景示例

  • 订单系统:多个微服务实例可能同时收到用户支付请求,如果没有分布式锁,可能会导致重复创建订单或者扣款。
  • 限流:多个服务实例需要共同控制某个资源的访问量,比如访问次数、流量限制等,可以通过分布式锁确保同一时刻只有一个服务实例在执行限流操作。
2. 防止重复任务的执行

在分布式系统中,如果没有协调机制,可能会出现多个节点同时执行同一个任务的情况。通过分布式锁,可以确保在同一时刻只有一个节点能够执行某个任务,避免任务重复执行。

场景示例

  • 定时任务:比如数据清理任务、缓存刷新任务等,如果有多个节点同时执行相同的定时任务,可能会导致资源浪费或者数据重复处理。使用分布式锁可以确保任务只在一个节点上执行。
  • 唯一性任务:比如生成唯一的 ID 或令牌时,通过分布式锁可以保证 ID 生成过程的唯一性,防止多个节点生成相同的 ID。
3. 协调跨节点的操作

在一些需要跨节点协同的场景中,多个节点可能需要顺序地进行某个操作。分布式锁可以确保这些操作按照特定的顺序执行,而不会发生并发冲突。

场景示例

  • 分布式事务:在分布式数据库或多服务之间的事务处理中,如果不同的节点需要修改同一份资源,分布式锁可以帮助确保事务的一致性和顺序性。
  • 唯一任务处理:比如某个任务只应该被一个节点处理,其他节点需要等待。
4. 资源的有限访问控制

在某些情况下,某些资源(如 API 请求、数据库连接池等)是有限的,多个节点可能会争抢访问这些资源。使用分布式锁可以控制资源的访问顺序,确保资源不会被过度占用。

场景示例

  • 限制高频访问:如果你有一个高频率的 API 或数据库接口,分布式锁可以确保不会有多个服务实例同时发送请求。
  • 限制并发请求:例如,某些操作需要保证最大并发数,分布式锁可以用于控制并发请求的数量,确保系统不会过载。

3. 分布式锁的实现方式

分布式锁的实现方式有多种,常见的包括:

  • Redis:通过 Redis 提供的 SETNX 命令或者 Redis 的 RedLock 算法实现分布式锁。
  • ZooKeeper:利用 ZooKeeper 的临时节点来实现分布式锁,ZooKeeper 本身就是为了协调分布式系统而设计的。
  • Etcd:类似于 ZooKeeper,Etcd 也可以用来实现分布式锁,通常用于 Kubernetes 等分布式系统中。
  • 数据库:通过数据库的行锁或其他机制实现,但效率相对较低,适合不频繁需要获取锁的场景。

4. 分布式锁的挑战

分布式锁虽然可以解决跨节点并发问题,但在实现和使用时也有一些挑战:

  • 死锁:分布式系统中的锁可能因网络延迟或节点故障等原因导致死锁,需要特别小心管理锁的获取和释放。
  • 性能问题:频繁获取和释放分布式锁会增加系统的延迟,可能成为瓶颈。
  • 过期时间管理:如果分布式锁没有合理的过期时间控制,可能会导致锁无法及时释放,影响系统的可用性。
  • 容错性:分布式锁需要具备容错机制,防止由于单点故障或网络问题导致锁失效。

总结

  • 分布式锁 是解决跨进程、跨机器资源同步与竞争的一个重要工具,适用于分布式系统、微服务架构中需要对共享资源进行安全访问的场景。
  • 普通的 本地锁 只能在同一进程内使用,无法满足跨机器同步的需求,而分布式锁则能有效地协调多个节点之间的资源访问。
  • 常见的应用场景包括 共享资源竞争、任务去重、跨节点协同工作、资源访问控制等,但也需要注意合理管理锁的获取和释放,避免性能问题和死锁。

是不是最好用分布式锁,尽量避免使用Synchronized,Reentrantlock?????????????
并不是所有场景下都需要使用 分布式锁,也不能说一定要避免使用 synchronizedReentrantLock。实际上,这两者(synchronizedReentrantLock)和 分布式锁 各自有其适用的场景,使用时要根据具体的需求和系统架构来选择。

1. synchronizedReentrantLock 的适用场景

1.1 本地锁的优势
  • 简单高效synchronizedReentrantLock 是 Java 内建的本地锁机制,它们的实现非常简单,开销低,并且效率高。它们适用于 单机应用,即只有一个 JVM 实例运行的情况。这种情况下,不需要复杂的分布式协调,使用本地锁完全可以满足并发控制的需求。

  • 易于使用

    • synchronized 是 Java 的语言级别的关键字,使用起来简洁直接,不需要额外的依赖。
    • ReentrantLock 提供了比 synchronized 更加灵活的功能,比如手动加锁、解锁、可重入、可中断锁等特性。
1.2 使用场景:
  • 多线程环境下的同步:当多个线程需要对同一个对象的资源进行并发操作时(如多个线程修改同一内存中的数据结构),synchronizedReentrantLock 是常用的本地锁,可以确保数据的一致性和完整性。
  • 在单台机器上运行:如果你的应用只部署在单台服务器上,且所有线程都运行在同一 JVM 进程内,那么本地锁是最合适的选择。

2. 分布式锁的必要性

分布式锁解决的主要问题是 跨进程、跨服务器、跨机器的同步问题。当你的应用是分布式的,或者在多个节点上运行时,就需要引入 分布式锁 来确保资源的共享和同步。

2.1 分布式锁的应用场景:
  • 多实例共享资源:当应用分布在多台服务器上,多个实例之间可能同时需要访问或修改某个共享的资源(如数据库记录、缓存、文件等),此时需要使用分布式锁来避免资源竞争和数据不一致。
  • 跨进程协调:分布式锁通常被用来解决不同进程或服务实例之间的资源竞争问题。
  • 确保唯一性和顺序性:分布式锁可以保证某些任务或操作只在一个节点上执行,避免重复执行任务或保证某些操作的顺序性(如定时任务、消息消费、数据库记录生成等)。
2.2 典型的分布式锁技术:
  • Redis:Redis 提供了强大的分布式锁功能,如基于 SETNXRedLock 算法等实现的锁,适合需要高性能和高可用性的分布式环境。
  • ZooKeeper:通过 ZooKeeper 的临时节点来实现分布式锁,适合需要强一致性和可靠性的分布式系统。
  • Etcd:类似 ZooKeeper,提供了强一致性保障,可以用来实现分布式锁。

3. 何时使用本地锁?

  • 应用只在单一 JVM 运行时:如果你的应用只运行在一台机器上,即使是多线程环境,也不需要考虑跨进程或跨机器的同步问题,那么使用本地锁(synchronizedReentrantLock)会更加简单和高效。
  • 性能要求较高:本地锁没有分布式锁的网络通信开销,通常会比分布式锁更高效。如果在单台机器上的并发控制足够满足需求,使用本地锁可以避免额外的分布式协调。

4. 何时使用分布式锁?

  • 应用部署在多台机器/多个服务实例中:当你的应用是分布式的,可能有多个服务实例或多个 JVM 节点在运行时,需要使用分布式锁来确保跨节点的资源访问同步。
  • 需要协调多个进程/服务实例之间的操作:如多个服务实例同时访问一个共享数据库表,或者多个应用实例处理相同的任务。
  • 避免重复执行任务:比如在分布式系统中有一些定时任务(例如,定时清理数据、批量任务等),分布式锁可以确保这些任务不会被多个服务实例同时执行。

5. 本地锁和分布式锁的权衡

本地锁(synchronized / ReentrantLock):
  • 优点
    • 使用简单。
    • 无网络延迟,性能高效。
    • 避免了分布式系统中的复杂性。
  • 缺点
    • 只能在同一 JVM 中有效,无法跨多个进程或机器同步。
分布式锁:
  • 优点
    • 可以在多个进程或服务实例间同步共享资源,保证数据一致性。
    • 适用于分布式环境中需要跨进程或跨服务进行同步的场景。
  • 缺点
    • 相对复杂,需要维护分布式系统的可靠性和一致性。
    • 存在性能开销(如网络延迟、锁争用等问题)。
    • 容易受到网络故障和服务宕机的影响。

6. 结论:两者的选择

  • 单机应用或单一 JVM 环境:如果你只在一个 JVM 进程中运行应用,synchronizedReentrantLock 是最好的选择,简洁高效,性能高。
  • 分布式应用或多节点环境:如果你的应用部署在多台机器、多个服务实例上,必须保证跨进程、跨机器的同步操作,那么就需要使用 分布式锁

总之,不同场景下使用不同的锁机制,不是越复杂的锁机制越好,而是要根据系统架构和业务需求来选择合适的同步方案。在 分布式系统 中, 分布式锁 是必不可少的,而在单一 JVM 中, 本地锁 是更为合适和高效的选择。

以下实现原理可以不看,B站连接 分布式锁教程



源码分析

   <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return this.evalWriteSyncedAsync(this.getRawName(), LongCodec.INSTANCE, command,
         "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", 
         Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
    }

这个方法 tryLockInnerAsync 是用来异步地尝试获取分布式锁的。它的作用是在 Redis 中执行一个 Lua 脚本来尝试获取一个锁,并返回一个 RFuture<T> 对象,代表一个异步操作的结果。具体来说,方法执行了一个 Redis 脚本来检查锁的状态,并设置过期时间和线程标识符。

方法解析

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command)
  • waitTime: 等待锁的最大时间(即最大尝试获取锁的时间)。
  • leaseTime: 锁的持有时间(即锁的过期时间)。
  • unit: 时间单位(如秒、毫秒等)。
  • threadId: 当前线程的标识符,用于标识锁的持有者。
  • command: 执行 Redis 命令的对象。

代码解析

return this.evalWriteSyncedAsync(
    this.getRawName(), 
    LongCodec.INSTANCE, 
    command, 
    "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
    "redis.call('pexpire', KEYS[1], ARGV[1]); return nil; " +
    "end; return redis.call('pttl', KEYS[1]);", 
    Collections.singletonList(this.getRawName()), 
    new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}
);

关键部分分析:

  1. evalWriteSyncedAsync:

    • evalWriteSyncedAsync 是一个异步执行 Redis Lua 脚本的方法。它会执行一个 Lua 脚本来进行 Redis 数据的操作。
    • this.getRawName() 获取 Redis 锁的键名。
    • LongCodec.INSTANCE 是用于处理返回结果的编解码器,表示返回类型是一个 Long 类型。
    • command 是 Redis 命令,用于进一步操作。
  2. Lua 脚本

    if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return nil;
    end;
    return redis.call('pttl', KEYS[1]);
    

    这段 Lua 脚本执行以下操作:

    • 检查锁是否存在
      • redis.call('exists', KEYS[1]) == 0: 检查锁是否存在(通过 Redis 键检查)。
      • redis.call('hexists', KEYS[1], ARGV[2]) == 1: 检查是否已经存在一个由 threadId 标识的锁。
    • 如果锁不存在或线程ID已经持有锁
      • redis.call('hincrby', KEYS[1], ARGV[2], 1): 增加该锁的计数,表示线程成功获得了锁。
      • redis.call('pexpire', KEYS[1], ARGV[1]): 设置锁的过期时间,ARGV[1] 是通过 unit.toMillis(leaseTime) 计算出的过期时间,单位是毫秒。
      • return nil: 如果获取锁成功,返回 nil
    • 如果锁存在且未获得,返回当前锁的剩余存活时间:
      • return redis.call('pttl', KEYS[1]): 返回当前锁的剩余过期时间,单位是毫秒。
  3. Collections.singletonList(this.getRawName()):

    • 传递锁的键名,这里 this.getRawName() 是锁的 Redis 键。
  4. new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}:

    • ARGV[1] 是锁的过期时间(以毫秒为单位)。
    • ARGV[2] 是线程的标识符,用来区分不同线程的锁。

总结

这个方法的目的是在 Redis 中异步地尝试获取一个分布式锁。它通过执行一个 Lua 脚本来检查锁是否已经存在,如果锁不存在或者当前线程已经持有锁,它会将锁的计数加 1,并设置过期时间。如果锁已经被其他线程持有,脚本将返回锁的剩余存活时间。

RFuture<T> 是一种异步结果的返回类型,表示锁的获取过程会异步执行,调用者可以通过它来获取操作结果。

希望这解释能够帮助你更好地理解代码的功能和实现方式!

    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture acquiredFuture;
        if (leaseTime > 0L) {
            acquiredFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        } else {
            acquiredFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }

        CompletionStage<Boolean> acquiredFuture = this.handleNoSync(threadId, acquiredFuture);
        CompletionStage<Boolean> f = acquiredFuture.thenApply((acquired) -> {
            if (acquired) {
                if (leaseTime > 0L) {
                    this.internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    this.scheduleExpirationRenewal(threadId);
                }
            }

            return acquired;
        });
        return new CompletableFutureWrapper(f);
    }

解释
这个方法 tryAcquireOnceAsync 是用来异步地尝试获取分布式锁的,并返回一个 RFuture<Boolean>,表示是否成功获取到锁。这个方法的核心功能是调用 tryLockInnerAsync 来请求锁,并根据是否成功获取锁来做进一步的处理。

代码解析

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture acquiredFuture;
    if (leaseTime > 0L) {
        acquiredFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    } else {
        acquiredFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }

    CompletionStage<Boolean> acquiredFuture = this.handleNoSync(threadId, acquiredFuture);
    CompletionStage<Boolean> f = acquiredFuture.thenApply((acquired) -> {
        if (acquired) {
            if (leaseTime > 0L) {
                this.internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                this.scheduleExpirationRenewal(threadId);
            }
        }

        return acquired;
    });
    return new CompletableFutureWrapper(f);
}

关键部分分析:

1. tryLockInnerAsync 调用
if (leaseTime > 0L) {
    acquiredFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
    acquiredFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
  • tryLockInnerAsync: 这个方法是执行 Redis Lua 脚本来尝试获取锁的异步方法。这里根据 leaseTime 的值选择不同的超时设置:
    • 如果 leaseTime > 0L,则使用提供的 leaseTimeunit 来设置锁的持有时间。
    • 如果 leaseTime <= 0L,则使用 internalLockLeaseTime(可能是类的一个字段,代表默认的锁持有时间)。
2. handleNoSync 处理
CompletionStage<Boolean> acquiredFuture = this.handleNoSync(threadId, acquiredFuture);
  • handleNoSync 方法通常用来处理异步操作的后续逻辑,确保它不会同步阻塞线程。
  • 它可能是为了解决某些同步问题,比如锁在获取时可能需要做一些额外的处理。
3. thenApply 对锁获取结果的处理
CompletionStage<Boolean> f = acquiredFuture.thenApply((acquired) -> {
    if (acquired) {
        if (leaseTime > 0L) {
            this.internalLockLeaseTime = unit.toMillis(leaseTime);
        } else {
            this.scheduleExpirationRenewal(threadId);
        }
    }

    return acquired;
});
  • thenApply: 对 acquiredFuture 结果进行进一步处理。acquired 表示是否成功获取到锁。
    • 如果获取到锁:
      • 如果传入了正的 leaseTime,更新类中的 internalLockLeaseTime 字段。
      • 如果没有传入 leaseTime(即 leaseTime <= 0L),则调用 scheduleExpirationRenewal(threadId) 来安排续期操作,这意味着锁会在过期之前自动续期,保持锁的有效性。
4. 返回 CompletableFutureWrapper
return new CompletableFutureWrapper(f);
  • 最后,CompletableFutureWrapper 是对 CompletionStage<Boolean> 的包装,返回一个 RFuture<Boolean>。这确保了调用者能够异步地获取锁是否成功的结果。

总结:

  1. 获取锁的逻辑

    • 通过调用 tryLockInnerAsync 异步地尝试获取分布式锁。如果传入了 leaseTime,则使用提供的时间;否则,使用默认的锁持有时间(internalLockLeaseTime)。
  2. 异步处理结果

    • handleNoSync 处理异步结果,确保后续的操作不阻塞当前线程。
    • 使用 thenApply 处理锁获取成功后的逻辑,例如更新锁持有时间或安排锁续期。
  3. 返回结果

    • 最终通过 CompletableFutureWrapper 包装一个 CompletionStage<Boolean> 返回给调用者。

这个方法的设计是为了支持异步地获取分布式锁,并在获取锁后进行一些额外的处理,如设置锁的过期时间或安排续期。

可能的改进或问题:

  1. 变量重名问题
    在代码中,有一行声明:

    CompletionStage<Boolean> acquiredFuture = this.handleNoSync(threadId, acquiredFuture);
    

    这里的 acquiredFuture 是重复定义的,实际上应该避免在同一作用域中重名。你可以修改成:

    CompletionStage<Boolean> future = this.handleNoSync(threadId, acquiredFuture);
    
  2. 异常处理
    异常处理在这个方法中没有体现,如果在异步操作过程中发生错误,应该通过 .exceptionally 或其他手段进行捕获和处理,防止异步操作失败时没有任何反馈。

希望这些解释有助于你理解 tryAcquireOnceAsync 方法的实现!如果有更多问题,欢迎继续讨论。

在这里插入图片描述

;