SpringBoot+Redis实现不重复消费的队列

news2024/11/15 8:16:06

     背景

        最近我们新研发了一个“年夜饭订购”功能(没想到吧,雷袭在是一个程序猿的同时,也是一名优秀的在厨子)。用户使用系统选择年夜饭,点击“下单”时,后台首先会生成一条订单数据,返回消息给用户:“您已成功下单,后厨正在准备菜品!”。同时,以线程的方式指挥各个厨子按菜单联系供应商准备食材,制作菜品,最后打包寄给客户。但是,用户在使用这个功能时,系统却有一定的机率卡死,这个问题极大的影响了用户的体验。年关将近,这个功能也显得越发重要,客户要求我们限期整改,三天内必须解决该问题。

        我首先对这个功能进行了分析,很明显,这是一个使用频次不高,但是使用时间比较集中的功能。在大量用户同时使用时,会导致后台的厨师,食材,供应商等全面告警(用程序员语言翻译一下,这个功能耗CPU,耗内存,耗IO)。但用户对于实时性的要求并不高。下单之后,订购的菜品是一天内完成,还是两天完成并没有关系,只要年前能做完就可以。

        因此,我们决定采用消息中间件的方式,以队列的形式逐次的执行“年夜饭制作”的操作, 来缓解服务器的各种资源的压力。

        之所以采用Redis来实现消息队列,而不是使用更为成熟的ONS,Kafka。不是因为ONS用不起,而是Redis更有性价比(用户只允许使用ONS中间件,但ONS会带来额外的网络开销,学习成本和风险都更大,这个功能使用频度并不高,没有必要为了它而引入一个重量级的中间件。)

     代码实践

        说干就干,咱们先看看源码,如下:

// 订单实体类
@Data
public class OrderEntity implements Serializable {

    /**
     * 客户姓名
     */
    private String customerName;

    /**
     * 订单号
     */
    private String orderCode;

    /**
     * 菜单
     */
    List<String> menus;
}

@Slf4j
@Service
public class DinnerService {
    
    /**
     * 年夜饭下单
     *
     * @param req 订单信息
     * @return
     */
    public Object orderNewYearEveDinner(OrderEntity entity) {
        // 存储订单信息
        saveOrder(entity);
        // 异步开始做菜
        CompletableFuture.runAsync(() -> doNewYearEveDinner(entity));
        return "您已成功下单,后厨正在准备预制菜!";
    }

    /**
     * 这里模拟的是做年夜饭的过程方法,该方法用时较长,整个过程需要10秒。
     * 这个过程中存在多种意外,可能导致该方法执行失败
     *
     * @param req 订单信息
     */
    public void doNewYearEveDinner(OrderEntity entity) {
        System.out.println("开始做订单 " + entity.getOrderCode() + " 的年夜饭");
        try {
            Thread.sleep(10000);
        }catch (Exception e ) {
            e.printStackTrace();
            System.out.println("厨子跑了,厨房着火了,供应商堵路上了");
        }
        System.out.println("订单 " + entity.getOrderCode() + " 的年夜饭已经完成");
    }
    
    private void saveOrder(OrderEntity req) {
        //这里假设做的是订单入库操作
        System.out.println("订单 " + req.getOrderCode() + " 已经入库, 做饭开始时间为 "+ new Date());
    }

}

        1、引入maven依赖,在application.yml中添加redis配置


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
spring:
  redis:
    database: 9
    host: 127.0.0.1
    port: 6379
    password: 
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

        2、添加Redis队列监听,添加Redis配置文件注册监听

// 监听类
@Component
public class DinnerListener implements MessageListener {

    @Autowired
    private DinnerService service;

    @Override
    public void onMessage(Message message, byte[] pattern)  {
        OrderEntity entity= JSON.parseObject(message.toString(), OrderEntity.class);
        service.doNewYearEveDinner(entity);
    }
}


//配置类,用于注册监听
@Configuration
public class RedisConfig {
    @Bean
    public ChannelTopic topic() {
        return new ChannelTopic("NEW_YEAR_DINNER");
    }

    @Bean
    public MessageListenerAdapter messageListenerAdapter(DinnerListener listener) {
        return new MessageListenerAdapter(listener);
    }

    @Bean
    public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,
                                                        MessageListenerAdapter messageListenerAdapter,
                                                        ChannelTopic topic) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        container.addMessageListener(messageListenerAdapter, topic);
        return container;
    }

}

        3、修改原方法,以及Controller调用

// DinnerService中的方法修改   
 /**
     * 年夜饭下单
     *
     * @param req 订单信息
     * @return
     */
    public Object orderNewYearEveDinner(OrderEntity entity) {
        // 存储订单信息
        saveOrder(entity);
        // 异步开始做菜
        redisTemplate.convertAndSend("NEW_YEAR_DINNER", JSON.toJSONString(entity));
        return "您已成功下单,后厨正在准备预制菜!";
    }


@RestController
public class DinnerController {

    private int i = 0;
    
    @Autowired
    private DinnerService service;

    @GetMapping("/orderDinner")
    public Object orderDinner() {
        OrderEntity entity = new OrderEntity();
        entity.setOrderCode("Order" + (++i));
        entity.setCustomerName("第"+i+"位客户");
        return service.orderNewYearEveDinner(entity);
    }
}

        4、通过postman调用四次请求,测试结果如下:

        5、Listener中添加同步锁

        细看上文中打出来的注释,我发现这和我设想的不一样啊。原定的计划是先做完第一份年夜饭,再做第二份,做完第二份再做第三份,为什么第一次没执行完就开始执行第二次了?

        在网上查了些资料后我才知道,要达到我想要的效果,得在Listener中添加上同步锁,如下:

@Component
public class DinnerListener implements MessageListener {

    @Autowired
    private DinnerService service;

    private final Object lock = new Object();

    @Override
    public void onMessage(Message message, byte[] pattern)  {
        synchronized (lock) {
            OrderEntity entity = JSON.parseObject(message.toString(), OrderEntity.class);
            service.doNewYearEveDinner(entity);
        }
    }
}

        再次执行测试用例,结果如下:

        6、多服务不重复消费消息

        上面的结果已经满足了我们的要求,但是,客户考虑到我们只有一个厨房,的确影响效率,决定给我们扩建一个厨房(添加服务器),希望能达到厨房A做第一份订单,厨房B做第二份订单,以上的代码能实现吗?我们把刚才的项目拷贝一份,修改端口,启动后测试。结果如下:

        从上面的日志可以看出来,两个服务都做了订单1的年夜饭,消息被重复消费了。但是根据业务需求,我们不需要重复消费消息,我们想达到的效果是多服务实现负载均衡,本服务在处理的数据,其他服务不需要再处理了,应该怎么实现呢?咱们依然可以运用Redis,对代码做如下调整:

@Component
public class DinnerListener implements MessageListener {

    @Autowired
    private DinnerService service;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private final Object lock = new Object();

    @Override
    public void onMessage(Message message, byte[] pattern)  {
        synchronized (lock) {
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(message.toString(), "1", 1, TimeUnit.DAYS);
            // 加锁失败,已有消费端在此时对此消息进行处理,这里不再做处理
            if (!flag) {
                return;
            }
            OrderEntity entity = JSON.parseObject(message.toString(), OrderEntity.class);
            service.doNewYearEveDinner(entity);
        }
    }
}

        从测试结果来看,这么调整解决达到了我们的效果。

        7、添加日志监控

        仔细检查,发现上面的代码虽然满足了我们的业务需求,但是在安全方面仍然没有得到一定的保障,方法doNewYearEveDinner存在很多不可预见的隐患,如厨师跑了,厨房着了,供应商堵路上了,这些都会导致方法执行失败,那么,我们怎么知道这个订单执行成功或者失败了呢?看日志吗?成百上千条数据堆起来,通过看日志来看结果多不方便啊?咱们是否可以对代码做一下调整?基于这方面考虑,我对代码做了以下调整

//订单类进行调整
@Data
public class OrderEntity implements Serializable {

    /**
     * 客户姓名
     */
    private String customerName;

    /**
     * 订单号
     */
    private String orderCode;

    /**
     * 菜单
     */
    List<String> menus;

    /**
     * 出餐状态
     */
    private String dinnerState;
    
    /**
     * 做饭开始时间
     */
    private String dinnerStartTime;

    /**
     * 做饭结束时间
     */
    private String dinnerEndTime;

    /**
     * 备注
     */
    private String remark;
}

// DinnerService做如下调整, 添加一个订单信息更新的方法
@Slf4j
@Service
public class DinnerService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 年夜饭下单
     *
     * @param req 订单信息
     * @return
     */
    public Object orderNewYearEveDinner(OrderEntity req) {
        // 存储订单信息
        saveOrder(req);
        // 异步开始做菜
        redisTemplate.convertAndSend("NEW_YEAR_DINNER", JSON.toJSONString(req));
        return "您已成功下单,订单号为"+ req.getOrderCode()+",后厨正在准备预制菜!";
    }
    /**
     * 这里模拟的是做年夜饭的过程方法,该方法用时较长,整个过程需要10秒,但是,这个过程中存在多种意外,该方法可能失败
     *
     * @param req 订单信息
     */
    public void doNewYearEveDinner(OrderEntity req) throws Exception {
        System.out.println("开始做订单 " + req.getOrderCode() + " 的年夜饭");
        Thread.sleep(10000);
        System.out.println("订单 " + req.getOrderCode() + " 的年夜饭已经完成");
    }

    private void saveOrder(OrderEntity req) {
        //这里假设做的是订单入库操作
        System.out.println("订单 " + req.getOrderCode() + " 已经入库, 做饭开始时间为 "+ new Date());
    }

    /**
     * 根据订单编号修改订单信息
     *
     * @param orderCode 订单编号
     * @param dinnerStatus
     * @param remark
     */
    public void updateOrder(String orderCode, String dinnerStatus, String remark) {
        // 根据订单编号修改订单的出餐结束时间,出餐状态,备注等信息。
        System.out.println("更新订单 "+ orderCode +" 信息,做饭结束时间为 "+ new Date() + ", 出餐状态为"+ dinnerStatus +", 备注为 " +remark);
    }
}

// Listener中做如下调整
    @Override
    public void onMessage(Message message, byte[] pattern)  {
        synchronized (lock) {
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(message.toString(), "1", 1, TimeUnit.DAYS);
            // 加锁失败,已有消费端在此时对此消息进行处理,这里不再做处理
            if (!flag) {
                return;
            }
            OrderEntity param = JSON.parseObject(message.toString(), OrderEntity.class);
            try {
                service.doNewYearEveDinner(param);
                service.updateOrder(param.getOrderCode(), "SUCCESS", "成功");
            }catch (Exception e) {
                e.printStackTrace();
                service.updateOrder(param.getOrderCode(), "FAIL", e.getMessage());
            }
        }
    }

        这部分代码就不贴测试结果了,与上一次的测试结果一致,只不过提升了功能的可测试性,扩展一下,这个结果能否达到我们的要求呢?其实仍然没有,对于执行失败的订单,我们需要一个机制来处理,根据报错信息决定是重新执行还是直接报警,人为介入处理,由此才能实现整个事务的闭环。

        这是一次简单的SpringBoot+Redis实现队列的实践,个人觉得这个过程比较有趣,分析问题出现的原因,需求的潜在归约,根据业务的需要、当前的条件选择合适的方法和组件,快而有效的解决问题,所以我将它记录了下来,供大家参考。实际上,已经有大神对于Redis实现队列的方法进行了完整细致的归纳,如果想深入的了解这部分的知识,推荐你们看看这篇博客: Redis队列详解(springboot实战)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1484074.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

了解 SYN Flood 攻击

文章目录&#xff1a; 什么是 SYN Flood 攻击&#xff1f;对网络的影响SYN Flood 发生的迹象如何解决&#xff1f; 什么是 SYN Flood 攻击&#xff1f; SYN Flood&#xff08;SYN 洪水攻击&#xff09;是一种常见的分布式拒绝服务&#xff08;DDoS - Distributed Denial of Se…

Slicer学习笔记(六十五) 3DSlicer的医学图像数据增强扩展模块

1. 医学图像数据增强扩展模块 基于3D Slicer5.1.0 编写了一个测试医学图像的数据增强测试扩展模块。 扩展模块名&#xff1a;DataAugementation 项目地址&#xff1a;DataAugmentation 下载该项目后&#xff0c;可以将该扩展模块添加到3D Slicer的扩展中。 关于如何给3DSlicer…

3.1 序列式容器-vector

STL中一些常见的容器&#xff1a; 序列式容器&#xff08;Sequence Containers&#xff09;&#xff1a; vector&#xff08;动态数组&#xff09;&#xff1a; 动态数组&#xff0c;支持随机访问和在尾部快速插入/删除。list&#xff08;链表&#xff09;&#xff1a; 双向链表…

Unity3d Shader篇(十一)— 遮罩纹理

文章目录 前言一、什么是遮罩纹理&#xff1f;1. 遮罩纹理工作原理2. 遮罩纹理优缺点优点&#xff1a;缺点&#xff1a; 3. 遮罩纹理图 二、使用步骤1. Shader 属性定义2. SubShader 设置3. 渲染 Pass4. 定义结构体和顶点着色器函数5. 片元着色器函数 三、效果四、总结 前言 在…

springboot基于web的音乐网站论文

音乐网站 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了音乐网站的开发全过程。通过分析音乐网站管理的不足&#xff0c;创建了一个计算机管理音乐网站的方案。文章介绍了音乐网站的系统分析部分&#xff0c…

C++_运算符_算数运算符

运算符作用 用于执行代码的运算 算数运算符作用 用于处理四则运算 算术运算符包括以下符号 示例 加减乘除 取模运算 前置递增、递减&#xff0c;后置递增、递减

【C初阶】预处理

前言&#xff1a;在本文中&#xff0c;我将系统的整理一下C语言关于预处理部分的语法&#xff0c;便于整理与回归。 1.预定义符号 在C语言中&#xff0c;C标准提供里一些C预定义符号&#xff0c;在预处理期间完成&#xff0c;可以直接使用。 有如下几个符号&#xff1a; 2.#…

AI硬件暴涨的一晚

DELL 31% 引爆AI&#xff0c;都在寻找catch-up plays 软件和云都没咋动&#xff0c;硬件尤其是Semi全在涨。NVDA4%&#xff0c;AMD5%&#xff0c;MRVL8%&#xff0c;西部数据8%&#xff0c;博通7%&#xff08;Oppenheimer、BofA提目标价&#xff09;&#xff0c;台积电4%&…

AI技术大揭秘!你不可不知的顶级大模型

在这个数字化飞速发展的时代&#xff0c;AI大模型以其惊人的应用范围和深远的影响力&#xff0c;正逐渐成为各行各业的革命性力量。想象一下&#xff0c;在一个晴朗的午后&#xff0c;一个智能客服系统正轻松地处理着成千上万的客户咨询&#xff0c;不仅回答速度快捷&#xff0…

【Logback】Logback 的配置文件

目录 一、初始化配置文件 1、logback 配置文件的初始化顺序 2、logback 内部状态信息 二、配置文件的结构 1、logger 元素 2、root 元素 3、appender 元素 三、配置文件中的变量引用 1、如何定义一个变量 2、为变量设置默认值 3、变量的嵌套 In symbols one observe…

Vue开发实例(七)Axios的安装与使用

说明&#xff1a; 如果只是在前端&#xff0c;axios常常需要结合mockjs使用&#xff0c;如果是前后端分离&#xff0c;就需要调用对应的接口&#xff0c;获取参数&#xff0c;传递参数&#xff1b;由于此文章只涉及前端&#xff0c;所以我们需要结合mockjs使用&#xff1b;由于…

基于CNN-LSTM-Attention的时间序列回归预测matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1卷积神经网络&#xff08;CNN&#xff09;在时间序列中的应用 4.2 长短时记忆网络&#xff08;LSTM&#xff09;处理序列依赖关系 4.3 注意力机制&#xff08;Attention&#xff09; 5…

EasyRecovery16电脑硬盘数据恢复软件功能详解

在数字化时代&#xff0c;人们在日常生活和工作中越来越依赖于电脑和移动设备。不管是个人用户还是企业&#xff0c;数据的重要性都不言而喻。然而&#xff0c;数据丢失和损坏的风险也随之增加&#xff0c;因此&#xff0c;数据恢复软件的需求也日益增长。 EasyRecovery 16是一…

小程序和页面生命周期详解

目录 小程序的生命周期 创建&#xff08;onLoad&#xff09;&#xff1a; 显示&#xff08;onShow&#xff09;&#xff1a; 隐藏&#xff08;onHide&#xff09;&#xff1a; 卸载&#xff08;onUnload&#xff09;&#xff1a; 错误监听&#xff08;onError&#xff09;…

数据分析-Pandas数据的探查面积图

数据分析-Pandas数据的探查面积图 数据分析和处理中&#xff0c;难免会遇到各种数据&#xff0c;那么数据呈现怎样的规律呢&#xff1f;不管金融数据&#xff0c;风控数据&#xff0c;营销数据等等&#xff0c;莫不如此。如何通过图示展示数据的规律&#xff1f; 数据表&…

【深入了解设计模式】组合设计模式

组合设计模式 组合模式是一种结构型设计模式&#xff0c;它允许你将对象组合成树状结构来表现“整体-部分”关系。组合模式使得客户端可以统一对待单个对象和组合对象&#xff0c;从而使得代码更加灵活和易于扩展。 概述 ​ 对于这个图片肯定会非常熟悉&#xff0c;上图我们可…

python 基础知识点(蓝桥杯python科目个人复习计划57)

今日复习计划&#xff1a;做题 例题1&#xff1a;笨笨的机器人 问题描述&#xff1a; 肖恩有一个机器人&#xff0c;他能根据输入的指令移动相应的距离。但是这个机器人很笨&#xff0c;他永远分不清往左边还是往右边移动。肖恩也知道这一点&#xff0c;所以他设定这个机器人…

红外电力设施检测数据集

需要的同学私信联系&#xff0c;推荐关注上面图片右下角的订阅号平台 自取下载。 红外检测技术目标检测准确、速度快、涵盖面积广&#xff0c;可以在不停电、不接触、不解体、不采样的状态下&#xff0c;对带电设备的状态进行检测和诊断&#xff0c;精确查找出设备的劣化程度、…

springboot+vue小区物业管理系统

摘 要 随着我国经济发展和城市开发&#xff0c;人们对住房的需求增大&#xff0c;物业管理也得到了发展。但是&#xff0c;基于人工的物业管理仍然是现阶段我国大部分物业管理公司的管理模式&#xff0c;这种管理模式存在管理人员效率低下、工作难度大的问题&#xff0c;同时…

SpringCloudAlibaba介绍

Spring Cloud Alibaba Spring Cloud Alibaba 是什么&#xff1f;微服务全景图核心特色 大家好&#xff0c;我叫阿明。下面我会为大家准备Spring Cloud Alibaba系列知识体系&#xff0c;结合实战输出案列&#xff0c;让大家一眼就能明白得技术原理&#xff0c;应用于各公司得各…