百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

基于Redis实现简单的延时消息队列

mhr18 2024-11-27 12:09 21 浏览 0 评论


说到消息队列相信作为开发人员的大家都不陌生,在实际的工作中我们可能在很多场景下都会用到消息队列,消息队列不仅仅是用于收发消息,而且也可以用于解耦我们的应用系统设计,在大型的应用系统或者分布式应用系统中,我们必然会用到消息队列。

总结下,消息队列的应用场景一般有以下几种场景:

  1. 异步处理任务;
  2. 应用系统解耦;
  3. 大流量削峰;
  4. 日志处理系统;
  5. 消息通讯;


目前主流的消息队列框架有:

  • Apache的ActiveMQ;
  • Erlang语言实现的RabbitMQ;
  • Apache的RocketMQ;
  • Apache的Kafka;

这几种主流的消息队列框架各有各的优势,也有略微的不同。



当然,消息队列也有一些特殊的使用场景,比如:一些电商系统中,当用户下单后,需要在规定的时间内对订单发起支付,如果在规定的时间内没有支付订单,那么该订单将自动取消。这个问题的常规解决方案有两个:

  1. 使用定时任务定时去扫描表,修改过期订单的状态。这种方案当然存在许多局限性,当订单数量不多,而且对系统性能要求不高的情况下可以考虑使用。这种方案也不够优雅;
  2. 使用基于消息队列的延时消息队列,这种方案可以对整个消息队列设置一个消息过期时间,也可以给每一个消息设置一个过期时间,这个时候消息的过期时间取决于设置的最小时间;

延时消息队列我们可以采用上面所说的消息队列框架去实现,也可以采用比较简单的基于Redis的方式去实现,众所周知Redis并不是一个消息队列框架,但是Redis在某些应用场景下可以采用其高级特性为我们提供消息队列的特性。


Rdis在常规的应用场景下,我们使用它作为高速缓存框架,搜索百度百科我们可以发现,对Redis的定义如下:

Redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis的最基本用法是我们使用它的Key-Value特性,存储我们的热点数据或者高频访问数据,来达到提高整个应用系统的吞吐量。

就像上面对Redis的定义,zset是对有序集合的操作,我们可以利用这一特性来实现我们的延时消息队列功能。大致实现的原理如下:

Zset本质就是Set结构上加了个排序的功能,除了添加数据value之外,还提供另一属性score,这一属性在添加修改元素时候可以指定,每次指定后,Zset会自动重新按新的值调整顺序。可以理解为有两列字段的数据表,一列存value,一列存顺序编号。操作中key理解为zset的名字,那么对延时队列又有何用呢?试想如果score代表的是想要执行时间的时间戳,在某个时间将它插入Zset集合中,它便会按照时间戳大小进行排序,也就是对执行时间前后进行排序,这样的话,起一个死循环线程不断地进行取第一个key值,如果当前时间戳大于等于该key值的socre就将它取出来进行消费删除,就可以达到延时执行的目的, 注意不需要遍历整个Zset集合,以免造成性能浪费。



有了实现原理,下面我们通过一个简单的例子来演示下具体的操作过程,可能对基于Redis实现延时消息队列这一主题有更好的理解,演示的源代码链接在文章末尾附上。

新建工程:delay-message-queue-redis

pom.xml

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.delay.message.queue</groupId>
        <artifactId>delay-message-queue</artifactId>
        <version>0.0.1</version>
    </parent>
    <groupId>com.delay.message.queue</groupId>
    <artifactId>delay-message-queue-redis</artifactId>
    <version>0.0.1</version>
    <name>delay-message-queue-redis</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

操作Redis我们使用RedisTemplate,简单的配置如下:

@Configuration
public class RedisConfig {
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration("10.0.0.50", 6379));
    }

    @Bean
    public RedisTemplate redisTemplate() {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

定义我们的消息生产者:

@Slf4j
@Service
public class ProducerService {
    private static final String QUEUE_NAME = "delay_order_queue";
    @Autowired
    private RedisTemplate redisTemplate;

    public void produce(String orderId, long expiredTime) {
        redisTemplate.opsForZSet().add(QUEUE_NAME, orderId, expiredTime);
        //log.info("order id:{} set success", orderId);
        long length = redisTemplate.opsForZSet().size(QUEUE_NAME);
        //log.info("[produce]{} length:{}", QUEUE_NAME, length);
    }
}

消息生产者主要完成的功能:以订单的过期时间为元素(value)的得分,将数据添加到队列中去;

定义我们的消息消费者:

@Slf4j
@Service
public class ConsumerService {
    private static final String QUEUE_NAME = "delay_order_queue";
    @Autowired
    private RedisTemplate redisTemplate;

    public void consume() {
        while (true) {
            Set<String> set = redisTemplate.opsForZSet().rangeByScore(QUEUE_NAME, 0, System.currentTimeMillis(), 0, 1);
            if (set == null || set.isEmpty()) {
                try {
                    //log.info("no data will sleep");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.error("InterruptedException", e);
                }
                continue;
            }
            String orderId = set.iterator().next();
            if (redisTemplate.opsForZSet().remove(QUEUE_NAME, orderId) > 0) {
                log.info("order id:{} handle success", orderId);
                long length = redisTemplate.opsForZSet().size(QUEUE_NAME);
                log.info("[consume]{} length:{}", QUEUE_NAME, length);
            }
        }
    }
}

消息消费者的主要功能如下:

  • 循环从Redis的Zset中拿取一个0<=score<=当前时间的元素(value);
  • 如果没有取到值,则整个线程休眠一秒钟;
  • 如果取到值,则从该队列(key)删除取到的元素(value),删除的目的是防止一个数据被重复的消费;然后对取到的值进行后续的数据处理,这里是将数据打印出来。

整个生产者和消费者我们已经实现了,下面我们通过Junit来生产消息和消费消息;

生产消息的过程:

@Slf4j
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class ProducerServiceTest {
    @Autowired
    private ProducerService producerService;

    @Test
    public void produce() {
        Random random = new Random(1);
        for (int i = 0; i < 10; i++) {
            Calendar calendar = Calendar.getInstance();
            // 产生一个随机数
            int time = random.nextInt(100);
            calendar.add(Calendar.SECOND, time);
            String orderId = "order-id-" + i;
            long expired = calendar.getTimeInMillis();
            log.info("order id:{}, expired:{}", orderId, time);
            producerService.produce(orderId, expired);
            try {
                //log.info("no data will sleep");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                log.error("InterruptedException", e);
            }
        }
    }
}

为了方便我们观察,我们采用在当前时间的基础上加上一个随机的时间作为订单的过期时间,每产生一个订单线程休眠0.5秒,总共产生10个订单。

消息的消费过程:

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class ConsumerServiceTest {
    @Autowired
    private ConsumerService consumerService;

    @Test
    public void consume() {
        consumerService.consume();
    }
}

该过程比较简单,它主要是启动我们的循环函数从Redis中取数据。

先启动我们的消息消费过程ConsumerServiceTest,然后再启动我们的消息生产过程ProducerServiceTest,观察日志的打印输出。

消息生产过程:


消息消费过程:


仔细对比我们可以很容易发现:

  1. order-id-5的过期时间最短,它也是最先被消费掉的,其次是order-id-7;
  2. 当生产消息的过程完成后共产生了10个消息,消息消费过程中,每消费一个消息,又没有产生新的消息的时候,整个消息队列的长度在变小;


通过上面的演示,我们实现了基于Redis实现简单的延时消息队列,最主要我们使用了Redis的Zset特性来完成延时消息队列的功能。

上面的演示在高并发场景下,可能会存在问题:

  • 高并发场景下对Redis的操作不够原子性;
  • 不适合分布式应用场景下使用;
  • 适合单体应用中延时消息队列的使用;

基于上面所提到的问题,最好的解决方案是采用消息队列框架去实现,例如:RabbitMQ给延时队列统一设置消息过期时间,过期的消息将被路由到另一个队列中;也可以给每一个消息设置一个过期时间,过期的消息同样路由到另一个队列中。


源代码GitHub地址:

https://github.com/bq-xiao/delay-message-queue


不积跬步,无以至千里;不积小流,无以成江海!

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: