分布式事务(四)Spring Cloud 微服务系统基于 RocketMQ 可靠消息最终一致性实现分布式事务
搭建 RocketMQ 服务器
RocketMQ 安装
-
克隆 centos-8-2105(centos-7-1908):rocketmq
-
设置 ip
./ip-static ip: 192.168.64.141 ifconfig
centos-8 设置若有问题:
可以开启 VMware 托管
nmcli n on systemctl restart NetworkManager
-
上传文件到 /root/
- 课前资料/分布式事务/rocketmq/ 文件夹的三个文件
-
按照 rocketmq 笔记安装:
-
启动 rocketmq
先启动 name server
# 进入 rocketmq 目录 cd /usr/local/rocketmq/ # 启动 name server nohup sh bin/mqnamesrv & # 查看运行日志, 看到"The Name Server boot success."表示启动成功 tail -f ~/logs/rocketmqlogs/namesrv.log
再启动 broker
# 启动 broker, 连接name server: localhost:9876 nohup sh bin/mqbroker -n localhost:9876 & # 查看运行日志, 看到"The broker[......:10911] boot success."表示启动成功 tail -f ~/logs/rocketmqlogs/broker.log
-
启动管理界面
# 切换到主目录 cd ~/ # 运行启动管理界面(控制台) nohup java -jar rocketmq-console-ng-1.0.1.jar --server.port=8080 --rocketmq.config.namesrvAddr=localhost:9876 &
访问管理界面:http://192.168.64.141:8080
查看 Cluster 选项卡,下面应有注册信息:
-
IP 地址应为 192.168.64.141
-
若 ip 地址为 172.xx.xx.xxx 则是虚拟机克隆错误或者网络配置问题。
-
-
收发消息出现超时问题
直接复制代码解决问题
cd /usr/local/rocketmq/ vim conf/broker.conf 末尾添加 brokerIP1=192.168.64.141 关闭 broker 服务 mqshutdown broker 重新使用 broker.conf 配置启动 broker nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf &
RocketMQ 双主双从同步复制集群方案
broker 集群是为了实现高可用,防止服务器宕机,每一个主 broker 都带有一个从 broker。
参考笔记:
RocketMQ 基本原理
Topic 基本原理
在管理界面,点击 Topic 选项卡,点击 ADD/UPDATE 创建一个新的 Topic。
clusterName 选择默认 DefaultCluster,命名 Topic1,读写队列为 3,权限为 6。
使用 RocketMQ 原生 API 收发消息代码样例
创建工程
新建空工程 rocketmq-dtx
创建 Maven 模块 rocketmq-api,注意项目目录在空工程下面。
添加依赖(直接复制):
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-store</artifactId>
<version>4.7.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
同步消息
同步消息发送要保证强一致性,发到master的消息向slave复制后,才会向生产者发送反馈信息。
这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
生产者
新建 m1 包。创建 Producer 生产者类,添加 main 方法。
创建生产者对象
添加组名 pro-group1
DefaultMQProducer p = new DefaultMQProducer("pro-group1");
设置 name server,启动连接
p.setNamesrvAddr("192.168.64.141:9876");
p.start();
创建消息封装对象 Message(Topic,Tag,消息)并发送
while (true){
System.out.println("输入消息: ");
String s = new Scanner(System.in).nextLine();
// Topic 相当于是一级分类
// Tag 相当于是二级分类
Message msg = new Message("Topic1", "TagA", s.getBytes());
SendResult r = p.send(msg);
System.out.println(r);
}
若有异常直接抛出。
启动测试
测试输入语句,查看控制台输出信息。
前面的状态,ID数据可不看,看后面的 topic 应为之前创建的 Topic1,队列ID(queueId)应为 0-2,队列下标(queueOffset)都应为 0
查看管理界面,点击 Topic1 的 status 会发现有请求的痕迹,只不过时间戳有问题(无所谓)。
查看 Message 的选项卡,查询 Topic1,注意后面时间范围要注意调整。
消费者
创建 Consumer 生产者类,添加 main 方法。
创建消费者对象
DefaultMQPushConsumer c = new DefaultMQPushConsumer("con-group1");
设置 name,server
c.setNamesrvAddr("192.168.64.141:9876");
设置从哪里订阅消息
c.subscribe("Topic1","TagA || TagB || TagC");
设置消息监听器
// Concurrently -- 并发的,会启动多个线程处理消息
c.setMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt ext : msgs) {
String s = new String(ext.getBody());
System.out.println("收到: "+s);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
//return ConsumeConcurrentlyStatus.RECONSUME_LATER;//稍后重新消费
}
});
启动连接
c.start();
启动测试
启动 Consumer 类,会接收到先前 Producer 发送的几条消息,只不过顺序不一样。
延时消息
消息发送到 Rocketmq 服务器后, 延迟一定时间再向消费者进行投递。
生产者
在创建完 msg 后面添加一句延迟。
msg.setDelayTimeLevel(3);
重启测试:Consumer 会根据延迟等级延迟发送消息。
消费者
将之前注释的 RECONSUME_LATER 打开,CONSUME_SUCCESS 注释掉。
重启 Consumer 测试是否有重新发送的消息。
顺序消息
Rocketmq 顺序消息的基本原理:
- 同一组有序的消息序列,会被发送到同一个队列,按照 FIFO 的方式进行处理。
- 一个队列只允许一个消费者线程接收消息,这样就保证消息按顺序被接收
下面以订单为例:
一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中。消费时,从同一个队列接收同一个订单的消息。
生产者
生产者发送消息时要把消息发到同一个队列。
方式:用第一条消息的 ID 去取余队列数来分组。
添加发送语句
从笔记复制即可。
static String[] msgs = {
"15103111039,创建",
"15103111065,创建",
"15103111039,付款",
"15103117235,创建",
"15103111065,付款",
"15103117235,付款",
"15103111065,完成",
"15103111039,推送",
"15103117235,完成",
"15103111039,完成"
};
基本配置
// 创建生产者对象
DefaultMQProducer p = new DefaultMQProducer("pro-group2");
// 设置 name server
p.setNamesrvAddr("192.168.64.141:9876");
// 启动,连接
p.start();
遍历数组发送消息
//遍历数组发送消息
for (String s: msgs) {
// s -- " 15103111039,完成"
Long orderId = Long.valueOf(s.split(",")[0]);
Message msg = new Message("Topic2", s.getBytes());
//p.send(消息,队列选择器,选择依据)
SendResult r = p.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long orderId = (Long) arg;
int index = (int) (orderId % mqs.size());
return mqs.get(index);
}
}, orderId);
System.out.println(r);
}
启动测试
会有很多数据显示被发送了。
消费者
基本配置
复制之前的 Consumer 类就行。
消费者只能启动同一个线程来处理消息
更改消费者对象分组为 “con-group2”
更改订阅和标签为 “Topic2” 和 “*”
消息监听器要重写
// orderly -- 单个线程
c.setMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt ext: msgs){
String s = new String(ext.getBody());
System.out.println("收到: "+s);
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
启动测试
收到先前发送的字符即可。
事务消息
生产者
创建事务消息生产者,设置 name server
TransactionMQProducer p = new TransactionMQProducer("pro-group3");
p.setNamesrvAddr("192.168.64.141:9876");
设置事务消息监听器
-
执行本地事务(service.业务方法())
-
处理 rocketmq 的事务回查
分别测试成功和失败两种情况:
p.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println("执行本地业务,参数: "+arg);
if (Math.random() < 0.5){
System.out.println("本地事务执行成功~");
return LocalTransactionState.COMMIT_MESSAGE; // 提交消息,通知服务器消息可以投递
}else{
System.out.println("本地事务执行失败~");
return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚消息,撤回消息
}
// return LocalTransactionState.UNKNOW; // 未知,当前方法中,一般不会出现这种状态
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
return null;
}
});
启动,发送事务消息
p.start();
while(true){
System.out.println("输入消息: ");
String s = new Scanner(System.in).nextLine();
Message msg =