预设场景:
“秒杀”这一词多半出现在购物方面,但是又不单单只是购物,比如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;@Autowiredprivate 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;@Overridepublic 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的作用是让消费者单线程顺序接收消息,从而保证消息的全局顺序