小米网站 用什么做的,深圳广告公司前十强,郑州建设信息网打不开,青岛开发区制作网站公司Redis学习Day3——黑马点评项目工程开发-CSDN博客
问题发现及描述 在黑马点评项目中#xff0c;进行到使用Redis提供的Stream消息队列优化异步秒杀问题时#xff0c;我在进行jmeter测试时遇到了重大的错误 发现无论怎么测试#xff0c;一定会进入到catch中#xff0c;又由…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 // 指定队列名称组名称消费者名称读取模式读取数量阻塞时间队列名称读取位置ListMapRecordString,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;}// 解析消息中的订单信息MapRecordString,Object,Object record list.get(0);// 获取键值对集合MapObject,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// 指定队列名称组名称消费者名称读取模式读取数量阻塞时间队列名称读取位置ListMapRecordString, 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;}// 解析消息中的订单信息MapRecordString, Object, Object record list.get(0);// 获取键值对集合MapObject, 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 DefaultRedisScriptLong 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 // 指定队列名称组名称消费者名称读取模式读取数量阻塞时间队列名称读取位置ListMapRecordString,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;}// 解析消息中的订单信息MapRecordString,Object,Object record list.get(0);// 获取键值对集合MapObject,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// 指定队列名称组名称消费者名称读取模式读取数量阻塞时间队列名称读取位置ListMapRecordString, 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;}// 解析消息中的订单信息MapRecordString, Object, Object record list.get(0);// 获取键值对集合MapObject, 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时我总喜欢做的事是将别人写的代码复制回来。但是随着学习的深入发现其实调代码是一件正常不过的事情为此锻炼自己发现问题、定位问题、解决问题能力十分重要不断地刨根问底才能愈发印象深刻。