【黑马点评】已解决java.lang.NullPointerException异常

news2025/1/9 16:41:49

Redis学习Day3——黑马点评项目工程开发`-CSDN博客

问题发现及描述

        在黑马点评项目中,进行到使用Redis提供的Stream消息队列优化异步秒杀问题时,我在进行jmeter测试时遇到了重大的错误

发现无论怎么测试,一定会进入到catch中,又由于消息队列是个循环读的过程,所以ERROR 33016错误就会不断的发生。

观察一下报错信息

java.lang.NullPointerException: Cannot invoke "com.hmdp.service.IVoucherOrderService.createVoucherOrder(com.hmdp.entity.VoucherOrder)" because the return value of "com.hmdp.service.impl.VoucherOrderServiceImpl.access$400(com.hmdp.service.impl.VoucherOrderServiceImpl)" is null

意思是

java.lang.NullPointerException 错误表明你的代码中有一个地方尝试调用了 null 对象的方法或访问了其属性。在你的具体错误信息中,问题出现在尝试调用 com.hmdp.service.IVoucherOrderService.createVoucherOrder(com.hmdp.entity.VoucherOrder) 方法时,但这个方法的调用是通过 com.hmdp.service.impl.VoucherOrderServiceImpl.access$400(com.hmdp.service.impl.VoucherOrderServiceImpl) 返回的对象进行的,而这个返回值为 null。

问题排除

既然明白了问题缘由是空对象导致出来的,那我们就根据报错的栈信息去处理:

定位位置

at com.hmdp.service.impl.VoucherOrderServiceImpl$VoucherOrderHandler.handleVocherOrder(VoucherOrderServiceImpl.java:406) ~[classes/:na]

at com.hmdp.service.impl.VoucherOrderServiceImpl$VoucherOrderHandler.handlePendingList(VoucherOrderServiceImpl.java:438) ~[classes/:na]

at com.hmdp.service.impl.VoucherOrderServiceImpl$VoucherOrderHandler.run(VoucherOrderServiceImpl.java:385) ~[classes/:na]

发现定位出现问题的是 执行订单创建方法  handleVocherOrder()

 跟进去看看,proxy代理对象也是一个报错提示点

结论         

        哦,这么一来问题就解决啦!原来是由于handleVocherOrder()需要使用到代理对象进行订单创建,那他必须不能写在线程任务了,要不然是没有办法获取到代理对象的,也就是null。就是因为这个空,才导致了我们的程序一致在报错。

错误代码说明

        一开始,为了代码逻辑的顺畅可懂,我将方法进行编号,并统一写入了线程任务VoucherOrderHandler方法中,在我看来handleVocherOrder()创建订单方法 和 handlePendingList()执行异常方法 对应着两者情况,本身的地位是一致的,于是乎将其都写在了线程的内部。

        但是没注意到的是,handleVocherOrder()需要调用在主线程提供的代理对象,这样一来就没理由将它写在异步线程任务中了。

//3. 创建线程任务用于接收消息队列的信息
    private class VoucherOrderHandler implements Runnable{

        // 消息队列名称
        private String queueName = "stream.orders";

        @Override
        public void run() {
            while (true) {
                try{
                    //1. 获取队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.oredes >
                    // 指定队列名称,组名称,消费者名称,读取模式,读取数量,阻塞时间,队列名称,读取位置
                    List<MapRecord<String,Object,Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    //2. 判断消息获取是否成功
                    if( list == null || list.isEmpty()){
                        //2.1 获取失败 说明没有消息 ---->继续循环
                        continue;
                    }
                    // 解析消息中的订单信息
                    MapRecord<String,Object,Object> record = list.get(0);
                    //  获取键值对集合
                    Map<Object,Object> values = record.getValue();
                    // 获取订单信息
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);

                    //3. 获取成功,执行订单创建
                    handleVocherOrder(voucherOrder);

                    //4. ACK确认  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());

                }catch (Exception e) {
                    // 消息没有被ACK确认 进入Pending List
                    log.error("订单处理出现异常",e);
                    handlePendingList();
                }
            }
        }

        // 4. 取到了订单—————创建订单
        private void handleVocherOrder(VoucherOrder voucherOrder){
            // 获取用户
            Long userId = voucherOrder.getUserId();

            // 1. 创建锁对象
            RLock lock =  redissonClient.getLock("lock:order:" + userId);

            //2. 尝试获取锁
            boolean isLock = lock.tryLock();

            // 3. 判断锁是否获取成功
            if(! isLock){
                log.error("不允许重复下单");
            }
            try {
                proxy.createVoucherOrder(voucherOrder);
            } finally {
                // 4. 释放锁
                lock.unlock();
            }
        }

        // 5.取不到订单————— 处理Pending List中的订单信息
        private void handlePendingList(){
            while (true) {
                try {
                    //1. 获取Pending List中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1  STREAMS stream.oredes 0
                    // 指定队列名称,组名称,消费者名称,读取模式,读取数量,阻塞时间,队列名称,读取位置
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    //2. 判断消息获取是否成功
                    if (list == null || list.isEmpty()) {
                        //2.1 获取失败 说明Pending List没有消息 ---->结束循环
                        break;
                    }

                    // 解析消息中的订单信息
                    MapRecord<String, Object, Object> record = list.get(0);
                    //  获取键值对集合
                    Map<Object, Object> values = record.getValue();
                    // 获取订单信息
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);

                    //3. 获取成功,执行订单创建
                    handleVocherOrder(voucherOrder);

                    //4. ACK确认  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());

                } catch (Exception e) {
                    log.error("Pending List订单处理出现异常", e);
                    try {
                        Thread.sleep(20);
                    }catch (InterruptedException interruptedException){
                        interruptedException.printStackTrace();
                    }
                }
            }
        }
    }

    

     正确代码展示

/** 方案二、三公共代码
     * 预加载lua脚本
     */
    private static DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        // 这是第二种方案需要执行的lua脚本
        // SECKILL_SCRIPT.setLocation(new ClassPathResource("lua/seckill.lua"));
        // 这是第三种方案需要执行的lua脚本
        SECKILL_SCRIPT.setLocation(new ClassPathResource("lua/streamSeckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);

    }


/*-----------------------------第三种方案: 使用Redis的stream消息队列 + redis + lua脚本判断秒杀资格添加消息队列 的方案-------------------------------------------------------------*/

    // 1,创建-- 秒杀线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    //2. 初始化方法  一初始化就执行
    @PostConstruct
    public void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    //3. 创建线程任务用于接收消息队列的信息
    private class VoucherOrderHandler implements Runnable{

        // 消息队列名称
        private String queueName = "stream.orders";

        @Override
        public void run() {
            while (true) {
                try{
                    //1. 获取队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.oredes >
                    // 指定队列名称,组名称,消费者名称,读取模式,读取数量,阻塞时间,队列名称,读取位置
                    List<MapRecord<String,Object,Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    //2. 判断消息获取是否成功
                    if( list == null || list.isEmpty()){
                        //2.1 获取失败 说明没有消息 ---->继续循环
                        continue;
                    }
                    // 解析消息中的订单信息
                    MapRecord<String,Object,Object> record = list.get(0);
                    //  获取键值对集合
                    Map<Object,Object> values = record.getValue();
                    // 获取订单信息
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);

                    //3. 获取成功,执行订单创建
                    handleVocherOrder(voucherOrder);

                    //4. ACK确认  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());

                }catch (Exception e) {
                    // 消息没有被ACK确认 进入Pending List
                    log.error("订单处理出现异常",e);
                    handlePendingList();
                }
            }
        }


        // 5.取不到订单————— 处理Pending List中的订单信息
        private void handlePendingList(){
            while (true) {
                try {
                    //1. 获取Pending List中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1  STREAMS stream.oredes 0
                    // 指定队列名称,组名称,消费者名称,读取模式,读取数量,阻塞时间,队列名称,读取位置
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    //2. 判断消息获取是否成功
                    if (list == null || list.isEmpty()) {
                        //2.1 获取失败 说明Pending List没有消息 ---->结束循环
                        break;
                    }

                    // 解析消息中的订单信息
                    MapRecord<String, Object, Object> record = list.get(0);
                    //  获取键值对集合
                    Map<Object, Object> values = record.getValue();
                    // 获取订单信息
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);

                    //3. 获取成功,执行订单创建
                    handleVocherOrder(voucherOrder);

                    //4. ACK确认  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());

                } catch (Exception e) {
                    log.error("Pending List订单处理出现异常", e);
                    try {
                        Thread.sleep(20);
                    }catch (InterruptedException interruptedException){
                        interruptedException.printStackTrace();
                    }
                }
            }
        }
    }

    // 4. 取到了订单—————创建订单
    private void handleVocherOrder(VoucherOrder voucherOrder){
        // 获取用户
        Long userId = voucherOrder.getUserId();

        // 1. 创建锁对象
        RLock lock =  redissonClient.getLock("lock:order:" + userId);

        //2. 尝试获取锁
        boolean isLock = lock.tryLock();

        // 3. 判断锁是否获取成功
        if(! isLock){
            log.error("不允许重复下单");
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            // 4. 释放锁
            lock.unlock();
        }
    }
    
    /**
     *  秒杀优惠券下单------秒杀优化代码----lua脚本---主线程---使用Redis stream的消息队列完成的
     */
    private IVoucherOrderService proxy;
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 获取用户
        Long userId = UserHolder.getUser().getId();
        // 获取订单id
        long orderId =  redisIdWorker.nextId("order");

        //1.执行Lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),userId.toString(),String.valueOf(orderId)
        );
        //2.判断结果是否为0
        int r = result.intValue();
        if(r != 0){
            //3.不为0,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");
        }

        //提前 获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();

        //5.返回订单id
        return Result.ok(orderId);
    }


    /**
     * 秒杀优惠券下单------秒杀优化代码----创建订单
     * @param voucherOrder
     */
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //4. 限制一人一单【悲观锁方案】
        Long userId = voucherOrder.getUserId();

        //4.1 查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        //4.2 判断订单是否存在
        // 是 -----> 返回异常信息---->结束
        if (count > 0) {
            log.error("用户已经购买了一次了");
        }

        //5. 扣减库存——解决超卖问题【乐观锁方案】
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1")
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // 库存大于0就行了
                .update();
        if (!success) {
            log.error("库存不足");
        }

        //6. 创建订单
        save(voucherOrder);
    }
}

   总结

        以前在遇到bug时,我总喜欢做的事是将别人写的代码复制回来。但是随着学习的深入发现,其实调代码是一件正常不过的事情,为此,锻炼自己发现问题、定位问题、解决问题能力十分重要,不断地刨根问底,才能愈发印象深刻。

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

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

相关文章

DRS部署(DM8-DM8)

DRS部署 一、规划端口二、设置环境变量三、开启源数据库的归档和逻辑日志四、配置DDL同步五、创建用户六、 DRS服务部署&#xff08;DM8目的端&#xff09;6.1 部署 drs 服务6.2启动drs服务 七、 DRS 服务部署&#xff08;DM8 源端&#xff09;7.1 部署 DRS服务7.2 启动dmhs服务…

C++第七节课 运算符重载

一、运算符重载 并不是所有情况下都需要运算符重载&#xff0c;要看这个运算符对这个类是否有意义&#xff01; 例如&#xff1a;日期减日期可以求得两个日期之间的天数&#xff1b;但是日期 日期没有意义&#xff01; #include<iostream> using namespace std; clas…

SpringBoot启动成功,但端口启动失败

目录 一、问题展示 二、问题分析 2.1.端口与Tomcat的关系 2.2.问题分析 三、SpringBoot常见知识记录 3.1.SpringBoot项目常用jar包 3.1.1.必要性jar包 3.1.2.选择性jar包 3.2.标签的作用及取值 3.2.1.compile&#xff08;编译范围&#xff09; 3.2.2.provided…

爵士编曲:爵士鼓编写 爵士鼓笔记 底鼓和军鼓 闭镲和开镲 嗵鼓

底鼓和军鼓 底鼓通常是动的音色&#xff0c;军鼓通常是大的音色。 “动”和“大”构成基础节奏。“动大”听着不够有连接性&#xff0c;所以可以加入镲片&#xff01; 开镲 直接鼓棒敲击是开镲音色 闭镲 当脚踩下踏板&#xff0c;2个镲片合并&#xff0c;然后用鼓棒敲击&am…

Koa安装和应用

文章目录 1、Koa21.1 简介1.2 安装1.3 简单使用1.4 使用脚手架创建Koa项目 1、Koa2 1.1 简介 Koa 是一个新的 web 框架&#xff0c;由 Express 幕后的原班人马打造&#xff0c; 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async…

rust快速创建Tauri App ——基于create-tauri-app

Tauri App Tauri是一个工具包&#xff0c;可以帮助开发人员使用现有的几乎任何前端框架为主要桌面平台制作应用程序。核心是用Rust构建的&#xff0c;CLI利用Node.js使Tauri成为创建和维护优秀应用程序的真正多语言方法。 cargo install create-tauri-appcreate-tauri-app&am…

多版本node管理工具nvm

什么是nvm&#xff1f; 在项目开发过程中&#xff0c;使用到vue框架技术&#xff0c;需要安装node下载项目依赖&#xff0c;但经常会遇到node版本不匹配而导致无法正常下载&#xff0c;重新安装node却又很麻烦。为解决以上问题&#xff0c;nvm&#xff1a;一款node的版本管理工…

FSFP——专为蛋白质工程设计的少样本学习策略

论文地址&#xff1a;通过小样本学习&#xff0c;以最少的湿实验室数据提高蛋白质语言模型的效率 参考文献&#xff1a;AI蛋白质设计“新引擎”:FSFP驱动大模型超低采样学习,少量数据显著提升蛋白质语言模型的性能 前言介绍&#xff1a;上海交通大学自然科学研究院洪亮教授课…

在STM32工程中使用Mavlink与飞控通信

本文讲述如何在STM32工程中使用Mavlink协议与飞控通信&#xff0c;特别适合自制飞控外设模块的项目。 需求来源&#xff1a; 1、增稳云台里的STM32单片机需要通过串口接收飞控传来的云台俯仰、横滚控制指令和相机拍照控制指令&#xff1b; 2、自制的有害气体采集器需要接收飞…

PCL 曲线点云提取

文章目录 一、简介二、实现代码三、实现效果参考文献一、简介 提取曲线点云的方法主要分为两种:参数化与非参数化,其中参数化是指事先直线曲线的形状,反之,非参数化则是不依赖与曲线的参数,通常是一种聚类的行为。这里我们采用非参数方法(TriplClust),将点集划分为一个未…

Java ETL - Apache Beam 简介

基本介绍 Apache Beam是一个用于大数据处理的开源统一编程模型。它允许用户编写一次代码&#xff0c;然后在多个批处理和流处理引擎上运行&#xff0c;如Apache Flink、Apache Spark和Google Cloud Dataflow等。Apache Beam提供了一种简单且高效的方式来实现数据处理管道&…

上海儿童自闭症寄宿制学校,让孩子找到归属感

在探讨自闭症儿童教育的广阔图景中&#xff0c;上海作为一座充满人文关怀的城市&#xff0c;始终致力于为这些特殊的孩子提供更加全面、专业的支持体系。而当我们把这份关注与努力投射到具体实践上&#xff0c;广州的星贝育园自闭症儿童寄宿制学校便成为了这样一个温馨而有力的…

蓝桥杯DS18B20程序源码

蓝桥杯DS18B20程序源码解析 蓝桥杯&#xff0c;作为一项全国瞩目的电子设计竞赛&#xff0c;其核心挑战在于参赛者需深度融合单片机编程与各类电子元件的应用能力。在众多项目中&#xff0c;涉及DS18B20数字温度传感器的程序源码尤为引人注目&#xff0c;它巧妙地将单片机技术…

ollama安装(ubuntu20.04)

Ollama是一款开源的自然语言处理工具&#xff0c;它可以帮助开发者快速构建文本处理应用。 ollama官网: https://ollama.ai/ 一、ollama 自动安装 linux统一采用sh脚本安装&#xff0c;一个命令行搞定。 curl -fsSL https://ollama.com/install.sh | sh二、ollama 手动安装 o…

使用 OpenCV 和 Matplotlib:绘制其彩色直方图以及拓展

如何使用 OpenCV 和 Matplotlib 读取、处理并显示图像。即将为您解答&#xff1a; 绘制其彩色直方图 代码解释 读取图像并转换颜色空间&#xff1a; image cv2.imread(001.jpg) image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB)cv2.imread(001.jpg)&#xff1a;从文件中…

C#基于SkiaSharp实现印章管理(7)

印章中的文本主要分为两种&#xff1a;1&#xff09;从左向右水平绘制的文本&#xff1b;2&#xff09;沿指定路径绘制的文本。前者使用SKCanvas的DrawText绘制文本&#xff0c;后者则使用SKCanvas的DrawTextOnPath绘制文本。   针对上述情况&#xff0c;调整SealElement类型…

Python编码系列—Python代理模式:为对象赋予超能力的魔法

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

【自学笔记】支持向量机(2)——核函数

引入 核函数的功能是将一组数据映射到更高维的特征空间&#xff0c;这样可以让在低维无法线性分类的数据能够在高维空间下被分类。   可以证明&#xff0c;如果原始数据是有限的维度&#xff0c;那么一定存在一个高维特征空间使得样本线性可分。 文章内容由《机器学习》相关内…

地平线秋招2025

【地平线秋招】 中秋卷起来&#xff01;&#xff01;&#xff01; 内推码 kbrfck 内推码 kbrfck 内推码 kbrfck 投递链接&#xff1a;https://wecruit.hotjob.cn/SU62d915040dcad43c775ec12c/mc/position/campus?acotycoCodekbrfck&recruitType1&isLimitShowPostScope…

Ubantu和Centos7一键shell更换镜像源与Linux系统Python3环境安装

目录 前言 1.一键更换源 1.1 创建文件 1.2 向环境赋予可执行的权限 2.Linux系统配置Python3环境 2.1 查看当前python环境 2.2 更换源 2.3 安装所需的依赖 2.4.下载python环境文件 2.5.解压文件 2.6 进行编译 2.7 开始安装 2.8 设置软连接 2.9 测试是否安装成功…