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
下面是搭建的一个基础使用案例
- 创建一个sp工程,导入rocketmq依赖
<!-- 原生的api-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.2</version>
<!-- docker的用下面这个版本<version>4.4.0</version>–>-->
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
- 编写生产者
/**
* 发消息
*/
@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
- 创建一个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项目只对应一个消费者
- 生产者发送消息
/**
* 同步发送消息
*/
@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消息消费的模式分为两种:负载均衡模式和广播模式
负载均衡模式表示多个消费者交替消费同一个主题里面的消息
广播模式表示每一个消费者都消费一遍订阅的主题消息
下面来测试负载均衡消费模式:
- 创建一个消费者,模拟往队列发送消息
// 重试集群模式-负载均衡模式
public void testCluster(){
for (int i = 1; i <= 10; i++) {
rocketMQTemplate.syncSend("modeTopic","我是第"+i+"个消息");
}
}
- 创建三个消费者
/**
** 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的热点参数限流规则,针对爆款商品和普通商品的区别,区分限制
``