一、基本介绍
①延时队列(实现定时任务)
场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
常用解决方案: spring的 schedule定时任务轮询数据库:
缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差
解决: rabbitmqExchange的消息TTL和死信结合
②消息的TL(Time To Live)消息的TTL就是消息的存活时间。
RabbitMQ可以对队列和消息分别设置TTL
- 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
- 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的 expiration-message-字段或者x--ttl属性来设置时间,两者是一样的效果。
③ Dead Letter Exchanges (DLX)
一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。(什么是死信)
- 一个消息被Consumer拒收了,并且 reject方法的参数里是。也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/basic.nack) requeue=false上面的消息的TTL到了,消息过期了。-
- 一队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
Dead Letter Exchangeexch其实就是一种普通的,和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到 Dead Letter Exchange中去。
·我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列
二、推荐:给队列设置延时时间
①:因为RabbitMQ采用惰性检查机制
RabbitMq采用惰性检查机制,也就是懒检查机制:比如消息队列中存放了多条消息,第一条是5分钟过期,第二条是1分钟过期,第三条是1秒钟过期,按照正常的过期逻辑,应该是1秒过期的先排出这个队列,进入死信队列中,但是实际RabbitMQ是先拿第一条消息,也就是5分钟过期的,一看5分钟还没到过期时间,然后等待5分钟会将第一条消息拿出来,放入死信队列,这里就会出现问题,第二条设置1分钟的和第三条设置1秒钟的消息必须要等待第一条5分钟过期后才能过期,等待第一条消息过期5分钟了,拿第二条、三条的时候都不需要判断就已经过期了,直接就放入死信队列中,所以第二条、三条需要等待第一条消息过了5分钟才能过期,这样的延时根本就没产生对应的效果。
②:理论结构图
③:项目结构图
④:代码实现
4.1:基础设置
4.1.1:创建信道、队列、路由(结构图)
4.1.2:MyRabbit配置
@Configuration public class MyRabbitConfig { /** * 使用JSON序列化机制,进行消息转移 */ @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } @Bean public Exchange stockEventExchange() { TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false); return topicExchange; } @Bean public Queue stockReleaseStockQueue() { return new Queue("stock.release.stock.queue", true, false, false); }
@Bean public Queue stockDelayQueue() { HashMap<String, Object> args = new HashMap<>(); args.put("x-dead-letter-exchange", "stock-event-exchange"); args.put("x-dead-letter-routing-key", "stock.release"); args.put("x-message-ttl", 120000); //延时2min return new Queue("stock.delay.queue", true, false, false,args); } @Bean public Binding stockReleaseBinding() { return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.release.#", null); } @Bean public Binding stockLockedBinding() { return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.locked", null); } }
4.1.3:设置监听器
@Service @RabbitListener(queues = "stock.release.stock.queue") public class StockRelaeaseListener { @Autowired WareSkuService wareskuService; @RabbitHandler public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException { System.out.println("收到解锁信息"); try { wareskuService.unlockStock(to); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); } } @RabbitHandler public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException { System.out.println("订单关闭准备解锁库存"); try { wareskuService.unlockStock(orderTo); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); } } }
4.2、wareskuService类:
4.2.1:orderLockStock方法:
/** * 为某个订单锁定库存 * * @param vo * @return * @Transactional(rollbackFor = NoStockException.class)运行出现异常时回滚 */ @Transactional @Override public Boolean orderLockStock(WareSkuLockVo vo) { /** * 保存库存工作单的详情,为了追溯哪个仓库锁了多少 */ WareOrderTaskEntity taskEntity = new WareOrderTaskEntity(); taskEntity.setOrderSn(vo.getOrderSn()); orderTaskService.save(taskEntity); //1、按照下单的收获地址,找到就近仓库进行锁定库存 List<OrderItemVo> locks = vo.getLocks(); List<SkuWareHasStock> collect = locks.stream().map(item -> { SkuWareHasStock stock = new SkuWareHasStock(); Long skuId = item.getSkuId(); stock.setSkuId(skuId); stock.setNum(item.getCount()); //查询这个商品在哪里有库存 List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId); stock.setWareId(wareIds); return stock; }).collect(Collectors.toList()); for (SkuWareHasStock hasStock : collect) { Boolean skuStocked = false; Long skuId = hasStock.getSkuId(); List<Long> wareIds = hasStock.getWareId(); if (wareIds == null || wareIds.size() == 0) { //没有库存抛出异常 throw new NoStockException(skuId); } //如果每一个商品都锁成功,将当前商品锁定了几件发送给MQ //如果锁定失败,前面保存的工作单信息就回滚了。 for (Long wareId : wareIds) { //成功就返回1,否则就是0 Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum()); if (count == 1) { //TODO:表明锁住了,发消息告诉MQ库存锁定成功 //在数据表wms_ware_order_task_detail中存入库存单(*仓库/*商品/*数量/被锁*件)做记号 WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1); orderTaskDetailService.save(entity); StockLockedTo lockedTo = new StockLockedTo(); lockedTo.setId(taskEntity.getId()); StockDetailTo stockDetailTo = new StockDetailTo(); //拷贝属性和数值 BeanUtils.copyProperties(entity, stockDetailTo); //防止wms_ware_order_task_detail表内数据因为回滚丢失,所以new一个StockLockedTo类记录失败提交的数据 lockedTo.setDetail(stockDetailTo); //将库存工作单的详情放入exchange中 rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo); skuStocked = true; break; } else { //锁失败了,重试下一个仓库 } } if (skuStocked == false) { //当前商品所有仓库都没锁住库存数量 throw new NoStockException(skuId); } } //肯定全部都是锁定 return true; }
4.2.1、unlockStock方法:
@Override public void unlockStock(StockLockedTo to) { StockDetailTo detail = to.getDetail(); Long detailId = detail.getId(); /** * 1、查询数据库wms_ware_order_task_detail表关于这个订单的锁定库存信息 * ①表里有关于锁库存的信息,队列设置延时时间,检查订单的状态,确认是否需要进行解锁 * 1.1:解锁前查看订单情况:则需要解锁库存 * 1.1.1查看订单状态,查看订单状态,若订单已取消则必须解锁库存 * 1.1.2查看订单状态,订单未取消则不能解锁库存 * * 1.2:如果没有订单情况:则必须解锁库存 * * ②没有则代表整个库存锁定失败,事务回滚了,这种情况无需解锁 * * 只要解锁库存失败,利用手动模式 */ WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId); if (byId != null) { //解锁 Long id = to.getId(); WareOrderTaskEntity taskEntity = orderTaskService.getById(id); String orderSn = taskEntity.getOrderSn();//根据订单号查询订单状态 //查找订单是否创建成功,此处远程调用会因为拦截器需要先登录,因此按4.3进行修改 R r = orderFeignService.getOrderStatus(orderSn); if (r.getCode() == 0) { //订单数据返回成功 OrderVo data = r.getData(new TypeReference<OrderVo>() { }); //只有订单状态是取消状态/或者订单不存在才可以解锁.4为状态码代表订单是取消状态 System.out.println("Data1:" + data); if (data == null || data.getStatus() == 4) { //当前库存单详情状态1已锁定但是未解锁才可以解锁 unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId); /* if (byId.getLockStatus() == 1) { System.out.println("Data2:" + data); }*/ } } else { //消息拒绝以后重新放入队列里,让其他人继续消费解锁 throw new RuntimeException("远程服务失败"); } } }
4.2.2:unLockStock方法:
//库存解锁方法 private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) { wareSkuDao.unlockStock(skuId, wareId, num); } //wareSkuDao.xml中更新代码: /**<update id="unlockStock"> UPDATE wms_ware_sku SET stock_locked = stock_locked- #{num} WHERE sku_id= #{skuId} AND ware_id=#{wareId} </update>**/
4.2.3:unlockStock(OrderTo orderTo)方法:
/* *防止订单服务卡顿,导致订单消息一直更改不了,库存优先到期,查订单状态新建状态,什么都做不了就走了 *导致卡顿的订单,永远不能解锁 */ @Transactional @Override public void unlockStock(OrderTo orderTo) { String orderSn = orderTo.getOrderSn(); //进行到这一步再查一下最新的状态 WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn); //获取was_ware_order_task中的id,从而以其获取was_ware_order_task_detail状态为1(未解锁)的库存 Long id = task.getId(); List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1)); //Long skuId, Long wareId, Integer num, Long taskDetailId for (WareOrderTaskDetailEntity entity:entities){ unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId()); } }
4.3:拦截器修改
@Component public class LoginUserInterceptor implements HandlerInterceptor { public static ThreadLocal<MemberResVo> loginUser = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //此地址下不进行拦截 String uri = request.getRequestURI(); boolean match = new AntPathMatcher().match("/order/order/status/**", uri); boolean match1 = new AntPathMatcher().match("/payed/notify", uri); if (match || match1){ return true; }
//获取登录用户的键 MemberResVo attribute = (MemberResVo) request.getSession().getAttribute(AuthServerConstant.LONG_USER); if (attribute!=null){ loginUser.set(attribute); return true; }else { request.getSession().setAttribute("msg","请先进行登录!"); response.sendRedirect("http://auth.gulimall.com/login.html"); return false; } } }