预设场景:
“秒杀”这一词多半出现在购物方面,但是又不单单只是购物,比如12306购票和学校抢课(大学生的痛苦)也可以看成一个秒杀。秒杀应该是一个“三高”,这个三高不是指高血脂,高血压和高血糖。而是指“高并发”,“高性能”和“高可用”。
假设有一百个库存商品需要抢购,可以试用mq进行削峰,防止宕机。
1.创建rocketmq server模块。
1.1. 配置相关文件
springboot2.6.13
版本
xml
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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.3</version>
</dependency>
</dependencies>
application.properties
相关配置
#应用名
spring.application.name=seckill-server
server.port=8081
rocketmq.producer.groupName=${spring.application.name}
# mq的nameserver地址
rocketmq.producer.namesrvAddr=127.0.0.1:9876
1.2. controller代码
- 这里我使用1000冗余数量,控制接口访问数,正常来讲,应该使用中间件去同步真实库存,我这里省略了。
- 我这里的业务逻辑比较简单,可以根据自身需要更改逻辑。
@RestController
public class OpenOrderController{
int redundancy = 1000;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@GetMapping("/secKill")
public String secKill(String id){
redundancy--;
if(redundancy > 0){
rocketMQTemplate.convertAndSend("seckill-topic", id);
return id+"正在抢购中请等待";
}else{
return "商品已售完";
}
}
}
2.创建rocketmq-consumer模块
1.1. 配置相关文件
- xml依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
application.properties
相关配置
spring.application.name=seckill-consumer
server.port=8082
rocketmq.consumer.group=${spring.application.name}
rocketmq.name-server=127.0.0.1:9876
1.2. controller代码
这里是真实的消息处理,springboot的监听处理极其简化了监听器的配置过程。
这里吧库存设置成一个简单的成员变量
,实际上在分布式项目中可能使用redis
同步真实库存。
在真实的场景中我们可以在这一步进行鉴权,是不是目标用户(黑户),生成订单等,发送短信(回调执行结果)等操作。由于已经由MQ进行了流量削峰,这一步可以进行更多的操作,有条不紊的进行业务逻辑的执行,
下面是示例代码:
@Component
@RocketMQMessageListener(topic = "seckill-topic", consumerGroup = "seckill-consumer-group")
public class SeckillConsumer implements RocketMQListener<String> {
int realInventory = 100;
@Override
public void onMessage(String id) {
// 处理秒杀请求
// 执行库存扣减和订单生成等操作
// 返回秒杀结果给用户
realInventory--;
if(realInventory >= 0){
System.out.println("当前商品剩余"+realInventory);
System.out.println(id + "抢到商品");
}else{
System.out.println("商品已售完");
}
}
}
3.创建测试示例
- 使用1w线程发送1w请求进行接口测试。
public class HttpTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 10000; i++) {
executorService.execute(() -> {
try {
URL url = new URL("http://localhost:8081/secKill?id=" + UUID.randomUUID().toString());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
System.out.println("Request success!");
} else {
System.out.println("Request failed!");
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
- 测试结果
测试代码控制台
生产者控制台
消费者控制台
这里抛出一个问题?
为什么会出现消息的乱序消费呢?如何实现顺序消费呢?
答:springboot默认是异步多线程消费的,无法保证顺序。
consumeMode = ConsumeMode.ORDERLY
ConsumeMode.ORDERLY的作用是让消费者单线程顺序接收消息,从而保证消息的全局顺序