1、RocketMq服务安装
(1)、下载
http://rocketmq.apache.org/release_notes/release-notes-4.6.0/
(2)、安装
下载二进制版本,解压到任意目录即可。
需要注意的是:无论是windows环境还是linux环境,安装路径中都不能有空格否则启动mqnamesrv.cmd时会报 “找不到或无法加载到主类”的错。且jdk安装目录也不能有空格,否则启动mqbroker时也会报 “找不到或无法加载到主类”的错。
Linux和windows上都能运行,解压即可(以windows为例):
(3)配置环境变量
ROCKETMQ_HOME(linux中也要配置环境变量)
(4)启动mqnamesrv服务
在命令提示符(cmd)窗口中执行命令 mqnamesrv.cmd 启动日志如果有 The Name Server boot success 打印则表示NameServer 启动成功
(5)启动mqbroker服务
重新开启一个命令提示符(cmd)窗口,执行命令 mqbroker.cmd -n localhost:9876,注意只要没有报错日志应该就是启动成功了,如果启动成功则不会打印任何日志,不要关闭命令提示符(cmd)窗口。需要注意的是必须先启动 NameServer 再启动 Broker,Broker 要在 NameServer 上注册。
(6)关闭broker
首先,执行命令 mqshutdown.cmd broker 关闭 Broker,如果有 Broker 运行则会打印关闭的 Broker 所在线程
(7)关闭
其次,执行命令mqshutdown.cmd namesrv 关闭 NameServer,如果有 NameServer 运行则会打印关闭的 NameServer 所在线程
(8)RocketMQ Console安装(视化管理控制台)
下载 rocketmq-externals
修改配置文件 rocketmq-externals-master/rocketmq-console/src/main/resources/application.propertis
rocketmq.config.namesrvAddr=localhost:9876
运行rocketmq-console项目
java -jar rocketmq-console-ng-1.0.1.jar
打开浏览器,输入 localhost:8080,可以看到如下界面
2、producer开发
(1)、引入jar包
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
(2)配置properties
rocketmq.name-server=localhost:9876
#这个group只在启动的时候用,在代码中没用到
rocketmq.producer.group=producer-mq-msg
(3)编写事务消息发送方法及本地事务方法
package com.esc.payservice.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.esc.payservice.dao.AccountMapper;
import com.esc.payservice.model.Account;
import com.esc.payservice.model.AccountExample;
import com.esc.payservice.service.IPayService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
@Service
public class MqPayService implements IPayService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private AccountMapper accountMapper;
@Override
public String payMoney() {
/**
* 步骤一:
* 功能:发送事务消息
* txProducerGroup:消息生产者组
* destination:消息发送目的地,即topic(生产者和消费者topic相同才能消费到消息)
* message:消息
* arg:参数
*/
Account modAccount = new Account();
modAccount.setAccountNo("1234567890");
modAccount.setBalance(new BigDecimal("700.00"));//通知1234567890账户新增700元
//将要发送的对象转换成消息需要的json字符串格式
JSONObject jsonObject = new JSONObject();
jsonObject.put("modAmount", modAccount);
jsonObject.toString();
Message<String> message = MessageBuilder.withPayload(jsonObject.toString()).build();
rocketMQTemplate.sendMessageInTransaction("produce_pay", "topic_mqmsg",message,null);
return "success";
}
public void updateAmount(Account modAccount){
/**
* 步骤三
* 功能:更新本地账户
* 描述:事务消息发送成功后,mq收到消息并应答,开始执行本地事务
*/
AccountExample example = new AccountExample();
AccountExample.Criteria criteria = example.createCriteria();
criteria.andAccountNoEqualTo("9876543210");//本地账户
//查询本地账户
List<Account> accountList = accountMapper.selectByExample(example);
//扣减本地账户金额
Account record = new Account();
record.setBalance(accountList.get(0).getBalance().subtract(modAccount.getBalance()) );//本地账户9876543210扣减消息中的发送的金额
//更新本地账户
AccountExample example2 = new AccountExample();
AccountExample.Criteria criteria2 = example2.createCriteria();
criteria2.andIdEqualTo(accountList.get(0).getId());
accountMapper.updateByExampleSelective(record, example2);
}
}
注意:sendMessageInTransaction方法中的topic名称要与服务消费方中的topic名称一致
(4)编写MQ监听器,监听生产者发送消息后mq的回调以及mq回调检查本地事务的执行情况
package com.esc.payservice.listener.mqlistener;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.esc.payservice.model.Account;
import com.esc.payservice.service.IPayService;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 功能:本地事务监听器
* 描述:mq收到事务消息后的应答回调以及mq回调检查本地事务执行情况
* @Component:让监听器成为容器中的一个组件
* @RocketMQTransactionListener:监听器,监听某个组的生产者发送消息后的回调
*
*/
@Component
@RocketMQTransactionListener(txProducerGroup = "produce_pay")
public class ProduceMqMsgListner implements RocketMQLocalTransactionListener {
@Autowired
private IPayService mqPayService;
/**
* 步骤二:
* 描述:mq收到事务消息后,开始执行本地事务
* @Transactional:开启本地事务
* @param message
* @param o
* @return
*/
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try {
String jsonString = new String((byte[])message.getPayload());//转换成String
JSONObject jsonObject = JSONObject.parseObject(jsonString);
//消息中的账户为消费方中的账户
Account account = JSONObject.parseObject(jsonObject.getString("modAmount"), Account.class);
//更新本地账户时,只用到了消息中的金额
mqPayService.updateAmount(account);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e){
return RocketMQLocalTransactionState.UNKNOWN;
}
}
/**
* 步骤四
* 描述:mq回调检查本地事务执行情况
* @param message
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
//此处只是模拟,需根据实际情况编写检查本地事务执行情况方法
if(System.currentTimeMillis() % 3 == 0){
return RocketMQLocalTransactionState.COMMIT;
}else{
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
注意:
(1)、类上需添加@component和@RocketMQTransactionListener注解
(2)、@RocketMQTransactionListener中需指定txProducerGroup属性,且属性值与上面(3)中发送事务消息时用到的txProducerGroup值一致
(3)、回调执行本地事务的方法上要加上@Transactional注解,以执行本地事务
到此,producer的主要方法编写完成
3、consumer开发
(1)、引入jar包
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
(2)配置properties
rocketmq.name-server=localhost:9876
#这个group只在启动的时候用,在代码中没用到
rocketmq.producer.group=consumer-mq-msg
(3)、编写consumer监听器
package com.esc.bank.listener.mqlistener;
import com.alibaba.fastjson.JSONObject;
import com.esc.bank.model.Account;
import com.esc.bank.service.IBankService;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 功能:mq消息监听器
* 描述:监听来自topic_mqmsg的topic的消息,此topic要与生产者发送的topic一致
* @Component:注册成为容器中的一个组件
* @RocketMQMessageListener:消息监听器,监听来自topic_mqmsg的消息,并定义消费者自己的组
*/
@Component
@RocketMQMessageListener(topic = "topic_mqmsg",consumerGroup = "consumer_bank")
public class ConsumerMqMsgListener implements RocketMQListener<String> {
@Autowired
private IBankService mqBankService;
/**
* 监听消息,并执行本地事务
* @param s
*/
@Override
public void onMessage(String s) {
JSONObject jsonObject = JSONObject.parseObject(s);
final Account modAmount = JSONObject.parseObject(jsonObject.getString("modAmount"), Account.class);
mqBankService.mqModAmount(modAmount);
}
}
注意:
(1)、类上需添加@Component、@RocketMQMessageListener
(2)、@RocketMQMessageListener需要topic和consumerGroup属性,且topic的属性值必须与producer中配置的topic值一致
(3)、编写consumer本地事务方法
package com.esc.bank.service.impl;
import com.esc.bank.dao.AccountMapper;
import com.esc.bank.model.Account;
import com.esc.bank.model.AccountExample;
import com.esc.bank.service.IBankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class MqBankService implements IBankService {
@Autowired
private AccountMapper mapper;
/**
* @Transactional:开启本地事务
* @param account
* @return
*/
@Override
@Transactional
public String mqModAmount(Account account) {
AccountExample example = new AccountExample();
AccountExample.Criteria criteria = example.createCriteria();
criteria.andAccountNoEqualTo(account.getAccountNo());
List<Account> accountList = mapper.selectByExample(example);
Account record = new Account();
record.setBalance(accountList.get(0).getBalance().add(account.getBalance()));
AccountExample example2 = new AccountExample();
AccountExample.Criteria criteria2 = example2.createCriteria();
criteria2.andIdEqualTo(accountList.get(0).getId());
mapper.updateByExampleSelective(record,example2);
return "success";
}
}
注意:本地事务方法上也需要添加@Transactional注解。
至此,producer和consumer的主要步骤基本编写完成,加上调用方法以及其他配置就可以使用了。
运行项目前,需先运行manamesrv和mqbroker服务,再运行producer和consumer项目
为方便理解我们以注册送积分的例子来描述 整个流程。
Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
1、Producer 发送事务消息
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
本例中,Producer 发送 ”增加积分消息“ 到MQ Server。
2、MQ Server回应消息发送成功
MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。
3、Producer 执行本地事务
Producer 端执行业务代码逻辑,通过本地数据库事务控制。
本例中,Producer 执行添加用户操作。
4、消息投递
若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“ 。
MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
5、事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。