基于Redis和RabbitMQ简单实现秒杀回顾

概述

使用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被加入到队列中;
  • 生产者的内容如下:
    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;
            });
    
    这里设置了10秒的延迟;
  • 消费者实现相对简单
    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);
    }
}
  • 7
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RedisRabbitMQ和MyBatis可以结合使用来实现秒杀功能。 首先,Redis可以用作秒杀的缓存层。当用户请求秒杀商品时,可以先从Redis中查询商品的库存信息。由于Redis的高性能和擅长处理高并发的特性,可以快速返回库存信息,减轻数据库的压力。同时,为了防止超卖现象的发生,在Redis中可以设置一个计数器,记录已经被抢购的商品数量,每次秒杀成功后即使库存减一,确保不会超过实际库存数量。 其次,RabbitMQ可以用来处理秒杀请求的异步处理。当用户发起秒杀请求后,可以将请求消息发送至RabbitMQ中的秒杀队列。然后,消费者可以异步地从队列中获取消息,进行处理。这样可以有效地削峰填谷,降低系统的压力,提高并发处理能力。另外,通过RabbitMQ还可以实现消息的延迟投递功能,可以设置一个定时任务,定时将未处理完的请求重新放入队列中进行处理。 最后,MyBatis可以用来操作数据库,处理秒杀请求的商品库存信息。当消费者从队列中获取到秒杀请求消息后,可以通过MyBatis来更新商品库存信息。MyBatis提供了强大的数据库操作功能,可以方便地将秒杀请求与数据库进行交互。同时,为了防止超卖现象的发生,可以在更新库存信息之前进行乐观锁的检查,确保库存足够的情况下才进行更新操作。 综上所述,使用RedisRabbitMQ和MyBatis可以实现一个高效、可靠的秒杀系统。Redis用于缓存商品库存信息,RabbitMQ用于异步处理秒杀请求,MyBatis用于操作数据库。通过这三个工具的结合利用,可以满足高并发场景下的秒杀需求,提高系统的性能和可伸缩性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值