RocketMQ快速入门

RocketMQ快速入门

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

RocketMQ提供了发送多种发送消息的模式,例如同步消息,异步消息,顺序消息,延迟消息,事务消息等,我们一一学习

1.1 消息发送和监听的流程
我们先搞清楚消息发送和监听的流程,然后我们在开始敲代码

1.创建消息生产者producer,并制定生产者组名
2.指定Nameserver地址
3.启动producer
4.创建消息对象,指定主题Topic、Tag和消息体等
5.发送消息
6.关闭生产者producer
在这里插入图片描述

1.2 消息消费者

1.创建消费者consumer,制定消费者组名
2.指定Nameserver地址
3.创建监听订阅主题Topic和Tag等
4.处理消息
5.启动消费者consumer

下面是搭建的一个基础使用案例

  1. 创建一个sp工程,导入rocketmq依赖
<!-- 原生的api-->
       <dependency>
           <groupId>org.apache.rocketmq</groupId>
           <artifactId>rocketmq-client</artifactId>
           <version>4.9.2</version>
<!--            docker的用下面这个版本<version>4.4.0</version>&ndash;&gt;-->
       </dependency>

       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.12</version>
       </dependency>
  1. 编写生产者
/**
     * 发消息
     */
    @Test
    void contextLoads() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        // 创建一个生产者(定制一个生产者组名)
        DefaultMQProducer producer = new DefaultMQProducer("test-producer-test");
        // 连接nameServer
        producer.setNamesrvAddr("127.0.0.1:9876");

        // 启动
        producer.start();

        // 创建一个消息
        Message message = new Message("testTopic", "我是一个简单的消息".getBytes());
        // 发送消息
        SendResult send = producer.send(message);
        System.out.println(send.getSendStatus());

        // 关闭生产者
        producer.shutdown();

    }

编写消费者:

@Test
       void simpleConsumer() throws MQClientException, IOException {
         // 创建一个消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
         // 连接namesrv
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 订阅一个主题 * 表示订阅这个主题中的所有消息,后期会有消息过滤
        consumer.subscribe("testTopic","*");

        // 设置一个监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                // 这个就是消费的方法(业务处理)
                System.out.println("我是消费者");
                System.out.println("消息的内容是:"+new String(msgs.get(0).getBody()));
                System.out.println("消费上下文:"+consumeConcurrentlyContext);
                // 返回值 CONSUME_SUCCESS成功,消息会从mq出队
                // RECONSUME_LATER (报错/null) 代表失败,消息会重新回到队列,过一会重新投递出来,给当前消费者或者其他消费者消费的

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
             // 启动
            consumer.start();
             // 挂起当前jvm,就是让其不要停止,因为消费逻辑是异步执行的,这里是为了防止主线程执行完而异步线程还没执行完
            System.in.read();

       }

执行效果:

在这里插入图片描述
2. 消费模式

MQ的消费模式可以大致分为两种,一种是推Push,一种是拉Pull。
Push是服务端【MQ】主动推送消息给客户端,优点是及时性较好,但如果客户端没有做好流控,一旦服务端推送大量消息到客户端时,就会导致客户端消息堆积甚至崩溃。
Pull是客户端需要主动到服务端取数据,优点是客户端可以依据自己的消费能力进行消费,但拉取的频率也需要用户自己控制,拉取频繁容易造成服务端和客户端的压力,拉取间隔长又容易造成消费不及时。
Push模式也是基于pull模式的,只能客户端内部封装了api,一般场景下,上游消息生产量小或者均速的时候,选择push模式。在特殊场景下,例如电商大促,抢优惠券等场景可以选择pull模式,rocketmq拉取消息的规则大致如下所示:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3. RocketMQ发送同步消息
上面的快速入门就是发送同步消息,发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式

在这里插入图片描述
4. RocketMQ发送异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知

代码如下:

    @Test
    public void asyncProducer() throws MQClientException, RemotingException, InterruptedException, IOException {
           DefaultMQProducer producer = new DefaultMQProducer();
           producer.setNamesrvAddr("127.0.0.1:9876");
           producer.start();
           Message message = new Message("asyncTopic", "我是一个异步消费者".getBytes());
           producer.send(message, new SendCallback() {
               @Override
               public void onSuccess(SendResult sendResult) {
                   System.out.println("发送成功");
               }

               @Override
               public void onException(Throwable throwable) {
                   System.out.println("发送失败:"+throwable.getMessage().toString());
               }
           });

           System.out.println("主线程先执行");
           System.in.read();

       }

消费者端代码不变:

 @Test
       void simpleConsumer() throws MQClientException, IOException {
         // 创建一个消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
         // 连接namesrv
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 订阅一个主题 * 表示订阅这个主题中的所有消息,后期会有消息过滤
        consumer.subscribe("testTopic","*");

        // 设置一个监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {

            // // 注册一个消费监听 MessageListenerConcurrently是并发消费    _
            //_// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                // 这个就是消费的方法(业务处理) 这里执行消费的代码,默认是多线程执行
                System.out.println("我是消费者");
                System.out.println("消息的内容是:"+new String(msgs.get(0).getBody()));
                System.out.println("消费上下文:"+consumeConcurrentlyContext);
                // 返回值 CONSUME_SUCCESS成功,消息会从mq出队
                // RECONSUME_LATER (报错/null) 代表失败,消息会重新回到队列,过一会重新投递出来,给当前消费者或者其他消费者消费的

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
             // 启动
            consumer.start();
            // 挂起当前jvm,就是让其不要停止,因为消费逻辑是异步执行的,这里是为了防止主线程执行完而异步线程还没执行完
            System.in.read();

       }

5.RocketMQ发送单向消息
这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,例如日志信息的发送

单向生产者代码:

/**
     * 单向消息发送,不需要在乎消息是否发送成功,
     *  这种模式下生产者只需要负责发送数据就行,不需要关注
     *  消息有没有发送成功,例如:日志处理
     * @throws Exception
     */
    @Test
    public void  OneWayProducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("oneway-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        Message message = new Message("oneWayTopic", "日志xxx".getBytes());
        producer.sendOneway(message);
        System.out.println("成功");
        producer.shutdown();
    }

消费者代码和上面一样

6. RocketMQ发送延迟消息
消息放入mq后,过一段时间,才会被监听到,然后消费
比如下订单业务,提交了一个订单就可以发送一个延时消息,30min后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

生产者代码:

/**
     * 延迟消息
     * @throws Exception
     */
    @Test
    public void msproducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("ms-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        Message message = new Message("orderTopicTest", "订单号,座位号".getBytes());
        // 为消息设置过期时间  1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        // 消息的不同level对应不同的时间,因此这里的3对应着10s
        message.setDelayTimeLevel(3);
        producer.send(message);
        System.out.println("发送时间:"+new Date());
        producer.shutdown();

    }

消费者端代码:

    @Test
    public void  msConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ms-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("orderTopicTest","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println("收到了消息"+new Date());
                System.out.println(new String(list.get(0).getBody()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.in.read();
    }

7.RocketMQ发送批量消息

Rocketmq可以一次性发送一组消息,那么这一组消息会被当做一个消息消费

批量消息生产者:

/**
     * 批量发送消息
     */
    @Test
    public void testBatchProducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("batch-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();

        List<Message> msgs = Arrays.asList(
                new Message("batchTopic","我是一组消息的A消息".getBytes()),
                new Message("batchTopic","我是一组消息的B消息".getBytes()),
                new Message("batchTopic","我是一组消息的C消息".getBytes())
        );

        SendResult send = producer.send(msgs);
        System.out.println(send.getSendStatus().toString());
        // 关闭实例
        producer.shutdown();
    }

消费者端代码:

@Test
    public void  msConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("batch-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("batchTopic","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println("收到了消息"+new Date());
                System.out.println(new String(list.get(0).getBody()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.in.read();
    }

8.RocketMQ发送顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为:分区有序或者全局有序
rocketMq的broker的机制,导致了rocketMq会有这个问题
因为一个broker中对应了四个queue

在这里插入图片描述

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

下面用订单进行分区有序的示例。一个订单的顺序流程是:下订单、发短信通知、物流、签收。订单顺序号相同的消息会被先后发送到同一个队列中,消费时,同一个顺序获取到的肯定是同一个队列。

8.1 创建一个消息模型

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MsgModel {

    private String orderSn;
    private Integer userId;
    private String desc;
}

8.2 顺序消息生产者

private List<MsgModel> msgModels = Arrays.asList(
            new MsgModel("qwer",1,"下单"),
            new MsgModel("qwer",1,"短信"),
            new MsgModel("qwer",1,"物流"),
            new MsgModel("zxcv",1,"下单"),
            new MsgModel("zxcv",1,"短信"),
            new MsgModel("zxcv",1,"物流")
    );
    
    /**
     * 顺序发送,这里是让相同订单号的消息进入到同一个队列
     * @throws Exception
     */
    @Test
    public void orderlyProducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("orderly-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();

        // 发送顺序消息,发送时要确保有序,并且要发送到同一个队列里去
        msgModels.forEach(msgModel -> {
            Message message = new Message("orderlyTopic", msgModel.toString().getBytes());
            try {
                producer.send(message, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                         // 在这里选择队列,让相同订单号进入到同一个队列
                        int hashCode = o.toString().hashCode();

                        // 2 % 4 = 2
                        // 3 % 4 = 3   周期性函数,这里得到的结果永远比模数小,保证了不会被越界
                        int i = hashCode % list.size();
                        return list.get(i);
                    }
                },msgModel.getOrderSn());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        producer.shutdown();
        System.out.println("发送完成");

    }

8.3 顺序消费消息

/**
     * 顺序消费
     * @throws Exception
     */
    @Test
    public void  orderlyConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-producer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("orderlyTopic","*");

        // MessageListenerConcurrently 代表并发模式,多线程,默认20个线程  // 重试16次
        //consumer.setConsumeThreadMax();// 这种模式下可以设置最大线程数

        // MessageListenerOrderly 顺序模式,单线程 无线重试Integer.Max_Value
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                System.out.println("线程id:"+Thread.currentThread().getId());
                System.out.println(new String(list.get(0).getBody()));
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }

9.RocketMQ发送带标签的消息,消息过滤
Rocketmq提供消息过滤功能,通过tag或者key进行区分
我们往一个主题里面发送消息的时候,根据业务逻辑,可能需要区分,比如带有tagA标签的被A消费,带有tagB标签的被B消费,还有在事务监听的类里面,只要是事务消息都要走同一个监听,我们也需要通过过滤才区别对待:

在这里插入图片描述

在这里插入图片描述
生产者代码:

@Test
    public void tagProducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("tag-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        Message message = new Message("tagTopic", "vip1", "我是vip1的文章".getBytes());
        Message message2 = new Message("tagTopic", "vip2", "我是vip2的文章".getBytes());
        producer.send(message);
        producer.send(message2);
        System.out.println("发送成功");
        producer.shutdown();


    }

消费者代码:

/**
     * 订阅关系一致性
     * @throws Exception
     */
    @Test
    public void tagConsumer1() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tag-consumer-group-a");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("tagTopic","vip1");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println("我是vip1的消费者,我正在消费消息"+new String(list.get(0).getBody().toString()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }


    @Test
    public void tagConsumer2() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tag-consumer-group-b");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("tagTopic","vip1 || vip2");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println("我是vip2的消费者,我正在消费消息"+new String(list.get(0).getBody().toString()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }

9.1什么时候该用 Topic,什么时候该用 Tag?

总结:不同的业务应该使用不同的Topic如果是相同的业务里面有不同表的表现形式,那么我们要使用tag进行区分
可以从以下几个方面进行判断:
1.消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的 Topic,无法通过 Tag 进行区分。
2.业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分。
3.消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分。
4.消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个 Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的 Topic。
总的来说,针对消息分类,您可以选择创建多个 Topic,或者在同一个 Topic 下创建多个 Tag。但通常情况下,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息,例如全集和子集的关系、流程先后的关系。

10.RocketMQ中消息的Key

在rocketmq中的消息,默认会有一个messageId当做消息的唯一标识,我们也可以给消息携带一个key,用作唯一标识或者业务标识,包括在控制面板查询的时候也可以使用messageId或者key来进行查询

在这里插入图片描述

在这里插入图片描述

带key的消息生产者代码:

/**
     * 业务参数,我们自身要确保唯一性
     * 为了查阅和去重
     * @throws Exception
     */
    @Test
    public void keyProducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("key-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        String key = UUID.randomUUID().toString();
        System.out.println(key);
        Message message = new Message("keyTopic","vip1",key,"我是vip1的文章".getBytes());

        producer.send(message);

        producer.shutdown();

    }

在这里插入图片描述

消费者端代码:

@Test
    public void keyConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("key-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("keyTopic","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = list.get(0);
                System.out.println("我是vip1,我正在消费消息"+list.get(0).getBody());
                System.out.println("我们的业务唯一标识是:"+messageExt.getKeys());
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

            }
        });

    }

11.Rocketmq重复消费问题

BROADCASTING(广播)模式下,所有注册的消费者都会消费,而这些消费者通常是集群部署的一个个微服务,这样就会多台机器重复消费,当然这个是根据需要来选择。
CLUSTERING(负载均衡)模式下,如果一个topic被多个consumerGroup消费,也会重复消费。 即使是在CLUSTERING模式``下,同一个consumerGroup下,一个队列只会分配给一个消费者,看起来好像是不会重复消费。但是,有个特殊情况:一个消费者新上线后,同组的所有消费者要重新负载均衡(反之一个消费者掉线后,也一样)。一个队列所对应的新的消费者要获取之前消费的offset(偏移量,也就是消息消费的点位),此时之前的消费者可能已经消费了一条消息,但是并没有把offset提交给broker,那么新的消费者可能会重新消费一次。虽然orderly模式是前一个消费者先解锁,后一个消费者加锁再消费的模式,比起concurrently要严格了,但是加锁的线程和提交offset的线程不是同一个,所以还是会出现极端情况下的重复消费。
还有在发送批量消息的时候,会被当做一条消息进行处理,那么如果批量消息中有一条业务处理成功,其他失败了,还是会被重新消费一次。
那么如果在CLUSTERING(负载均衡)模式下,并且在同一个消费者组中,不希望一条消息被重复消费,改怎么办呢?我们可以想到去重操作,找到消息唯一的标识,可以是msgId也可以是你自定义的唯一的key,这样就可以去重了

在这里插入图片描述

生产者代码:伪造重复消息:

@Test
    public void rePeatProducer() throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("repeat-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        java.lang.String key = UUID.randomUUID().toString();
        Message message = new Message("tagTopic", null, key,"扣减库存-1".getBytes());
        Message message2 = new Message("tagTopic",null, key,"扣减库存-1".getBytes());
          // 测试发送两个一样的key的消息
        producer.send(message);
        producer.send(message2);
        System.out.println("发送成功");
        producer.shutdown();
    }

消费者端消费幂等性处理:

/**
     * 我们设计一个去重表,对消息的唯一key添加唯一索引
     * 每次消费消息的时候,先插入数据库,如果成功则执行业务逻辑
     * 如果插入失败,则说明消息来过了,直接签收了
     * @throws Exception
     */
    @Test
    public void rePeatConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("repeat-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("keyTopic","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = list.get(0);
                java.lang.String keys = messageExt.getKeys();
                // 插入数据库,因为我们key做了唯一索引
//                int i = jdbcTemplate.update("insert into order_per_log(`type`,`order_dn`,`user`) values(1,keys,'123')");
                // 新增,要么成功,要么报错,修改要么成功,要么返回0,要么报错
                // 如果成功,就处理业务逻辑,如果报错,就不执行
                System.out.println(new String(messageExt.getBody()));

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

            }
        });
        consumer.start();
        System.in.read();

    }

12.RocketMQ重试机制

一般我们的消息在投递过程中出现问题时,比如我们投递到队列失败了,这时候我们可以引入一个重试机制来进行重新投递

/**
     * 生产者发送消息失败进行重试
     * @throws Exception
     */
        @Test
    public void retryProducer() throws Exception{
            DefaultMQProducer producer = new DefaultMQProducer("retry-producer-group");
            producer.setNamesrvAddr("127.0.0.1:9876");
            producer.start();

            // 生产者发送消息 重试次数
            producer.setRetryTimesWhenSendFailed(2);
//            producer.setRetryTimesWhenSendAsyncFailed(2);

            String key = UUID.randomUUID().toString();
            System.out.println(key);
            Message message = new Message("retryTopic","vip1",key,"我是vip1的文章".getBytes());

            producer.send(message);

            producer.shutdown();

        }

在消费者端进行消费时,也可以进行重试:

/**
     * 重试的时间间隔
     * 10s 30s 1m,2m,3m,4m,5m,6m,7m 8m 9m 10m 20m 30m 1h 2h
     * 默认重试16次
     *
     * 1. 能否自定义重试次数
     * 2. 如果重试16次都是失败的?
     *     (并发模式下可以重试16次,顺序模式性下可以重试int的最大值次),如果都失败了,就认为是一个死信消息
     *      则会放置在一个死信主题中,主题的名称:%DLQ%retry-consumer-group,然后在死信队列中进行处理,找人工,电话啥的
     * 3. 当消息失败时该如何处理
     *
     * 重试次数一般五次
      * @throws Exception
     */
    @Test
    public void retryConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("retryTopic","*");
        // 设置重试次数
        consumer.setMaxReconsumeTimes(2);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = list.get(0);
                System.out.println(new Date());
                System.out.println(messageExt.getReconsumeTimes());
                // 业务报错,返回 返回RECONSUME_LATER都会重试
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

            }
        });

    }


    /**
     * 监听死信队列
     * @throws Exception
     */
    @Test
    public void retryDeadConsumer() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-dead-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("%DLQ%retry-consumer-group","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = list.get(0);
                System.out.println(new Date());
                System.out.println("记录到特别的位置,文件 mysql 通知人工处理");
                // 业务报错,返回 返回RECONSUME_LATER都会重试
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

            }
        });

    }


    /**
     * 第二种方案 用法比较多
     * @throws Exception
     */
    @Test
    public void retryConsumer2() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("retryTopic","*");
        // 设置重试次数
        consumer.setMaxReconsumeTimes(2);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = list.get(0);
                System.out.println(new Date());
                 // 业务处理
                try {
                    handDB();
                }catch (Exception e){
                    // 重试
                    int reconsumeTimes = messageExt.getReconsumeTimes();
                    if (reconsumeTimes >= 3){
                        // 不要重试了
                        System.out.println("记录到特别的位置,文件,mysql,通知人工处理");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    }
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;

                }
                System.out.println(messageExt.getReconsumeTimes());
                // 业务报错,返回 返回RECONSUME_LATER都会重试
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

            }
        });

    }
    // 加上事务控制业务,报错回滚
    private void handDB() {
        int i = 10 / 0;
    }

springboot整合rocketmq

  1. 创建一个sp项目,引入maven依赖
<!--引入rocketmq的starter依赖-->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.25</version>
        </dependency>

2.编写配置文件

rocketmq:
  name-server: 192.168.206.186:9876
  producer:
    group: boot-producer-group
# 一个boot项目中可以写很多个消费者,但是一般在开发中,一个boot项目只对应一个消费者
  1. 生产者发送消息
/**
     * 同步发送消息
     */
    @Test
    public void test(){
       rocketMQTemplate.syncSend("bootTestTopic","我是boot的一个消息");
    }

    // 异步发送消息
    public void asynTest(){
        rocketMQTemplate.asyncSend("bootTestTopic", "我是boot的异步消息", new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("成功");
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println("失败"+throwable.getMessage());
            }
        });
    }


    // 单向消息
    public void sendOneWay(){
        rocketMQTemplate.sendOneWay("bootOneWayTopic","单向消息");
    }

    // 延迟消息
    public void delaySendMessage(){
        Message<String> message = MessageBuilder.withPayload("我是一个延迟消息").build();
        // 主题、消息、超时时间,延迟等级
        rocketMQTemplate.syncSend("bootOneWayTopic",message,3000,3);
    }

4.消费消息

import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
// 代表这是一个消费者
@RocketMQMessageListener(topic = "bootTestTopic",consumerGroup = "boot-test-consumer-group")
public class ABootSimpleMsgListener implements RocketMQListener<MessageExt> {


    /**
     * 这个方法就是消费者的方法
     * 如果泛型制定了固定的类型,那么消息体就是我们的参数
     * MessageExt,类型是消息的所有内容
     * ------------------------
     * 没有报错 就签收
     * 如果报错了 就是拒收  就会重试
     *
     */
    @Override
    public void onMessage(MessageExt messageExt) {
        System.out.println(new String(messageExt.getBody()));
    }
}

5.发送顺序消息

// 发送顺序消息 发送者发消息,需要将一组消息 都发在同一个队列中 消费者 需要单线程消费
    public void order(){
        List<MsgModel> msgModels = Arrays.asList(new MsgModel("qwer", 1, "下单"),
                new MsgModel("qwer", 1, "短信"),
                new MsgModel("qwer", 1, "物流"),
                new MsgModel("qwer", 1, "下单"),
                new MsgModel("zxcv", 1, "短信"),
                new MsgModel("zxcv", 1, "物流"));

        msgModels.forEach(msgModel -> {
            // 发送  一般都是以json的方式进行处理
            rocketMQTemplate.syncSendOrderly("bootOrderlyTopic", JSON.toJSONString(msgModel),msgModel.getOrderSn());
        });

    }

消费顺序消息

import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "bootOrderlyTopic",
                         consumerGroup = "boot-orderly-consumer-group",
                         consumeMode = ConsumeMode.ORDERLY,// 顺序消费模式  单线程
                         maxReconsumeTimes = 5 // 消费重试次数
)
public class BOrderlyMsgListener implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        MsgModel msgModel = JSON.parseObject(new String(messageExt.getBody()), MsgModel.class);
        System.out.println(msgModel);
    }
}

6.带标签的消息

// 带消息标签的消息
    @Test
    public void testTag(){
        // topic:tag
        rocketMQTemplate.syncSend("bootTagTopic:tagA","我是一个带tag的消息");
    }

消费带标签的消息

/**
 * 带消费标签的消费者
 */
@Component
@RocketMQMessageListener(topic = "bootTagTopic",
                         consumerGroup = "boot-tag-consumer-group",
                         selectorType = SelectorType.TAG, // tag过滤模式
                         selectorExpression = "tagA || tagB"
)
public class CTagMsgListener implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        MsgModel msgModel = JSON.parseObject(new String(messageExt.getBody()), MsgModel.class);
        System.out.println(msgModel);

    }
}

7.发送带key的消息

// 带key的消息
    public void keyTest(){
          // key是携带写在消息头的
        Message<String> message = MessageBuilder.withPayload("我是一个带key的消息")
                .setHeader(RocketMQHeaders.KEYS, "qwertasdafg")
                .build();
        rocketMQTemplate.syncSend("bootKeyTopic",message);
    }

消费带key的消息

import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
 * 带key的消息
 */
@Component
@RocketMQMessageListener(topic = "bootTagTopic",
        consumerGroup = "boot-tag-consumer-group"
)
public class DKeyMsgListener implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        String keys = messageExt.getKeys();
        System.out.println(keys);
    }
}

RocketMQ消息消费的模式分为两种:负载均衡模式和广播模式
负载均衡模式表示多个消费者交替消费同一个主题里面的消息
广播模式表示每一个消费者都消费一遍订阅的主题消息

下面来测试负载均衡消费模式:

  1. 创建一个消费者,模拟往队列发送消息
 // 重试集群模式-负载均衡模式
    public void testCluster(){
        for (int i = 1; i <= 10; i++) {
            rocketMQTemplate.syncSend("modeTopic","我是第"+i+"个消息");
        }
    }
  1. 创建三个消费者
/**
** CLUSTERING 集群模式下,队列会被消费者均摊,队列数量>= 消费者数量  消息的消费位点mq服务器会记录处理
** BROADCASTING 广播模式下  消息会被每一个消费者都处理一次,mq服务器不会记录消费点位,也不会重试
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
        consumerGroup = "mode-consumer-group-a",
        messageModel = MessageModel.CLUSTERING // 集群模式,会进行负载均衡消费
)
public class DC1 implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        System.out.println("我是mode-consumer-group-a组的第1个消费者:"+message);
    }
}

其他消费者代码如上相同

消费情况如下所示:
在这里插入图片描述

下面测试广播模式
1.参考上面的负载均衡模式生产者代码投递消息:
2.消费者代码,其余消费者代码同下

@Component
@RocketMQMessageListener(topic = "modeTopic",
        consumerGroup = "mode-consumer-group-b",
        messageModel = MessageModel.BROADCASTING // 广播模式
)
public class DC4Fanout implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        System.out.println("我是mode-consumer-group-b组的第一个消费者:"+message);
    }
}

消费结果如下:
在这里插入图片描述
每一个消费者都获取到了消息

下面打开面板:
在这里插入图片描述
这里需要注意:广播模式的消费者位点是没有参考价值的,生产者只负责投递消息,终端不会去管消费者有没有消费成功,无论消费成功或者失败都不管了,也不会进行重试

如何解决消息堆积问题

一般认为单条队列消息差值>=10w 时,算堆积问题

什么情况下会出现堆积
生产者太快了

  • 增加消费者数量,但是消费者数量<=队列数量,适当的设置最大的消费线程数量(根据IO(2n)/CPU(n+1))
  • 动态扩容队列数量,从而增加消费者数量

消费者消费出现问题

  • 排查消费者程序的问题

如何确保消息不丢失?

1.生产者使用同步发送模式 ,收到mq的返回确认以后 顺便往自己的数据库里面写
msgId status(0) time
在这里插入图片描述

2.消费者消费以后 修改数据这条消息的状态 = 1
在这里插入图片描述
在这里插入图片描述

3.写一个定时任务 间隔两天去查询数据 如果有status = 0 and time < day-2

4.将mq的刷盘机制设置为同步刷盘

5.使用集群模式 ,搞主备模式,将消息持久化在不同的硬件上

6.可以开启mq的trace机制,消息跟踪机制

    1.在broker.conf中开启消息追踪
      traceTopicEnable=true
    2.重启broker即可
    3.生产者配置文件开启消息轨迹
      enable-msg-trace: true
    4.消费者开启消息轨迹功能,可以给单独的某一个消费者开启
      enableMsgTrace = true
      在rocketmq的面板中可以查看消息轨迹
      
    默认会将消息轨迹的数据存在 RMQ_SYS_TRACE_TOPIC 主题里面

安全
1.开启acl的控制 在broker.conf中开启aclEnable=true
2.配置账号密码 修改plain_acl.yml
3.修改控制面板的配置文件 放开52/53行 把49行改为true 上传到服务器的jar包平级目录下即可

秒杀
在这里插入图片描述
在这里插入图片描述

1.1技术选型

  • Springboot +接收请求并操作redis和mysql
  • Redis 用于缓存+分布式锁
  • Rocketmq 用于解耦 削峰,异步
  • Mysql 用于存放真实的商品信息
  • Mybatis 用于操作数据库的orm框架

1.2系统架构图:
在这里插入图片描述

1.3 准备工作-数据库
在这里插入图片描述

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `price` decimal(10, 2) NULL DEFAULT NULL,
  `stocks` int(255) NULL DEFAULT NULL,
  `status` int(255) NULL DEFAULT NULL,
  `pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '小米12s', 4999.00, 1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES (2, '华为mate50', 6999.00, 10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES (3, '锤子pro2', 1999.00, 100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');

-- ----------------------------
-- Table structure for order_records
-- ----------------------------
DROP TABLE IF EXISTS `order_records`;
CREATE TABLE `order_records`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NULL DEFAULT NULL,
  `order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `goods_id` int(11) NULL DEFAULT NULL,
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

1.4 创建项目选择依赖spike-web(接受用户秒杀请求)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.powernode</groupId>
    <artifactId>spike-web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spike-web</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- rocketmq的依赖 -->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.14</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

修改配置文件

server:
  port: 7001
spring:
  application:
    name: spike-web
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    lettuce:
    pool:
        enabled: true
        max-active: 100
        max-idle: 20
        min-idle: 5
rocketmq:
  name-server: 192.168.188.129:9876     # rocketMq的nameServer地址
  producer:
    group: powernode-group        # 生产者组别
    send-message-timeout: 3000  # 消息发送的超时时间
    retry-times-when-send-async-failed: 2  # 异步消息发送失败重试次数
    max-message-size: 4194304       # 消息的最大长度

秒杀接口

import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: DLJD
 * @Date: 2023/4/24
 */
@RestController
public class SeckillController {


    @Autowired
    private StringRedisTemplate redisTemplate;

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    //CAS java无锁的   原子性 安全的
    AtomicInteger userIdAt = new AtomicInteger(0);

    /**
     * 1.用户去重
     * 2.库存的预扣减
     * 3.消息放入mq
     * 秒杀不是一个单独的系统
     * 都是大项目的某一个小的功能模块
     *
     * @param goodsId
     * @param userId  真实的项目中 要做登录的 不要穿这个参数
     * @return
     */
    @GetMapping("seckill")
    public String doSecKill(Integer goodsId /*, Integer userId*/) {
        // log 2023-4-24 16:58:11
        // log 2023-4-24 16:58:11
        int userId = userIdAt.incrementAndGet();
        // uk uniqueKey = [yyyyMMdd] +  userId + goodsId
        String uk = userId + "-" + goodsId;
        // setIfAbsent = setnx
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("uk:" + uk, "");
        if (!flag) {
            return "您已经参与过该商品的抢购,请参与其他商品O(∩_∩)O~";
        }
        // 记住 先查再改 再更新  不安全的操作
        Long count = redisTemplate.opsForValue().decrement("goodsId:" + goodsId);
        if (count < 0) {
            // 保证我的redis的库存 最小值是0
            redisTemplate.opsForValue().increment("goodsId:" + goodsId);
            return "该商品已经被抢完,下次早点来(●ˇ∀ˇ●)";
        }
        // 方mq 异步处理
        rocketMQTemplate.asyncSend("seckillTopic3", uk, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("发送成功");
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println("发送失败:" + throwable.getMessage());
                System.out.println("用户的id:" + userId + "商品id" + goodsId);
            }
        });
        return "正在拼命抢购中,请稍后去订单中心查看";
    }


    /**
     * 抢一个付费的商品
     * 1.先扣减库存  再付费  | 如果不付费 库存需要回滚
     * 2.先付费  再扣减库存  | 如果库存不足  则退费
     */

}

定时任务同步数据

/**
 * @Author: DLJD
 * @Date: 2023/4/24
 * 1.每天10点 晚上8点 通过定时任务 将mysql的库存 同步到redis中去
 * 2.为了测试方便 希望项目启动的时候 就同步数据
 */
@Component
public class DataSync {

    @Autowired
    private GoodsMapper goodsMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

//    @Scheduled(cron = "0 0 10 0 0 ?")
//    public void initData(){
//    }

    /**
     * 我希望这个方法再项目启动以后
     * 并且再这个类的属性注入完毕以后执行
     * bean生命周期了
     * 实例化 new
     * 属性赋值
     * 初始化  (前PostConstruct/中InitializingBean/后BeanPostProcessor)
     * 使用
     * 销毁
     * ----------
     * 定位不一样
     */
    @PostConstruct
    public void initData() {
        List<Goods> goodsList = goodsMapper.selectSeckillGoods();
        if (CollectionUtils.isEmpty(goodsList)) {
            return;
        }
        goodsList.forEach(goods -> {
            redisTemplate.opsForValue().set("goodsId:" + goods.getGoodsId(), goods.getTotalStocks().toString());
        });
    }
}
/**
 * @Author: DLJD
 * @Date:   2023/4/24
 */
/**
    * 商品
    */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Goods implements Serializable {
    /**
    * 商品ID
    */
    private Integer goodsId;

    /**
    * 商品名称
    */
    private String goodsName;

    /**
    * 现价
    */
    private BigDecimal price;

    /**
    * 详细描述
    */
    private String content;

    /**
    * 默认是1,表示正常状态, -1表示删除, 0下架
    */
    private Integer status;

    /**
    * 总库存
    */
    private Integer totalStocks;

    /**
    * 录入时间
    */
    private Date createTime;

    /**
    * 修改时间
    */
    private Date updateTime;

    /**
    * 是否参与秒杀1是0否
    */
    private Integer spike;

    private static final long serialVersionUID = 1L;
}
/**
 * @Author: DLJD
 * @Date:   2023/4/24
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Order implements Serializable {
    private Integer id;

    private Integer userid;

    private Integer goodsid;

    private Date createtime;

    private static final long serialVersionUID = 1L;
}

监听器消费者

/**
 * @Author: DLJD
 * @Date: 2023/4/24
 */
@Component
@RocketMQMessageListener(topic = "seckillTopic3",
        consumerGroup = "seckill-consumer-group3",
        consumeMode = ConsumeMode.CONCURRENTLY,
        consumeThreadNumber = 40
)
public class SeckillListener implements RocketMQListener<MessageExt> {

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    int ZX_TIME = 20000;

    /**
     * 扣减库存
     * 写订单表
     *
     * @param message
     */
//    @Override
//    public void onMessage(MessageExt message) {
//        String msg = new String(message.getBody());
//        // userId + "-" + goodsId
//        Integer userId = Integer.parseInt(msg.split("-")[0]);
//        Integer goodsId = Integer.parseInt(msg.split("-")[1]);
//        // 方案一: 再事务外面加锁 可以实现安全 没法集群
    // jvm  EntrySet WaitSet
//        synchronized (this) {
//            goodsService.realSeckill(userId, goodsId);
//        }
//    }


    // 方案二  分布式锁  mysql(行锁)   不适合并发较大场景
//    @Override
//    public void onMessage(MessageExt message) {
//        String msg = new String(message.getBody());
//        // userId + "-" + goodsId
//        Integer userId = Integer.parseInt(msg.split("-")[0]);
//        Integer goodsId = Integer.parseInt(msg.split("-")[1]);
//        goodsService.realSeckill(userId, goodsId);
//    }

    // 方案三: redis setnx 分布式锁  压力会分摊到redis和程序中执行  缓解db的压力
    @Override
    public void onMessage(MessageExt message) {
        String msg = new String(message.getBody());
        Integer userId = Integer.parseInt(msg.split("-")[0]);
        Integer goodsId = Integer.parseInt(msg.split("-")[1]);
        int currentThreadTime = 0;
        while (true) {
            // 这里给一个key的过期时间,可以避免死锁的发生
            Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "", Duration.ofSeconds(30));
            if (flag) {
                // 拿到锁成功
                try {
                    goodsService.realSeckill(userId, goodsId);
                    return;
                } finally {
                    // 删除
                    redisTemplate.delete("lock:" + goodsId);
                }
            } else {
                currentThreadTime += 200;
                try {
                    Thread.sleep(200L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
/**
 * @Author: DLJD
 * @Date: 2023/4/24
 */
@Service
public class GoodsServiceImpl implements GoodsService {

    @Resource
    private GoodsMapper goodsMapper;

    @Autowired
    private OrderMapper orderMapper;

    @Override
    public int deleteByPrimaryKey(Integer goodsId) {
        return goodsMapper.deleteByPrimaryKey(goodsId);
    }

    @Override
    public int insert(Goods record) {
        return goodsMapper.insert(record);
    }

    @Override
    public int insertSelective(Goods record) {
        return goodsMapper.insertSelective(record);
    }

    @Override
    public Goods selectByPrimaryKey(Integer goodsId) {
        return goodsMapper.selectByPrimaryKey(goodsId);
    }

    @Override
    public int updateByPrimaryKeySelective(Goods record) {
        return goodsMapper.updateByPrimaryKeySelective(record);
    }

    @Override
    public int updateByPrimaryKey(Goods record) {
        return goodsMapper.updateByPrimaryKey(record);
    }

    /

    /**
     *
     *  常规的 方案
     *  锁加载调用方法的地方 要加载事务外面
     * @param userId
     * @param goodsId
     */
//    @Override
//    @Transactional(rollbackFor = Exception.class) // rr
//    public void realSeckill(Integer userId, Integer goodsId) {
//        // 扣减库存  插入订单表
//        Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
//        int finalStock = goods.getTotalStocks() - 1;
//        if (finalStock < 0) {
//            // 只是记录日志 让代码停下来   这里的异常用户无法感知
//            throw new RuntimeException("库存不足:" + goodsId);
//        }
//        goods.setTotalStocks(finalStock);
//        goods.setUpdateTime(new Date());
//        // update goods set stocks =  1 where id = 1  没有行锁
//        int i = goodsMapper.updateByPrimaryKey(goods);
//        if (i > 0) {
//            Order order = new Order();
//            order.setGoodsid(goodsId);
//            order.setUserid(userId);
//            order.setCreatetime(new Date());
//            orderMapper.insert(order);
//        }
//    }//


    /**
     * 行锁(innodb)方案 mysql  不适合用于并发量特别大的场景
     * 因为压力最终都在数据库承担
     *
     * @param userId
     * @param goodsId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void realSeckill(Integer userId, Integer goodsId) {
        // update goods set total_stocks = total_stocks - 1 where goods_id = goodsId and total_stocks - 1 >= 0;
        // 通过mysql来控制锁
        int i = goodsMapper.updateStock(goodsId);
        if (i > 0) {
            Order order = new Order();
            order.setGoodsid(goodsId);
            order.setUserid(userId);
            order.setCreatetime(new Date());
            orderMapper.insert(order);
        }
    }

}

public interface GoodsMapper {
    int deleteByPrimaryKey(Integer goodsId);

    int insert(Goods record);

    int insertSelective(Goods record);

    Goods selectByPrimaryKey(Integer goodsId);

    int updateByPrimaryKeySelective(Goods record);

    int updateByPrimaryKey(Goods record);

    List<Goods> selectSeckillGoods();

    int updateStock(Integer goodsId);
}


public interface OrderMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Order record);

    int insertSelective(Order record);

    Order selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Order record);

    int updateByPrimaryKey(Order record);
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.mapper.GoodsMapper">
  <resultMap id="BaseResultMap" type="com.powernode.domain.Goods">
    <!--@mbg.generated-->
    <!--@Table goods-->
    <id column="goods_id" jdbcType="INTEGER" property="goodsId" />
    <result column="goods_name" jdbcType="VARCHAR" property="goodsName" />
    <result column="price" jdbcType="DECIMAL" property="price" />
    <result column="content" jdbcType="LONGVARCHAR" property="content" />
    <result column="status" jdbcType="INTEGER" property="status" />
    <result column="total_stocks" jdbcType="INTEGER" property="totalStocks" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
    <result column="spike" jdbcType="INTEGER" property="spike" />
  </resultMap>
  <sql id="Base_Column_List">
    <!--@mbg.generated-->
    goods_id, goods_name, price, content, `status`, total_stocks, create_time, update_time, 
    spike
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    <!--@mbg.generated-->
    select 
    <include refid="Base_Column_List" />
    from goods
    where goods_id = #{goodsId,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    <!--@mbg.generated-->
    delete from goods
    where goods_id = #{goodsId,jdbcType=INTEGER}
  </delete>
  <insert id="insert" keyColumn="goods_id" keyProperty="goodsId" parameterType="com.powernode.domain.Goods" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into goods (goods_name, price, content, 
      `status`, total_stocks, create_time, 
      update_time, spike)
    values (#{goodsName,jdbcType=VARCHAR}, #{price,jdbcType=DECIMAL}, #{content,jdbcType=LONGVARCHAR}, 
      #{status,jdbcType=INTEGER}, #{totalStocks,jdbcType=INTEGER}, #{createTime,jdbcType=TIMESTAMP}, 
      #{updateTime,jdbcType=TIMESTAMP}, #{spike,jdbcType=INTEGER})
  </insert>
  <insert id="insertSelective" keyColumn="goods_id" keyProperty="goodsId" parameterType="com.powernode.domain.Goods" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into goods
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="goodsName != null">
        goods_name,
      </if>
      <if test="price != null">
        price,
      </if>
      <if test="content != null">
        content,
      </if>
      <if test="status != null">
        `status`,
      </if>
      <if test="totalStocks != null">
        total_stocks,
      </if>
      <if test="createTime != null">
        create_time,
      </if>
      <if test="updateTime != null">
        update_time,
      </if>
      <if test="spike != null">
        spike,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="goodsName != null">
        #{goodsName,jdbcType=VARCHAR},
      </if>
      <if test="price != null">
        #{price,jdbcType=DECIMAL},
      </if>
      <if test="content != null">
        #{content,jdbcType=LONGVARCHAR},
      </if>
      <if test="status != null">
        #{status,jdbcType=INTEGER},
      </if>
      <if test="totalStocks != null">
        #{totalStocks,jdbcType=INTEGER},
      </if>
      <if test="createTime != null">
        #{createTime,jdbcType=TIMESTAMP},
      </if>
      <if test="updateTime != null">
        #{updateTime,jdbcType=TIMESTAMP},
      </if>
      <if test="spike != null">
        #{spike,jdbcType=INTEGER},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.powernode.domain.Goods">
    <!--@mbg.generated-->
    update goods
    <set>
      <if test="goodsName != null">
        goods_name = #{goodsName,jdbcType=VARCHAR},
      </if>
      <if test="price != null">
        price = #{price,jdbcType=DECIMAL},
      </if>
      <if test="content != null">
        content = #{content,jdbcType=LONGVARCHAR},
      </if>
      <if test="status != null">
        `status` = #{status,jdbcType=INTEGER},
      </if>
      <if test="totalStocks != null">
        total_stocks = #{totalStocks,jdbcType=INTEGER},
      </if>
      <if test="createTime != null">
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
      <if test="updateTime != null">
        update_time = #{updateTime,jdbcType=TIMESTAMP},
      </if>
      <if test="spike != null">
        spike = #{spike,jdbcType=INTEGER},
      </if>
    </set>
    where goods_id = #{goodsId,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.powernode.domain.Goods">
    <!--@mbg.generated-->
    update goods
    set goods_name = #{goodsName,jdbcType=VARCHAR},
      price = #{price,jdbcType=DECIMAL},
      content = #{content,jdbcType=LONGVARCHAR},
      `status` = #{status,jdbcType=INTEGER},
      total_stocks = #{totalStocks,jdbcType=INTEGER},
      create_time = #{createTime,jdbcType=TIMESTAMP},
      update_time = #{updateTime,jdbcType=TIMESTAMP},
      spike = #{spike,jdbcType=INTEGER}
    where goods_id = #{goodsId,jdbcType=INTEGER}
  </update>

  <select id="selectSeckillGoods" resultMap="BaseResultMap">
    select goods_id,total_stocks from   goods where `status` = 1 and spike = 1
    </select>

  <update id="updateStock">
    update  goods set total_stocks = total_stocks - 1 ,update_time = now() where goods_id = #{value} and total_stocks - 1 >= 0

  </update>
</mapper>







<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.mapper.OrderMapper">
  <resultMap id="BaseResultMap" type="com.powernode.domain.Order">
    <!--@mbg.generated-->
    <!--@Table `order`-->
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="userid" jdbcType="INTEGER" property="userid" />
    <result column="goodsid" jdbcType="INTEGER" property="goodsid" />
    <result column="createtime" jdbcType="TIMESTAMP" property="createtime" />
  </resultMap>
  <sql id="Base_Column_List">
    <!--@mbg.generated-->
    id, userid, goodsid, createtime
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    <!--@mbg.generated-->
    select 
    <include refid="Base_Column_List" />
    from `order`
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    <!--@mbg.generated-->
    delete from `order`
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.powernode.domain.Order" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into `order` (userid, goodsid, createtime
      )
    values (#{userid,jdbcType=INTEGER}, #{goodsid,jdbcType=INTEGER}, #{createtime,jdbcType=TIMESTAMP}
      )
  </insert>
  <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.powernode.domain.Order" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into `order`
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="userid != null">
        userid,
      </if>
      <if test="goodsid != null">
        goodsid,
      </if>
      <if test="createtime != null">
        createtime,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="userid != null">
        #{userid,jdbcType=INTEGER},
      </if>
      <if test="goodsid != null">
        #{goodsid,jdbcType=INTEGER},
      </if>
      <if test="createtime != null">
        #{createtime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.powernode.domain.Order">
    <!--@mbg.generated-->
    update `order`
    <set>
      <if test="userid != null">
        userid = #{userid,jdbcType=INTEGER},
      </if>
      <if test="goodsid != null">
        goodsid = #{goodsid,jdbcType=INTEGER},
      </if>
      <if test="createtime != null">
        createtime = #{createtime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.powernode.domain.Order">
    <!--@mbg.generated-->
    update `order`
    set userid = #{userid,jdbcType=INTEGER},
      goodsid = #{goodsid,jdbcType=INTEGER},
      createtime = #{createtime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
技术选型:springBoot + Redis + Mysql + RocketMq + Security ...

设计: (抢优惠券..)
设计seckill-web接收处理秒杀请求
设计seckill-service处理秒杀真实业务的

部署细节:
用户量: 50w
日活量: 1w-2w   1%-5%
qps: 2w+   [自己打日志 | nginx(access.log) ]
几台服务器(什么配置):8C16G  4台    seckill-web : 4台    seckill-service 2台
带宽: 100M

技术要点:
1.通过redis的setnx对用户和商品做去重判断,防止用户刷接口的行为
2.每天晚上8点通过定时任务 把mysql中参与秒杀的库存商品,同步到redis中去,做库存的预扣减,提升接口性能
3.通过RocketMq消息中间件的异步消息,来将秒杀的业务异步化,进一步提升性能
4.seckill-service使用并发消费模式,并且设置合理的线程数量,快速处理队列中堆积的消息
5.使用redis的分布式锁+自旋锁,对商品的库存进行并发控制,把并发压力转移到程序中和redis中去,减少db压力
6.使用声明式事务注解Transactional,并且设置异常回滚类型,控制数据库的原子性操作
7.使用jmeter压测工具,对秒杀接口进行压力测试,在8C16G的服务器上,qps2k+,达到压测预期
8.使用sentinel的热点参数限流规则,针对爆款商品和普通商品的区别,区分限制

``
  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值