江西建设局网站,广东网站建设类公司,做淘宝优惠网站,app推荐说明#xff1a;本文记录一次偶然出现的空指针异常#xff0c;在微服务架构中#xff0c;一个服务在调用另一个服务时#xff0c;出现了空指针异常。
业务描述#xff1a;在做订单超时功能时#xff0c;大家都知道#xff0c;可以使用RabbitMQ延迟队列#xff0c;下单…说明本文记录一次偶然出现的空指针异常在微服务架构中一个服务在调用另一个服务时出现了空指针异常。
业务描述在做订单超时功能时大家都知道可以使用RabbitMQ延迟队列下单的同时给队列发送一个延迟消息消息的内容是订单号比如延迟10分钟。10分钟之后该消息被消费者监听到会根据该订单ID查询数据库看该订单的状态是否为已支付是则忽略否则取消该订单恢复商品库存等等其他操作然而此时出现了空指针异常消息未被消费被路由到死信队列中。
微服务调用报空指针异常 消息被路由到死信队列 如下图的第三步 分析
首先排除FeignClient的问题因为下单减少库存取消订单恢复库存我使用的是同一个接口只是修改了商品的正负数不可能出现下单时可以取消订单时再使用就报错。
controller层代码 /*** 根据ID更新商品库存* param id* param num*/PutMapping(/update/{id}/{num})public void updateStockById(PathVariable(id) Long id, PathVariable(num) Integer num){itemService.updateStockById(id,num);}service层代码 Overridepublic void updateStockById(Long id, Integer num) {if (!ObjectUtil.isAllNotEmpty(id, num)) {System.out.println(参数不能为空);}if (id 0 || num 0) {System.out.println(参数非法);}update().setSql(stock stock num).eq(id, id).update();}其次再思考会不会不是因为Feign的调用报错而是微服务之间有什业务产生的报错。于是我找到了拦截器。
为了保证用户登录后经过Gateway网关后信息可以被下游服务获取到我的代码中是使用MVC拦截器Feign拦截器实现的如下图 每个服务会有两个拦截器分别把服务接收到的请求发出的请求拦截到然后分别解析用户信息添加用户信息到请求头以此达到参数透传用户信息可在微服务之间流传。
MVC拦截器代码获取请求头中用户的ID存到ThreadLocal中
public class AuthorizationInterceptor implements HandlerInterceptor {/*** 收到请求会执行的方法* param request* param response* param handler* return* throws Exception*/Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String id request.getHeader(authorization);if (id ! null id ! ){long l Long.parseLong(id);TokenThreadLocal.set(l);}else {responseHandler(response);return false;}// 放行return true;}……
}Feign拦截器将本服务中的ThreadLocal中的用户ID再设置到请求头上
/*** 发送请求拦截器*/
Slf4j
public class AuthorizationRequestInterceptor implements RequestInterceptor {Overridepublic void apply(RequestTemplate requestTemplate) {requestTemplate.header(authorization,TokenThreadLocal.get().toString());}
}排查
给这两个地方分别打上断点等订单超时后进入拦截器的代码排查一下
断点来到Feign拦截器选中这行代码一看原来是这里报了空指针异常 再一看原来是TokenThreadLocal.get().toString()这里是空的 然后恍然大悟MQ发送消息是异步请求ThreadLocal本地线程池对象自然为空
解决
很自然的想到一种很简单的解决方法发送消息的时候把ThreadLocal中的值用户ID也给发到延迟队列中然后在消费者监听的代码里面再使用ThreadLocal的set()方法把用户ID设置到线程池中
把订单ID、用户ID封装成一个Map转为json格式发送到延迟队列里 消费者代码这边使用ThreadLocal的set()方法把用户ID再设置进去 启动测试下单等待订单超时清理超时订单进入断点问题解决 总结
这是一个非常隐蔽的异常因为设置了死信队列未被成功消费的消息会被路由到死信队列中程序并不会报错并且因为订单表的内容大部分是在订单服务中此异常仅仅会影响订单被取消后调用商品服务恢复商品库存数量这一个很小的功能未能执行要排除出来是非常困难的。
而问题原因概括来说是因为ThreadLocal的值不能在RabbitMQ的消息中传递导致在使用拦截器获取ThreadLocal值的时候报了空指针异常。