1、什么是MQ
mq是消息中间件,最简单的理解就是A服务发送一条消息给B服务,B服务收到消息后做一系列的动作
这个能解决什么问题呢
- 削峰,服务之间如果通过接口调用的话,无论是http还是rpc,但是属于同步的动作,意味着会存在大量的数据同一时刻到被调用服务,而使用mq完美解决该问题
- 解耦,服务之间数据交互不再通过mysql,redis等,实现服务之间的解耦
2、什么情况会用到mq呢?
服务之间有数据交互时,但不要求时效性
比如:
- 电商场景中,订单系统生成订单后会通知仓库生成出库单,会通知配送系统生成配送单,而这种场景为什么不用rpc或者http接口呢,因为http接口会存在调用失败导致数据丢失,rpc也存在同样的问题,第2个原因,某一时刻大量订单生成时,极有可能导致仓库服务或者配送服务被打挂。
- 日志服务,线上多台服务器会把日志保存到统一日志服务上,方便查看线上日志,这种场景也是非常适合使用mq
3、为什么测试同学需要掌握这个技能
在服务拆分的现在,大量的微服务场景出现,大量使用mq的场景,如果不进行相应的专项测试,大概率会出现线上问题。
4、mq的架构是什么样的?
生产者发送消息给topic(broker),broker再把消息发送给消费者 每个消费者都有1个offset(偏移位),作用是记录这个消息是否被消费过,防止重复消费
最重要的是幂等问题,当网络出现问题的时,broker同样的消息可能会给消费者发送多次
5、如何搭建本地rocketMQ
1、下载rocketMQ并解压 rocketmq.apache.org/release_not…
2、启动nameserver
sh mqnamesrv
3、启动broker
sh bin/mqbroker -n localhost:9876
4、使用自带工具测试
export NAMESRV_ADDR=localhost:9876
sh tools.sh org.apache.rocketmq.example.quickstart.Producer
SendResult [sendStatus=SEND_OK, msgId=C0A80064502A355DA2545C7FD53503E5, offsetMsgId=C0A8006400002A9F000000000002BC96, messageQueue=MessageQueue [topic=TopicTest, brokerName=taomindeMacBook-Pro-2.local, queueId=1], queueOffset=249]
SendResult [sendStatus=SEND_OK, msgId=C0A80064502A355DA2545C7FD53603E6, offsetMsgId=C0A8006400002A9F000000000002BD4A, messageQueue=MessageQueue [topic=TopicTest, brokerName=taomindeMacBook-Pro-2.local, queueId=2], queueOffset=249]
到目前位置,本地的rocketMQ环境已经搭建好了,接下来是怎么使用spring-boot进行rocketMQ接入
5、使用命令行创建topic和消费组
创建topic
sh mqadmin updateTopic -n 192.168.0.103:9876 -b 192.168.0.103:10911 -t message-topic
创建消费组
sh mqadmin updateSubGroup -c DefaultCluster -n 127.0.0.1:9876 -g nexcusdemo
6、在项目中引入依赖(pom.xml)
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-common</artifactId>
<version>4.5.1</version>
</dependency>
7、在消费者中配置(application.properties)
rocketmq.name-server=localhost:9876
8、生产者配置
# rocketMQ配置
rocketmq.name-server=127.0.0.1:9876
rocketmq.producer.group=test
9、生产者代码
@GetMapping("/send")
public String sendMessage(String content){
mqService.sendHello(content);
return "ok";
}
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private RocketMQProducer<String> stringRocketMQProducer;
public void sendHello(String message){
UserInfoDto userInfoDto = new UserInfoDto();
userInfoDto.setPassword("123");
userInfoDto.setUsername(message);
userInfoDto.setId(1L);
rocketMQTemplate.convertAndSend("message-topic", userInfoDto);
}
10、消费者配置
rocketmq.name-server=127.0.0.1:9876
11、消费者代码
@Log4j2
@Component
@RocketMQMessageListener(topic = "message-topic", consumerGroup = "nexcusdemo")
public class MessageListener implements RocketMQListener<UserInfoDto> {
@Override
public void onMessage(UserInfoDto message) {
log.info("接收到消息: {}", message);
}
}
到这里,通过Java来调用rocketMQ发送消息的代码和配置已经完成,进行结果演示
12、通过postman演示调用A服务接口,然后在B服务上查看到日志打印
- 通过postman调用
-
查看A服务日志,其中有发送消息的日志
-
查看B服务日志,其中有接收到消息的日志
6、python如何调用rocketMQ进行自动化测试
- 安装rocketMQ的C++依赖 macos:
wget https://github.com/apache/rocketmq-client-cpp/releases/download/2.0.0/rocketmq-client-cpp-2.0.0-bin-release.darwin.tar.gz
tar -xzf rocketmq-client-cpp-2.0.0-bin-release.darwin.tar.gz
cd rocketmq-client-cpp
mkdir /usr/local/include/rocketmq
cp include/* /usr/local/include/rocketmq
cp lib/* /usr/local/lib
install_name_tool -id "@rpath/librocketmq.dylib" /usr/local/lib/librocketmq.dylib
centos:
wget https://github.com/apache/rocketmq-client-cpp/releases/download/2.0.0/rocketmq-client-cpp-2.0.0-centos7.x86_64.rpm
sudo rpm -ivh rocketmq-client-cpp-2.0.0-centos7.x86_64.rpm
- 安装python依赖
pip install rocketmq-client-python
- 作为生产者
from rocketmq.client import Producer, Message
import json
producer = Producer("test")
producer.set_name_server_address('127.0.0.1:9876')
producer.start()
msg = Message('message-topic')
# msg.set_keys('XXX')
# msg.set_tags('XXX')
msg.set_body(json.dumps({"username":"python", "password":"python3", "id":2}))
ret = producer.send_sync(msg)
print(ret.status, ret.msg_id, ret.offset)
producer.shutdown()
- 执行生产者代码,查看Java消费者是否收到
- 作为消费者
import time
from rocketmq.client import PushConsumer, ConsumeStatus
def callback(msg):
print(msg.id, msg.body)
return ConsumeStatus.CONSUME_SUCCESS
consumer = PushConsumer('nexcusdemo')
consumer.set_name_server_address('127.0.0.1:9876')
consumer.subscribe('message-topic', callback)
consumer.start()
while True:
time.sleep(3600)
consumer.shutdown()
- 查看通过POST发送请求后是否能够接收到消息
7、常见的问题,幂等测试
- 场景如下
用户调用A服务订单接口,A服务会发送MQ消息给B服务,生成1个配送单
存在的问题是:如果A服务发送消息的时候,因为网络抖动等问题,broker会给B服务发送重复消息,导致生成2个配送单的问题,1个订单应该有几个配送单
- A服务(订单服务)在生成1个订单后会发送mq消息给B服务(配送服务)生成配送单 查看A服务代码
//生成订单接口
@PostMapping("/order/create")
public Result<String> createOrder(@RequestBody OrderQuery orderQuery){
// 生成订单,并发送消息给配送服务生成配送单
mqService.createOrder(orderQuery);
return new Result<String>().success("ok");
}
// 生成订单方法,并发送消息给配送服务
public boolean createOrder(OrderQuery orderQuery){
HcOrder hcOrder = new HcOrder();
hcOrder.setSku(orderQuery.getSku());
hcOrder.setOrderNo(OrderUtil.getLeafNo());
hcOrder.setQuantity(String.valueOf(orderQuery.getQuantity()));
// 订单数据落库
hcOrderMapper.insert(hcOrder);
// 发送消息给配送服务
sendOrder2Transport(hcOrder);
return true;
}
// 发送消息给配送服务
public void sendOrder2Transport(HcOrder hcOrder){
String order = JSONObject.toJSONString(hcOrder);
rocketMQTemplate.convertAndSend("order-topic", order);
}
那么我们可以确定A服务发送消息给B服务,实际发送的就是1个json,这个json就是hcOrder对象,实际就是
{
"sku": "1", //表示是哪个商品
"orderNo": "hc1111", //表示哪个订单号
"quantity": "2" // 表示买了几件
}
- 查看B服务代码
@Override
public void onMessage(String order) {
log.info("接收到消息: {}", order);
HcOrder hcOrder = JSONUtil.convertString2Object(order, HcOrder.class);
String orderNo = hcOrder.getOrderNo();
// 幂等处理,先查询,如果有就直接返回(如果不处理,就有可能出现1个订单对应对个配送单)
LambdaQueryWrapper<DeliveryOrder> deliveryOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
deliveryOrderLambdaQueryWrapper.eq(DeliveryOrder::getOrderNo, orderNo);
DeliveryOrder deliveryOrder1 = deliveryMapper.selectOne(deliveryOrderLambdaQueryWrapper);
if (deliveryOrder1 != null){
log.info("orderNo: {},对应的配送单: {}, 已存在", orderNo, deliveryOrder1.getDeliveryNo());
return;
}
DeliveryOrder deliveryOrder = new DeliveryOrder();
deliveryOrder.setOrderNo(orderNo);
deliveryOrder.setDeliveryNo(DeliveryOrderUtil.getLeafNo());
deliveryMapper.insert(deliveryOrder);
}
- 如何用配送验证是否做了幂等 首先,查看配送单表中存在hc1624011377626对应的配送单
如果我们用python再发送一条消息给B服务,查看B服务是否继续往数据库里在插入一条数据就能判断是否做了幂等
from rocketmq.client import Message, Producer
import json
producer = Producer("test")
producer.set_name_server_address("127.0.0.1:9876")
producer.start()
msg = Message("order-topic")
msg.set_body(json.dumps({"orderNo":"hc1624011377626", "sku":"1", "quantity":"2"}))
res = producer.send_sync(msg)
print(res.status, res.msg_id, res.offset)
producer.shutdown()
- 执行python脚本查看日志和数据库,日志打印数据重复,订单对应的配送单也只有1个,最终证明幂等生效
8、写在最后
MQ消息中间件是微服务测试中很小的一部分,后续还会有各种微服务测试开发需要掌握的技能,大家有什么问题可以在下面评论