概述
使用springboot + druid + redis + rabbitmq实现简单的秒杀系统,大致思路如下:
- ①、启动服务,缓存预热,将需要被秒杀的商品加载进redis缓存中,使用redis简单的string数据类型,key为商品ID,value为商品数量;
- ②、登录之后,选择可以秒杀的商品,可选操作有加入购物车和立即抢购;
- ③、点击抢购之后首先根据所选的商品ID查询redis,看是否商品数量小于等于0,如果是,则直接返回商品已售罄,否则将请求加入rabbitmq队列;
- ④、入队之后进行发送端发送确认,消息不管是否投递到交换机都进行ConfirmCallback回调,如果消息可以投递到交换机就返回true,投递不到交换机就返回false,之后交换机匹配到队列成功则不进行ReturnCallback回调;
- ⑤、之后消费者消费时进行手动确认,并再次查询redis的数量,预减之后看数量是否小于0,如果是就返回,否则就生成订单,并减少数据库的商品数量。
1、具体实现
①、生产者
请求到达后台,先查询redis的对应商品数量是否为0,若不为零,则将请求加入rabbitmq队列,队列生产者实现了RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback
两个接口用于发送端确认,从写了下面两个方法:
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("消息ID:" + correlationData.getId());
if (b) {
logger.info("消息发送确认成功");
} else {
logger.info("消息发送确认失败:" + s);
}
}
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
try {
logger.info("return--message:" + new String(message.getBody(), "UTF-8") + ",replyCode:" + i
+ ",replyText:" + s + ",exchange:" + s1 + ",routingKey:" + s2);
} catch (UnsupportedEncodingException e) {
}
}
消费者的具体实现如下:
public void enqueue(SpikeCommodity spikeCommodity){ //收到请求
rabbitTemplate.setConfirmCallback(this); //消息确认
rabbitTemplate.setReturnCallback(this); //消息退回
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
logger.info("======== " + spikeCommodity.getCommodity_id() + " ========");//打印发送的消息
rabbitTemplate.convertAndSend("spikeTopicExchange","spike.commodity", spikeCommodity.toString(),correlationData);
}
②、消费者
消费者相对于生产者来说处理的任务较多,首先要进行手动确认,生产者的自动确认会在消息消息发送给消费者后就确认,但存在消息丢失的可能,如当消费者的消费逻辑出现了异常,也就是没有成功处理消息,相当于丢失了消息;或者是消息成功消费了但是后面的代码出现了问题可能也会造成消息的丢失。
常用的处理如下:
-
消息正常处理没有异常,我们需要手动应答消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
-
消息异常时要对消息进行回滚,有两种方式
//ack返回false,并重新回到队列,api里面解释得很清楚 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); //拒绝消息 channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
-
上面的重新返回到队列中,返回的是队列的头部,这意味着消费者马上会再次接收这条消息,并且再次陷入异常,如此反复,这样会导致消息阻塞和堆积,导致正常消息也无法正常被消费,这种情况的优化方案就是将出现异常的消息重新添加到队列的尾部,这样最起码会先保证其他消息的正常消费,处理如下:
//手动进行应答 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //重新发送消息到队尾 channel.basicPublish(message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), MessageProperties.PERSISTENT_TEXT_PLAIN, JSON.toJSONBytes(new Object()));
上面是手动确认的原因和简单实现,下面介绍消费者做的事情:
- 消费者拿到消息后再次查询redis,保证有库存;
- 预减库存;
- 生成订单并插入;
- 减少数据库中的库存
总结: 上面所述的秒杀实际上是对多用户同时抢购某一商品时使用rabbitmq做了请求的处理,实际上还是顺序处理各个请求的。
推荐阅读:
开发中使用RabbitMQ的手动确认机制
2、死信队列
死信消息:当一个消息变成死信之后,如果这个消息所在的队列有x-dead-letter-exchange
参数,那么这个消息就会被发送到x-dead-letter-exchange所指的交换机,这个交换机就叫做死信交换机,与这个死信交换机绑定的队列叫做死信队列。
一个消息变成死信消息的情况:
- 消息被拒绝,并且requeue被设置为false;
- 消息过期了;
- 队列到达最大长度
引用一张图片说明死信队列的产生过程:
继续上述项目的订单模块:
- 选择购物车中的商品后提交订单,订单id被加入到队列中;
- 生产者的内容如下:
这里设置了10秒的延迟;rabbitTemplate.convertAndSend("order_delay_exchange","order_delay_routing_key",orderId,message -> { // 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间 message.getMessageProperties().setExpiration(1 * 1000*10 + ""); return message; });
- 消费者实现相对简单
Order order = orderService.getOrderById(orderId); if(order.getIsPay() == 0) { logger.info("订单未支付超出支付时间"); orderService.cancleOrder(order.getOrder_id()); commodityService.addCommodityNums(order.getCommodity_id()); } else if(order.getIsPay() == 1) { logger.info("订单已支付"); } else if(order.getIsPay() == 2) { logger.info("订单已取消"); }
下面是关于rabbitmq的死信队列的配置类
@Configuration
public class RabbitMQConfig {
@Bean
public Queue spikeQueue(){
return new Queue("spike_good");
}
@Bean
public TopicExchange spikeTopicExchange(){
return new TopicExchange("spikeTopicExchange");
}
@Bean
public Binding spikeBinding(Queue spikeQueue,TopicExchange spikeTopicExchange){
return BindingBuilder.bind(spikeQueue).to(spikeTopicExchange).with("spike.commodity");
}
private static final String ORDER_DELAY_QUEUE = "order_delay_queue";
private static final String ORDER_DELAY_EXCHANGE = "order_delay_exchange";
private static final String ORDER_DELAY_ROUTING_KEY = "order_delay_routing_key";
private static final String ORDER_QUEUE = "order_queue";
private static final String ORDER_EXCHANGE = "order_exchange";
private static final String ORDER_ROUTING_KEY = "order_routing_key";
//订单交换机
@Bean
public DirectExchange orderDirectExchange() {
return new DirectExchange(ORDER_EXCHANGE);
}
//订单队列
@Bean
public Queue orderQueue() {
return new Queue(ORDER_QUEUE);
}
//
//订单绑定
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderDirectExchange()).with(ORDER_ROUTING_KEY);
}
//订单死信交换机
@Bean
public DirectExchange orderDelayExchange(){
return new DirectExchange(ORDER_DELAY_EXCHANGE);
}
//死信队列
@Bean
public Queue orderDelayQueue(){//延时队列的配置
Map<String, Object> params = new HashMap<>(2);
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,使用order队列原来的exchange
params.put("x-dead-letter-exchange", ORDER_EXCHANGE);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);
return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
}
//死信队列绑定
@Bean
public Binding dlxBinding(){
return BindingBuilder.bind(orderDelayQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
}
}