RabbitMQ延时队列--实现定时任务
消息的TTL:消息的存活时间
RabbitMQ可以分别对队列和消息设置TTL:
- 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做设置,超过了这个时间,我们就认为消息死了,称之为死信
- 如果队列设置了,消息也设置了,会取小的时间,所以一个消息如果被路由到不同的队列中,这个消息的死亡时间有可能不一样(因为队列的TTL不一致)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键,可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者都是一样的效果。
如果一个消息在满足如下条件下,会进死信路由:
- 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用
- 上面消息的TTL到了,消息过期了
- 队列的长度被限制满了,排在前面的消息被丢弃或者扔到死信路由上
我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由指定到某一个交换机,结合二者,就可以实现一个延时队列,总结来说我们就是要使用死掉的消息来完成延时队列。
一般设定队列的TTL来完成延时队列功能,如果设置消息延时的话,假如有三个消息分别是5分钟,1分钟,2分钟,由于RMQ的惰性检查机制,他检查第一个消息发现是五分钟后过期,服务器就会五分钟之后再过来取走消息,导致后面两个短时间的消息都要五分钟后才能取出来
订单模块具体延时流程(一旦队列创建,更改设置无法更新,必须先删除了之后再进行重新创建)
测试流程可用性:
订单模块创建交换机,绑定关系,队列,消费者
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/29-15:59
* @ Description
*/
@Configuration
public class MyMQConfig {
@RabbitListener(queues = "order.release.order.queue")//消费者
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单"+orderEntity);
//手动签收消息(拿到原生消息,选择不批量告诉)
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
//创建绑定关系,队列和交换机的便捷方式
@Bean
public Queue orderDelayQueue(){
HashMap<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange","order-event-exchange");//死信路由
map.put("x-dead-letter-routing-key","order.release.order");//死信的路由键
map.put("x-message-ttl",60000);//消息过期时间一分钟
//队列名字,是否持久化,是否排他(只能被一个连接使用),是否自动删除
return new Queue("order.delay.queue",true,false,false,map);
}
@Bean
public Queue orderReleaseOrderQueue(){
return new Queue("order.release.order.queue",true,false,false);
}
@Bean
public Exchange orderEventExchange(){
//名字,是否持久化,是否自动删除 Topic交换机可以绑定多个队列
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
//两个绑定关系
public Binding orderCreateOrder(){
return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.create.order",null);
}
@Bean
public Binding orderReleaseOrder(){
return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.release.order",null);
}
}
创建测试接口
package com.wuyimin.gulimall.order.web;
/**
* @ Author wuyimin
* @ Date 2021/8/26-9:53
* @ Description
*/
@Controller
public class HelloController {
@Autowired
RabbitTemplate rabbitTemplate;
@ResponseBody
@GetMapping("/test/createorder")
public String createOrderTest(){
OrderEntity orderEntity=new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给mq发送消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "ok";
}
}
测试结果
创建业务交换机和队列
库存模块延时具体流程:
库存模块整合MQ
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件:
应用注解:
消息转换Json配置,创建交换机,队列,绑定关系
package com.wuyimin.gulimall.ware.config;
/**
* @ Author wuyimin
* @ Date 2021/8/25-18:58
* @ Description 返回Json配置
*/
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();//返回消息转成json
}
//交换机
@Bean
public Exchange stockEventExchange(){
return new TopicExchange("stock-event-exchange",true,false,null);
}
//普通队列用于解锁库存
@Bean
public Queue stockReleaseStockQueue(){
return new Queue("stock.release.stock.queue",true,false,false,null);
}
//延迟队列
@Bean
public Queue stockDelayQueue(){
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue",true,false,false,arguments);
}
/**
* 交换机和延迟队列绑定
*/
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
/**
* 交换机和普通队列绑定
*/
@Bean
public Binding stockReleaseBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
}
随便创建一个消费者,然后就可以看到创建好的队列和交换机了
库存的自动解锁功能
库存解锁的场景
1.下订单成功,订单过期没有支付被系统自动取消,被用户手动取消
2.下订单成功,库存锁定成功,但是接下来的业务调用失败导致订单回滚,之前锁定的库存就要自动解锁
创建一个库存系统发送库存锁定消息的To(id其实并没有用到。。。)
package com.wuyimin.common.to.mq;
/**
* @ Author wuyimin
* @ Date 2021/8/29-19:08
* @ Description 库存锁定成功的To
*/
@Data
public class StockLockedTo {
private Long id;
private StockDetailTo detailTo;//工作单详情的所有id
}
package com.wuyimin.common.to.mq;
/**
* @ Author wuyimin
* @ Date 2021/8/29-19:18
* @ Description
*/
@Data
public class StockDetailTo {
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 1-已锁定 2-已解锁 3-扣减
*/
private Integer lockStatus;
}
记得之前我们为order服务配置了一个拦截器,导致其他服务远程调用order服务会被拦截(因为需要登录),这显然是不合理的,所以我们方向远程调用
package com.wuyimin.gulimall.order.interceptor;
/**
* @ Author wuyimin
* @ Date 2021/8/26-10:52
* @ Description 拦截未登录用户
*/
@Component //放入容器中
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser=new ThreadLocal<>();//方便其他请求拿到
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri=request.getRequestURI();
//放行远程调用,匹配/order/order/status/**的uri直接放行
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if(match){
return true;
}
HttpSession session = request.getSession();//获取session
MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute!=null){
//已经登录
loginUser.set(attribute);
return true;
}else{
//给前端用户的提示
request.getSession().setAttribute("msg","请先进行登录");
//未登录
response.sendRedirect("http://auth.gulimall.com/login.html");//重定向到登录页
return false;
}
}
}
来创建一个consumer监听消费消息
package com.wuyimin.gulimall.ware.listener;
/**
* @ Author wuyimin
* @ Date 2021/8/29-20:53
* @ Description
*/
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
//这里我放在类方法上报错了
@RabbitListener(queues = "stock.release.stock.queue")//监听队列
/*
处理消息的方法(解锁)
*/
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("进入了方法");
try {
System.out.println("收到了消息开始处理。。。");
wareSkuService.handleStockLockedRelease(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不选择批量回复
} catch (Exception e) {
System.out.println("拒收了消息。。。");
//有异常就让他重新回队
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
对于wareService的处理消息方法
/*
处理消息的方法(解锁)
*/
@Override
public void handleStockLockedRelease(StockLockedTo to) {
System.out.println("收到了解锁库存的消息");
StockDetailTo detailTo = to.getDetailTo();//详细信息
Long detailToId = detailTo.getId();//具体工作单的id
/*
解锁的两种情况:根据我们拿到的To,我们取查询数据库,看是否能得到订单的锁定库存结果
如果没有:库存锁定失败了,库存回滚,无需解锁,无需操作
如果有:说明是库存以下的10/0错误,这种时候需要回滚
*/
WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailToId);
if(byId!=null){
//解锁--到此库存系统一切正常,但是不确定订单是什么情况
//订单情况1:没有这个订单(订单回滚),必须解锁
//订单情况2:有这个订单,要看订单状态(如果是已取消:就可以取解锁库存,其他任何状态都不可以解锁库存)
Long orderId = to.getId();//拿到订单的id
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getById(orderId);
String orderSn = orderTaskEntity.getOrderSn();//订单号,我们需要拿着这个订单取查询订单状态
R r = orderFeignService.getOrderByOrderSn(orderSn);
if(r.getCode()==0){
//远程调用成功
Integer status = r.getData(new TypeReference<Integer>() {
});
if(status==null||status==4){
//订单已经被取消了,订单不存在, 解锁库存
if(byId.getLockStatus()==1){//当前具体工作单必须是未解锁状态才行
unlockStock(detailTo.getSkuId(),detailTo.getWareId(),detailTo.getSkuNum(),detailTo.getId());
}
}
}else{
//远程服务失败
throw new RuntimeException("远程服务失败,消息消费失败");
}
}
}
中间的远程调用方法,注意这里不能return null过去,即使没有数据也是一种成功的情况,如果返回null,那么调用方会觉得是远程调用失败了,而不是订单回滚了
@GetMapping("/status/{orderSn}")
public R getOrderByOrderSn(@PathVariable("orderSn") String orderSn){
OrderEntity order_sn = orderService.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
if(order_sn!=null){
Integer status = order_sn.getStatus();
return R.ok().setData(status);
}
else return R.ok().setData(null);
}
解锁库存的方法
/*
只要解锁库存的消息失败,一定要告诉服务器此次消息解锁是失败的,启用手动的ack机制
*/
//解锁的方法
private void unlockStock(Long skuId, Long wareId, Integer num, Long Id){
wareSkuDao.unlockStock(skuId,wareId,num);
//更新库存工作单的状态
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
entity.setId(Id);
entity.setLockStatus(2);//已经解锁
wareOrderTaskDetailService.updateById(entity);
}
unlockStock方法(具体落实到数据库的逻辑)
<update id="unlockStock">
update wms_ware_sku set stock_locked=stock_locked-#{num}
where sku_id=#{skuId} and ware_id=#{wareId}
</update>
定时关单
生产者发送消息:
package com.wuyimin.gulimall.order.service.impl;
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
//@GlobalTransactional//全局事务注解,高并发模式下并不适用
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
threadLocal.set(orderSubmitVo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//下单:去创建订单,检验令牌,检验价格,锁定库存
//1.验证令牌
String orderToken = orderSubmitVo.getOrderToken();//页面传递过来的值
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId();//redis里存的key
//lua脚本保证原子性 返回1代表删除成功,0代表删除失败
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);
if(res==1){
//验证成功--下单创建订单,检验令牌,检验价格,锁库存
OrderCreateVo order = createOrder();
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = orderSubmitVo.getPayPrice();
if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
//金额对比成功
//保存信息
saveOrder(order);
//锁定库存,只要有异常就回滚订单数据
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks=order.getOrderItems().stream().map(item->{
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockVo.setLocks(locks);
//远程锁库存操作
R r = wareFeignService.orderLockStock(wareSkuLockVo);
if(r.getCode()==0){
//锁定成功了
responseVo.setOrder(order.getOrder());
responseVo.setCode(0);
//RabbitMQ发送消息---到这里下面的步骤都不可能失败了,所以可以发送订单创建成功的消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order);
return responseVo;
}else{
//锁定失败了
responseVo.setCode(3);
throw new NoStockException();
}
}else{
responseVo.setCode(2);
return responseVo;
}
}else{
//验证失败
return responseVo;
}
}
消费者消费消息
package com.wuyimin.gulimall.order.listener;
@RabbitListener(queues = "order.release.order.queue")
@Component
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单"+orderEntity);
//手动签收消息(拿到原生消息,选择不批量告诉)
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
closeOrder方法和saveOrder方法
package com.wuyimin.gulimall.order.service.impl;
//订单过期后关闭订单
@Override
public void closeOrder(OrderEntity orderEntity) {
OrderEntity byId = this.getById(orderEntity);
if(byId.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())){
//只有待付款的状态需要关单
byId.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(byId);
}
}
//保存订单信息
private void saveOrder(OrderCreateVo orderCreateTo) {
OrderEntity order = orderCreateTo.getOrder();
order.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
order.setCreateTime(new Date());
order.setModifyTime(new Date());
this.save(order);
orderItemService.saveBatch(orderCreateTo.getOrderItems());
}
这一块联合库存服务实现的主要逻辑:
所以在关闭订单的时候也要发送一个消息:告诉库存服务需要解锁库存
添加一个绑定关系:
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/29-15:59
* @ Description 测试MQ可用性
*/
@Configuration
public class MyMQConfig {
/*
添加的绑定关系
*/
@Bean
public Binding orderReleaseOther(){
return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.release.other.#",null);
}
}
更改后的关闭订单函数
package com.wuyimin.gulimall.order.service.impl;
@Override
public void closeOrder(OrderEntity orderEntity) {
OrderEntity byId = this.getById(orderEntity);
if(byId.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())){
//只有待付款的状态需要关单
byId.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(byId);
/*
给仓储服务发送消息
*/
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
stock模块添加新的监听器
使用RabbitListener和RabbitHandler配合可以处理同一队列的不同消息
package com.wuyimin.gulimall.ware.listener;
/**
* @ Author wuyimin
* @ Date 2021/8/29-20:53
* @ Description
*/
@RabbitListener(queues = "stock.release.stock.queue")//监听队列
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/*
处理消息的方法(解锁)
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("进入了方法");
try {
System.out.println("收到了消息开始处理。。。");
wareSkuService.handleStockLockedRelease(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不选择批量回复
} catch (Exception e) {
System.out.println("拒收了消息。。。");
//有异常就让他重新回队
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
/*
处理消息的方法(解锁)
*/
public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
System.out.println("收到订单关闭的消息");
try{
wareSkuService.handleStockLockedRelease(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不选择批量回复
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
调用的重载方法-其实就是把参数准备好重新调用unlock
package com.wuyimin.gulimall.ware.service.impl;
/*
考虑服务卡顿
*/
@Transactional
@Override
public void handleStockLockedRelease(OrderTo to) {
String orderSn=to.getOrderSn();
//查以下最新库存的状态
WareOrderTaskEntity orderTaskEntity=wareOrderTaskService.getOrderTaskByOrderSn(orderSn);//按照名字来查到库存工作单
Long id = orderTaskEntity.getId();
//按照工作单id找到所有工作单详情
List<WareOrderTaskDetailEntity> detailEntities = wareOrderTaskDetailService.list(
new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status",1));//状态为1表示未解锁 2为已解锁
//解锁操作
for (WareOrderTaskDetailEntity detailEntity : detailEntities) {
unlockStock(detailEntity.getSkuId(),detailEntity.getWareId(),detailEntity.getSkuNum(),detailEntity.getId());
}
}
新实现的逻辑:
最后的逻辑
类似于双保险,两分钟自动解锁和立刻解锁
消息丢失,积压,重复等解决方案
消息丢失:
1)发送出去没抵达服务器
1.做好容错方法,(try-catch)包裹消息发送代码段
2.做好日志记录,每个消息状态是否被服务器收到都要记录
3.做好定期重发,如果消息没有发送成功,定期取数据库扫描未成功发送的消息重发
数据库建立表
public class MqMessageEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private String messageId;
/**
* JSON
*/
private String content;
private String toExchange;
private String classType;
/**
* 0-新建 1-已发送 2-错误抵达 3-已抵达
*/
private Integer messageStatus;
private Date createTime;
private Date updateTime;
}
2)消息抵达Broker,写入磁盘的时候宕机
publisher加入确认回调机制,确认成功的消息,修改数据库消息状态
3)自动ACK状态下,消费者收到消息,单没来的及处理就宕机了
开启手动ACK,消费成功才移除,失败的消息重新入队
消息重复:
消息消费成功,事务已经提交,ack的时候,机器宕机,导致ack没有成功,Broker的消息重新由unack变成ready,发送给其他消费者
消息消费失败,由于重试机制,自动又将消息发送出去
成功消费,ack的时候宕机,消息由unack变成ready,Broker又重新发送(只有这个情况需要解决):
- 消费者的业务消费接口应该设计为幂等性的
- 使用防重表,发送消息每一个都有业务的唯一标识,处理过就不用梳理
- MQ每个消息都由receive字段,可以获取消息是否是被重新投递过来的信息
消息挤压:
消费者宕机挤压
消费者消费能力不足
发送者发送流量过大:
- 上线更多的消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理