假如有一个文章审核,通过后加积分的需求,审核是这个方法的主体,而加积分并不需要实时同步,这时,我们就可以将加积分的功能变为异步执行。
Spring实现异步的方法
1.AsyncRestTemplate
https://blog.csdn.net/jiangchao858/article/details/86709750
2.@Async注解
https://spring.io/guides/gs/async-method/
3.WebClient(Spring 5.0引入)
https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web-reactive.html#webflux-client
4.MQ
引入MQ后,生产者生产消息,然后将消息发送给MQ,消费者监听这个消息所属的topic,一旦接收到消息,就进行相关的处理。
MQ的适用场景
1.异步处理:不解释
2.流量削峰填谷:例如秒杀活动,可以用MQ控制参加人数,人数一旦达到阈值,就丢弃请求或跳转到错误页。防止应用被流量洪峰打死。
3.解耦微服务:假如A调用B,B挂了,虽然有sentinel可以保护A不被B拖死,但是依然无法正常返回,用MQ以后,A把消息发给MQ,就算B挂了也没事,后面B恢复正常会从MQ中拿消息处理,AB也解耦了。
RocketMQ
https://git.imooc.com/coding-358/rocketmq-dev-guide
搭建RocketMQ
1.http://rocketmq.apache.org/release_notes/release-notes-4.5.1/下载Binary并解压
2.配置环境变量:变量名:ROCKETMQ_HOME, 变量值:MQ解压路径\MQ文件夹名
3.启动NAMESERVER:cmd到bin目录下,执行start mqnamesrv.cmd
4.启动BROKER:cmd到bin目录下,执行start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true
tips:假如弹出提示框提示‘错误: 找不到或无法加载主类 xxxxxx’。打开runbroker.cmd,然后将‘%CLASSPATH%’加上英文双引号。保存并重新执行start语句。
RocketMQ插件部署
1.下载https://github.com/apache/rocketmq-externals.git
2.配置:进入rocketmq-console\src\main\resources文件夹,打开application.properties进行配置。
3.修改依赖:把pom.xml中<rocketmq.version>4.4.0</rocketmq.version>改成你的RocketMQ版本
4.改代码:修改pom以后org.apache.rocketmq.console.service.impl.MessageServiceImpl#queryMessageByTopic编译会报错,DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, null);
改为
RPCHook rpcHook = null;
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook);
5.编译启动:进入rocketmq-externals\rocketmq-console文件夹,执行mvn clean package -DskipTests
6.cmd进入target文件夹,执行java -jar rocketmq-console-ng-1.0.0.jar
7.浏览器中输入127.0.0.1:配置端口(例如17890),成功后即可查看。
生产可用的集群搭建
http://www.itmuch.com/books/rocketmq/operation.html
RocketMQ的术语和概念
主题Topic:
一类消息的集合,RocketMQ的基本订阅单位
例如:听广播需要知道是什么调频,fm89.7之类的,这个调频就相当于topic
消息模型:
Producer(生产者,生产消息)
Broker(消息代理,存储消息、转发消息)
Consumer(消费者,消费消息)
部署结构
NameServer(名字服务)生产者/消费者通过名字服务查找各主题相应的Broker IP列表
相当于RocketMQ的服务发现组件
BrokerServer(代理服务器)消息中转角色,负责存储消息、转发消息
消费模式
PullConsumer(拉取式消费)应用调用Consumer的拉取信息方法从BrokerServer拉取消息
PushConsumer(推动式消费)Broker收到消息后主动推送给消费端,该模式实时性较高
组Group
ProducerGroup(生产者组)同一类Producer的集合,这类Producer发送同一类消息且
ConsumerGroup(消费者组)同一类Consumer的集合,这类Consumer通常消费同一类消息
消息传播模式
Clustering(集群)相同ConsumerGroup的每个Consumer实例平均分摊消息
Broadcasting(广播)相同ConsumerGroup的每个Consumer实例都接受全量的消息
消息类型
普通消息、顺序消息、定时/延时消息、事务消息
编写生产者
加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
这里要自己写一下版本,因为使用的RocketMQ版本不一样
写配置
rocketmq:
name-server: 127.0.0.1:9876
producer:
#必须指定group,否则启动会报无法初始化RocketMQ
group: test-group
写代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserAddBonusMsgDTO{
//为谁加积分
private Integer userId;
//加多少积分
private Integer bonus;
}
public class ShareService{
private final RocketMQTemplate rocketMQTemplate;
public ShareDTO findById(Integer id){
this.rocketMQTemplate.convertAndSend(
"add-bonus",
UserAddBonusMsgDTO.builder()
.userId(share.getUserId())
.bonus(50)
.build()
);
}
发送请求后,在控制台就能看到一个叫add-bonus的主题了
编写消费者
加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
写配置
rocketmq:
name-server: 127.0.0.1:9876
写业务代码
@Service
@RocketMQMessageListener(consumerGroup = "consumer-group" , topic = "add-bonus")
public class AddBonusListener implements RocketMQListener<UserAddBonusMsgDTO>{
@Override
public void onMessage(UserAddBonusMsgDTO message){
//当收到消息的时候执行的业务
...
}
}
启动以后就可以自动运行
RocketMQ的分布式事务
一般添加注解@Transactional(rollbackFor = Exception.class),发生异常,数据库就会回滚,但是由于有了MQ,发生异常后,数据库回滚了,但是MQ消息已经发出去了,就没办法取消了,基于这种情况,RocketMQ提供了事务消息。
流程:
概念术语:
消息的三种状态:
实现分布式事务
public class ShareService{
private final RocketMQTemplate rocketMQTemplate;
public ShareDTO findById(Integer id){
String transactionId = UUID.randomUUID().toString();
//发送半消息
this.rocketMQTemplate.sendMessageInTransaction(
"tx-add-bonus-group", //随便起个名称
"add-bonus", //topic
MessageBuilder.withPayload( //消息
UserAddBonusMsgDTO.builder()
.userId(share.getUserId())
.bonus(50)
.build()
.setHeader(RocketMQHeaders.TRANSACTION_ID , transactionId)
.setHeader("share_id" , id)
).build(),
auditDTO //arg
);
}
@RocketMQTransactionListener(txProducerGroup = "tx-add-bonus-group")
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener{
//本地事务
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg , Object arg){
MessageHeaders headers = msg.getHeaders();
String transactionId = (String)headers.get(RocketMQHeaders.TRANSACTION_ID);
Integer shareId = Integer.valueOf((String)header.get("share_id"));
//接下来就可以执行本地事务了
try{
...
//建一个日志表,正常通过后,将日志记录写到表里,保证如果要回查可以查到
//如果本地事务正常通过
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//回查
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg){
MessageHeaders headers = msg.getHeaders();
String transactionId = (String)headers.get(RocketMQHeaders.TRANSACTION_ID);
//回查 如果有就返回commit,不是空就rollback
if(成功){
return RocketMQLocalTransactionState.COMMIT;
}else{
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
Spring Cloud Stream
来源:CSDN
作者:耗子肉
链接:https://blog.csdn.net/haozi_rou/article/details/99678907