第六章 Rocketmq-消息驱动
代码地址:源码地址
1. MQ简介
MQ(Message Queue)是一种跨进程的通信机制,用于传递消息。通俗点说,就是一个先进先出的数据结构。
本来在一个微服务中需要调用另一个微服务,使用MQ之后第一个微服务只需要在自己的逻辑处理完之后,发送消息到MQ,即可完成任务,被调用的微服务接到消息后执行任务。
应用场景有:异步解耦:微服务间不必调用,继而联系变少。
流量削峰:将庞大请求放到消息队列中,避免下游系统崩溃导致上游调用系统崩溃。
相关的技术栈:ZeroMQ,RabbitMQ,ActiveMQ,RocketMQ,Kafka。
2. RocketMQ环境搭建
RocketMQ:下载地址
下载Binary包方便安装
2.1 RocketMQ安装部署
①首先安装虚拟机centos7,带界面,带网络。
②与FileZilla连接,参考这篇博客:博客地址
③将RocketMQ传给虚拟机,并解压。
④修改bin/runbroker.sh和bin/runserver.sh文件,JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g",将内存变为256,256,128。
⑤启动NameServer和Broker
简单几个Linux命令:
查看ssh状态:
sudo service sshd status
开启ssh服务:
sudo service sshd start
启动NameServer:
nohup ./bin/mqnamesrv &
启动Broker:
nohup bin/mqbroker -n localhost:9876 &
关闭MQ:
bin/mqshutdown broker
bin/mqshutdown namesrv
开放9876端口:
firewall-cmd --zone=public --add-port=9876/tcp --permanent
firewall-cmd --reload
关闭防火墙:
systemctl stop firewalld.service
查看9876端口占用情况:
netstat -alnp | grep 10062
杀死进程:
kill -9 进程id
内部测试RocketMQ是否成功部署:
开两个命令终端,一个运行:
export NAMESRV_ADDR=localhost:9876
bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
另一个运行:
export NAMESRV_ADDR=localhost:9876
bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
如果第一个终端可以接受到另一个终端发来的消息,即内部部署成功。
2.2 RocketMQ架构及概念
这部分,搬运黑马的教程里的原文内容:视频地址
RocketMQ的基本单元是MessageQueue,几个MessageQueue组成一个Topic,Broker负责把一个一个Topic发给Consumer,而Producer是产生MessageQueue和Topic,NameServer管理着Broker。
Broker(邮递员)
Broker是RocketMQ的核心,负责消息的接收,存储,投递等功能
NameServer(邮局)
消息队列的协调者, Broker向它注册路由信息,同时Producer和Consumer向其获取路由信息
Producer(寄件人)
消息的生产者,需要从NameServer获取Broker信息,然后与Broker建立连接,向Broker发送消息
Consumer(收件人)
消息的消费者,需要从NameServer获取Broker信息,然后与Broker建立连接,从Broker获取消息
Topic(地区)
用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息
Message Queue(邮件)
为了提高性能和吞吐量,引入了Message Queue,一个Topic可以设置一个或多个MessageQueue,这样消息就可以并行往各Message Queue发送消息,消费者也可以并行的从多个Message Queue读取消息
Message
Message 是消息的载体。
Producer Group
生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组。
Consumer Group
消费者组,消费同一类消息的多个 consumer 实例组成一个消费者组。
2.3 RocketMQ控制台安装
①下载地址:地址
②解压,修改配置文件rocketmq-console\src\main\resources\application.properties
server.port=7777 #项目启动后的端口号
rocketmq.config.namesrvAddr=192.168.18.128:9876 #nameserv的地址(虚拟机的地址), 注意防火墙要开启9876端口,ping一下虚拟机是否ping通。
③打成jar包,运行
打成jar包
mvn clean package -Dmaven.test.skip=true
启动
java -jar target/rocketmq-console-ng-1.0.0.jar
访问localhost:7777
可能会报错10911访问超时,需要在虚拟机上开一下10911端口
firewall-cmd --zone=public --add-port=10911/tcp --p
firewall-cmd --reload
实在不行把防火墙关了,命令在上面。
3. 微服务发送,订阅消息
3.1 发送消息
①添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
②添加配置
rocketmq:
name-server: 192.168.18.128:9876 #rocketMQ服务的地址
producer:
group: shop-order # 生产者组
③利用RocketMQTemplate,convertAndSend方法,发送消息。注入:
@Autowired
private RocketMQTemplate rocketMQTemplate;
发送消息:
//参数1指定topic 参数2指定消息
rocketMQTemplate.convertAndSend("order-topic",order);
消息发送完成以后可以看一眼rocketmq的控制台是否发送成功。
3.2 订阅消费消息
①引入依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
②添加配置
rocketmq:
name-server: 192.168.18.128:9876
③编写service层的消息监听类。实现接口RocketMQListener<>泛型里写上面消息发过来传来的对象:Order
@Slf4j
@Service
@RocketMQMessageListener(consumerGroup = "shop-user",topic = "order-topic")
public class SmsService implements RocketMQListener<Order> {
@Override
public void onMessage(Order order) {
log.info("接受到了一个订单信息{},接下来可以发送短信通知了",order);
}
}
4. 消息类型
4.1 普通消息
可靠同步发送:“可靠”指的是发送者能否获得发送结果。同步指要等结果返回之后才进行下一步操作。先向RocketMQ发送一个消息直到消息结果返回之后才发送下一个消息。
可靠异步发送:先向RocketMQ发送一个消息,继续发下一个消息,返回结果以回调函数的方式回调。
单向发送:只发出消息,不接受结果。
4.1.1 可靠同步发送
利用RocketMQTemplate的syncSend方法发送消息,用快捷键ctrl+alt+v生成变量接受该方法的返回结果,可以根据返回结果进行后续操作,实例代码:
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void testSyncSend(){
//参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
SendResult result = rocketMQTemplate.syncSend("test-topic-1", "这是一条同步消息");
System.out.println(result);
}
4.1.2 可靠异步发送
asyncSend方法,第三个参数,定义回调函数,收集返回结果,如果消息发送成功执行onSuccess方法,如果失败执行onException方法。注:返回结果需要时间,而testAsynSend这个最外层的方法在消息发完之后就执行完成了,所以需要让该线程休眠一段时间,等待返回结果的接受并启动回调函数。
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void testAsynSend() throws Exception{
//参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
//参数三: 回调函数, 处理返回结果
rocketMQTemplate.asyncSend("test-topic-1:sayncsend", "这是一条异步消息", new SendCallback() {
//成功回调
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
//异常回调
@Override
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
System.out.println("=========================");
Thread.sleep(1000);
}
4.1.3 单向发送
消息发送完成就结束。
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void testOneway(){
rocketMQTemplate.sendOneWay("test-topic-1:oneway","这是一条单向消息");
}
4.2 顺序消息
顺序消息是消息队列提供的一种严格按照顺序来发布和消费的消息类型。
由于一个topic有多个消息队列,发送多个消息时,便不能保证消息的顺序性。
在普通消息的基础上,加上Orderly后缀即可顺序发送。
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void testOnewayOrderly(){
for(int i=0;i<10;i++){
//第三个参数决定发送到哪个队列上
rocketMQTemplate.sendOneWayOrderly("test-topic-1:oneway","这是一条单向消息","xx");
}
}
4.3 事务消息
事务消息交互流程:
①首先sendMessageInTransaction发送半事务消息(保存在MQ,暂时不给消费者)这里类似于可靠同步消息,需要MQ返回一个半事务消息发送成功的信息。(都集成在sendMessageInTransaction里了)
②消息发送方会根据自己运行情况再发送commit或者rollback的消息给MQ,这个消息类似于单向发送消息,MQ根据这个单向消息的内容再做处理,如果返回的是commit就把消息发给消费者,如果rollback就不发。
③如果MQ长时间没有收到这个单向消息,MQ会发一个回查消息给消息发送者,查询事务状态。这部分类似于可靠异步发送的回查。
抄来的流程图:
整体思路:新建新的表tx_log用于记录事务是否正常完成。(在处理本地事务操作数据库时,同时往tx_log插入数据,并记录插入数据的id,之后把id传递下去,要确认本地事务是否完成就查询tx_log表,看看表内有没有id是传递id的数据。如果有就返回commit,如果没有就返回rollback)
实现步骤:①在主service类中,调用sendMessageInTransaction方法,发送事务消息,新建的tx_log的id,存到Message中,传给步骤②
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createOrderBefore(Order order){
String txId = UUID.randomUUID().toString();
rocketMQTemplate.sendMessageInTransaction(
"tx_producer_group",
"order-topic",
MessageBuilder.withPayload(order).setHeader("txId",txId).build(),
order
);
}
②创建一个类,用于处理本地事务以及回查事务,需要实现RocketMQLocalTransactionListener接口,重写里面的executeLocalTransaction和checkLocalTransaction方法。
executeLocalTransaction方法里面写本地事务(上图中的步骤3)接受步骤①传来的需要新增的tx_log表记录的id,编写本地事务逻辑,并新增tx_log表记录如果没有异常则返回commit,如果出现异常则返回rollback(上图的4)。
③在checkLocalTransaction方法里写回查逻辑:根据Message带的txid,查询tx_log表里有该id的记录,则返回commit,如果没有记录则返回rollback(上图的6,7)
@Service
@RocketMQTransactionListener(txProducerGroup = "tx_producer_group")
public class OrderServiceImpl1Listener implements RocketMQLocalTransactionListener {
@Autowired
private OrderDao orderDao;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderServiceImpl1 orderServiceImpl1;
@Autowired
private TxLogDao txLogDao;
//执行本地事务
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
String txId = (String) message.getHeaders().get("txId");
try {
Order order = (Order)o;
orderServiceImpl1.createOrder(txId,order);
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//消息回查
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
String txId = (String) message.getHeaders().get("txId");
TxLog txLog = txLogDao.findById(txId).get();
if(txLog!=null){
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
扩展:命令行jps命令可以查看当前运行的进程。
taskkill -F /pid 杀死进程。
上图的5场景是消息发送者一直没有发送commit或者rollback,利用debug,当函数运行到要返回值时,杀死进程,即可模拟出上图5的场景。