背景:

在实际项目中,经常会遇到需要延迟任务的业务场景。比如网上购物下单后,需要跳转到其它支付平台支付,同时需要创建一个延迟任务,这个延迟的时间一般时等待支付的时间。如果在这段时间,用户没有支付,那么这个延迟任务就将订单取消掉。如果支付了,就走后面的流程。
延时任务实现方案有很多。在这里只讨论基于Redis的实现。基于Redis 实现延时任务的功能无非就下面两种方案:

1.Redis 过期事件监听
2.Redisson 内置的延时队列

Redis过期事件监听有着时效性差、丢消息缺点,一般在实践中不适用。

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。我们可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。Redisson 定期使用 zrangebyscore 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被消费者监听到。这样做可以避免消费者对整个 SortedSet 进行轮询,提高了执行效率。相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势:

1.减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
2.消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题

跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。

Redisson中RDelayedQueue相关用法

RedissonClient获取RDelayedQueue的方法

   public <V> RDelayedQueue<V> getDelayedQueue(RQueue<V> destinationQueue) {
        if (destinationQueue == null) {
            throw new NullPointerException();
        }
        return new RedissonDelayedQueue<V>(queueTransferService, destinationQueue.getCodec(), commandExecutor, destinationQueue.getName());
    }

RDelayedQueue是一种延迟队列接口,Redisson自带一个叫‘RedissonDelayedQueue’的实现类是他的子类,用于实现延迟任务。注意根据上面源码可以看到用Redisson获取RDelayedQueue时需要传一个实现RQueue队列的参数,而Redisson又可以创建另一个队列叫‘RedissonBlockingDeque’,它就是实现RBlockingDeque接口的阻塞队列,而RBlockingDeque又是RQueue的子类。

获取:RBlockingDeque

    public <V> RBlockingDeque<V> getBlockingDeque(String name) {
        return new RedissonBlockingDeque<V>(commandExecutor, name, this);
    }

实践Demo

任务需求:新增一个对象User,并让他5s以后介绍自己(toString方法打印信息)。

pom依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.code</groupId>
    <artifactId>MyRDelayedQueue</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <dependencies>
        <!-- starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.5.4</version>
        </dependency>
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.5.4</version>
        </dependency>
        <!-- Redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

application.yml文件:

#Redis配置
# Redis数据库索引(默认为0)
# Redis服务器地址
# Redis服务器连接端口
# Redis服务器连接密码(默认为空)
# 链接超时时间 单位 ms(毫秒)
# 连接池最大连接数(使用负值表示没有限制) 默认 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
# 连接池中的最大空闲连接 默认 8
# 连接池中的最小空闲连接 默认 0
spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    connect-timeout: 3000
    password:
    lettuce:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
 
server:
  port: 8080

启动类:

@SpringBootApplication
public class MyRDQueueApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyRDQueueApplication.class, args);
    }
}

Redisson配置文件:

@Configuration
public class RedissonConfig {
 
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.database}")
    private int database;
 
    @Bean
    public RedissonClient getRedisson() {
        Config config = new Config();
        String address = "redis://" + host + ":" + port;
        config.useSingleServer().setAddress(address).setDatabase(database);
        return Redisson.create(config);
    }
}

User:

public class User {
    private String userId;
    private String userName;
    private String password;
 
    public User() {
    }
 
    public String getUserId() {
        return userId;
    }
 
    public void setUserId(String userId) {
        this.userId = userId;
    }
 
    public String getUserName() {
        return userName;
    }
 
    public void setUserName(String userName) {
        this.userName = userName;
    }
 
    public String getPassword() {
        return password;
    }
 
    public void setPassword(String password) {
        this.password = password;
    }
 
    //介绍自己
    public String Speak() {
        return "我的信息是:{" +
                "userId='" + userId + '\'' +
                ", userName='" + userName + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

controller:

@RestController
public class IndexController {
 
    @Autowired
    public QueueService queueService;
 
    @RequestMapping("/queue")
    public String addUser(){
        return queueService.addUser();
    }
}

service:
让RedissonDelayedQueue的‘offer’方法添加延迟任务对象,然后通过RedissonBlockingDeque的‘poll’或者‘take’方法获取延迟任务对象然后进行后续操作。

@Service
public class QueueService {
 
    @Autowired
    public RedissonClient redissonClient;
 
    public String addUser() {
        //新增User
        User user = new User();
        user.setUserId("123456");
        user.setUserName("queueTask");
        user.setPassword("666666");
        //5秒后让新增的User讲话
        RBlockingDeque<User> blockingDeque = redissonClient.getBlockingDeque("speak");
        RDelayedQueue<User> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        delayedQueue.offer(user, 5, TimeUnit.SECONDS);
        //指定日期格式
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        //打印添加任务时间
        System.out.println("添加时间:" + formatter.format(LocalDateTime.now()));
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    User task = null;
                    try {
                        task = blockingDeque.take();
                        System.out.println(task.Speak());
                        //打印执行任务时间
                        System.out.println("执行时间:" + formatter.format(LocalDateTime.now()));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        return user.getUserId();
    }
}

结果测试:
启动项目后请求路径:http://localhost:8080/queue

后台打印:

添加时间:2024-06-05 13:42:07
我的信息是:{userId='123456', userName='queueTask', password='666666'}
执行时间:2024-06-05 13:42:12

结果:执行任务时间比添加时间推迟5秒。

RBlockingQueue的poll()和take()区别

队列take()和poll()的区别:
take():返回队列的头元素,并把它从队列中删除,如果队列为空时则阻塞线程直到有新的元素添加进来并返回;
poll():返回队列的头元素,并把它从队列中删除,如果队列头元素为空则返回null但是不阻塞线程;


云开月明
4 声望1 粉丝