当前位置: 首页 > news >正文

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

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";@Overridepublic 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 idstringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());}catch (Exception e) {// 消息没有被ACK确认 进入Pending Listlog.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 idstringRedisTemplate.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. 初始化方法  一初始化就执行@PostConstructpublic void init(){SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}//3. 创建线程任务用于接收消息队列的信息private class VoucherOrderHandler implements Runnable{// 消息队列名称private String queueName = "stream.orders";@Overridepublic 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 idstringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());}catch (Exception e) {// 消息没有被ACK确认 进入Pending Listlog.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 idstringRedisTemplate.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;@Overridepublic Result seckillVoucher(Long voucherId) {// 获取用户Long userId = UserHolder.getUser().getId();// 获取订单idlong orderId =  redisIdWorker.nextId("order");//1.执行Lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(),userId.toString(),String.valueOf(orderId));//2.判断结果是否为0int r = result.intValue();if(r != 0){//3.不为0,代表没有购买资格return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");}//提前 获取代理对象proxy = (IVoucherOrderService) AopContext.currentProxy();//5.返回订单idreturn Result.ok(orderId);}/*** 秒杀优惠券下单------秒杀优化代码----创建订单* @param voucherOrder*/@Transactionalpublic 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.lryc.cn/news/442011.html

相关文章:

  • 计算机专业的就业方向
  • VSCode C++ Tasks.json中的变量
  • 第一次安装Pytorch
  • Python数据分析-Steam 收入排名前 1500 的游戏
  • Android14请求动态申请存储权限
  • Doris:数据库建表最佳实践
  • Parallels Desktop 20(Mac虚拟机) v20.0.0 for Mac 最新破解版(支持M系列)
  • 【已解决】华为AR100-S路由器 恢复出厂后,找不到5G wifi的设置
  • 【MongoDB】--MongoDB批量操作
  • 数据库常规操作
  • 基于STM32设计的水渠闸门远程控制系统(华为云IOT)(226)
  • 鸿蒙开发(NEXT/API 12)【响应校验】远场通信服务
  • 2024最新!!!iOS高级面试题,全!(二)
  • 【C#生态园】构建你的C#操作系统:框架选择与实践
  • ADB 安装教程:如何在 Windows、macOS 和 Linux 上安装 Android Debug Bridge
  • java(2)方法的使用
  • 基于对数变换的图像美白增强,Matlab实现
  • MySQL高阶1873-计算特殊奖金
  • Ngnix 在windows上的简单使用
  • 嵌入式开发--STM32延时函数重构
  • OpenAI最新发布的o1-preview模型,和GPT-4o到底哪个更强?
  • 基于Python+SQLite的课程管理系统
  • 每日一练 | USG系统默认安全区域
  • 技术老总眼中的品宣与促销:挑战与对策
  • [全网首篇]关于 VMSA-2024-0019 安全公告(CVE-2024-38812、CVE-2024-38813)的说明与解决方案
  • 监控易监测对象及指标之:全面监控GBase数据库
  • 推荐五种msvcr71.dll丢失的解决方法,msvcr71.dll为什么会丢失?
  • Java 内存模型(JMM)
  • 用于安全研究的 Elastic Container Project
  • STM8L101低功耗的理解