springboot整合rocketmq
生产者
依赖:
<properties>
<java.version>1.8</java.version>
<rocketmq-spring-boot-starter-version>2.1.0</rocketmq-spring-boot-starter-version>
</properties>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot-starter-version}</version>
</dependency>
配置文件:这里配置文件设置的超时时间是10秒,默认时间是3秒
#nameserver地址
rocketmq.name-server=192.168.59.131:9876
#发送者组名
rocketmq.producer.group=my-group
rocketmq.producer.send-message-timeout=10000
测试类:
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void test1(){
/**
* 参数一:topic
* 参数二:消息内容
*/
rocketMQTemplate.convertAndSend("springboot-mq","hello springboot rocketmq");
}
如果启动测试。
出现Caused by: org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: sendDefaultImpl call timeout
1.检查端口是否开放。
2.如果使用的是虚拟机上centos7
上装的mq
,可能访问会有延迟 配置文件rocketmq.producer.send-message-timeout=10000
超时时间设置的长一点。(我就在这里被坑了,好久)
3.如果使用的是云服务器,自行百度,很多解决方案
单元测试成功启动:
到监控平台查看:发现单元测试的发送的消息的主题已经注册上去了
测试完成,整合成功!
消费者
依赖:
注意与生产者的区别,版本换了,消费者使用2.1.0
版本启动会出现invokeSync call timeout
的异常,把版本降到2.0.4
即可;
还要添加web
依赖
<properties>
<java.version>1.8</java.version>
<rocketmq-spring-boot-starter-version>2.0.4</rocketmq-spring-boot-starter-version>
</properties>
<!--添加web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springboot-rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot-starter-version}</version>
</dependency>
配置文件:
#nameserver
rocketmq.name-server=192.168.59.131:9876
#消费者组名
rocketmq.consumer.group=my-group
#消费者主题
rocketmq.consumer.topic=springboot-mq
消息监听器:
@RocketMQMessageListener(topic = "${rocketmq.consumer.topic}",
consumerGroup = "${rocketmq.consumer.group}",
messageModel = MessageModel.BROADCASTING //BROADCASTING广播模式 CLUSTERING负载均衡模式
)
@Component
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String s) {
System.out.println("接受到消息:"+s);
}
}
启动消费者:
失败补偿机制
例
下单业务:
确认订单业务逻辑(消息生产者):
public Result confirmOrder(TradeOrder order) {
//1.校验订单
checkOrder(order);
//2.生成预订单
Long aLong = savePreOrder(order);
try {
//3.扣减库存
reduceGoodsNum(order);
//4.扣减优惠券
updateCouponStatus(order);
//5.使用余额
reduceMoneyPaid(order);
//6.确认订单
updateOrderStatus(order);
//模拟出现异常,导致确认订单失败,以至发送订单确实失败消息
CastException.cast(ShopCode.SHOP_FAIL);
//7.返回成功状态
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage());
} catch (Exception e) {
//1.确认订单失败,发送消息
MQEntity mqEntity=new MQEntity();
mqEntity.setOrderId(aLong);
mqEntity.setUserId(order.getUserId());
//已付金额
mqEntity.setUserMoney(order.getMoneyPaid());
mqEntity.setGoodsId(order.getGoodsId());
//商品数量
mqEntity.setGoodsNum(order.getGoodsNumber());
mqEntity.setCouponId(order.getCouponId());
try {
//发送订单确实失败消息
sendCancelOrder(topic, tag, order.getOrderId().toString(), JSON.toJSONString(mqEntity));
}catch (Exception e1){
e1.printStackTrace();
}
//2.返回失败状态
return new Result(ShopCode.SHOP_FAIL.getSuccess(),ShopCode.SHOP_FAIL.getMessage());
}
}
//发送订单确实失败消息
private void sendCancelOrder(String topic, String tag, String key, String body) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
//构造消息体
Message message=new Message(topic,tag,key,body.getBytes());
//获取生产者
DefaultMQProducer producer = rocketMQTemplate.getProducer();
//发送消息
producer.send(message);
}
失败补偿机制:
库存给减了,优惠券给用了,余额也给用了,因为出现异常,订单确认却失败了,所以库存得加回来,优惠券给恢复为未使用状态,余额也得加回来。
回退库存(消费者)
为啥要去查询消息的消费记录呢?
因为要保证消息处理的幂等性。处理过的消息会存到数据库中去(trade_mq_consumer_log表
),接受一条消息,不是拿来立马就消费,而是拿来判断一下有没有处理过,如果已经处理过,就不用了处理了,因为如果提供者给你发重复的消息,你进行重复消息,存库连续回退,数值肯定对不上。因此,消息的幂等性,需要在消费端进行保证的。
具体做法:通过存储消息的表的字段,组名,tag,key以及消费状态(处理成功表示消费了),判断当前消息是否消费过。
业务逻辑:
MessageExt是Message的子类,是对Message的扩展,内含消息id等信息
@RocketMQMessageListener(topic = "${mq.order.topic}",
consumerGroup ="${mq.order.consumer.group.name}",
messageModel = MessageModel.BROADCASTING //广播模式
)
@Component
@Slf4j
public class CancelMQListener implements RocketMQListener<MessageExt> {
。。。
@Override
public void onMessage(MessageExt message) {
String msgId=null;
String tags=null;
String keys=null;
String body=null;
try {
//1.解析消息内容
msgId = message.getMsgId();
tags = message.getTags();
keys = message.getKeys();
body = new String(message.getBody(), "UTF-8");
log.info("接受消息成功");
//2.查询消息消费记录
TradeMqConsumerLogKey tradeMqConsumerLogExample=new TradeMqConsumerLogKey();
tradeMqConsumerLogExample.setGroupName(groupName);
tradeMqConsumerLogExample.setMsgTag(tags);
tradeMqConsumerLogExample.setMsgKey(keys);
TradeMqConsumerLog tradeMqConsumerLog = tradeMqConsumerLogMapper.selectByPrimaryKey(tradeMqConsumerLogExample);
//3.判断 如果消费过
if (tradeMqConsumerLog!=null) {
//获取消息的处理状态 0:正在处理;1:处理成功;2:处理失败
Integer consumerStatus = tradeMqConsumerLog.getConsumerStatus();
//处理过
if (ShopCode.SHOP_MQ_MESSAGE_STATUS_SUCCESS.getCode().intValue() == consumerStatus.intValue()) {
log.info("消息:" + msgId + "处理过");
return;
}
//正在处理
if (ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode().intValue() == consumerStatus.intValue()) {
log.info("消息:" + msgId + "正在处理");
return;
}
//处理失败
if (ShopCode.SHOP_MQ_MESSAGE_STATUS_FAIL.getCode().intValue() == consumerStatus.intValue()) {
//消费次数已经大于3次
if (tradeMqConsumerLog.getConsumerTimes()>3){
log.info("消息:" + msgId + ",处理超过三次,不能再进行处理了");
return;
}
//消费次数小于三次
//状态设置为正在处理
tradeMqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode());
//updateByExampleSelective方法默认支持数据库乐观锁
//设置并发修改条件
TradeMqConsumerLogExample tradeMqConsumerLogExample1=new TradeMqConsumerLogExample();
TradeMqConsumerLogExample.Criteria criteria=tradeMqConsumerLogExample1.createCriteria();
criteria.andMsgTagEqualTo(tradeMqConsumerLog.getMsgTag());
criteria.andMsgKeyEqualTo(tradeMqConsumerLog.getMsgKey());
criteria.andGroupNameEqualTo(groupName);
//消费次数相当于是乐观锁的version,只不过这里的version人为规定不能大于3
criteria.andConsumerTimesEqualTo(tradeMqConsumerLog.getConsumerTimes());
int i = tradeMqConsumerLogMapper.updateByExampleSelective(tradeMqConsumerLog, tradeMqConsumerLogExample1);
if (i<=0){
//未修改成功,其他线程并发修改
log.info("并发修改,稍后处理");
}
}
}
//4.判断 如果没有消费过
else {
tradeMqConsumerLog=new TradeMqConsumerLog();
tradeMqConsumerLog.setMsgTag(tags);
tradeMqConsumerLog.setMsgKey(keys);
tradeMqConsumerLog.setGroupName(groupName);
//消息状态改为正在处理
tradeMqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode());
tradeMqConsumerLog.setMsgBody(body);
//消费次数,指的是消费失败时才记录
tradeMqConsumerLog.setConsumerTimes(0);
tradeMqConsumerLog.setMsgId(msgId);
//将消息处理信息添加到数据库
tradeMqConsumerLogMapper.insert(tradeMqConsumerLog);
}
//5.回退库存
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
TradeGoods tradeGoods = goodsMapper.selectByPrimaryKey(mqEntity.getGoodsId());
tradeGoods.setGoodsNumber(tradeGoods.getGoodsNumber()+mqEntity.getGoodsNum());
goodsMapper.updateByPrimaryKey(tradeGoods);
//6.记录消息消费记录,更改消息处理状态为成功处理
tradeMqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_SUCCESS.getCode());
tradeMqConsumerLog.setConsumerTimestamp(new Date());
tradeMqConsumerLogMapper.updateByPrimaryKey(tradeMqConsumerLog);
log.info("回退库存成功");
} catch (Exception e) {
e.printStackTrace();
//如果消息处理失败,需要记录
//先查询消息消费记录表有没有数据
TradeMqConsumerLogKey tradeMqConsumerLogKey=new TradeMqConsumerLogKey();
tradeMqConsumerLogKey.setGroupName(groupName);
tradeMqConsumerLogKey.setMsgTag(tags);
tradeMqConsumerLogKey.setMsgKey(keys);
TradeMqConsumerLog tradeMqConsumerLog = tradeMqConsumerLogMapper.selectByPrimaryKey(tradeMqConsumerLogKey);
//没有,就将当前消息消费失败的信息,插进去
if (tradeMqConsumerLog==null){
tradeMqConsumerLog=new TradeMqConsumerLog();
tradeMqConsumerLog.setMsgTag(tags);
tradeMqConsumerLog.setMsgKey(keys);
tradeMqConsumerLog.setGroupName(groupName);
//消息状态改为正在处理
tradeMqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode());
tradeMqConsumerLog.setMsgBody(body);
//消费次数,指的是消费失败时才记录
tradeMqConsumerLog.setConsumerTimes(1);
tradeMqConsumerLog.setMsgId(msgId);
tradeMqConsumerLogMapper.insert(tradeMqConsumerLog);
}
//如果已经有消息消费记录,更新消费次数
else {
tradeMqConsumerLog.setConsumerTimes(tradeMqConsumerLog.getConsumerTimes()+1);
tradeMqConsumerLogMapper.updateByPrimaryKeySelective(tradeMqConsumerLog);
}
}
}
}
回退优惠券(消费者)
为啥不需要考虑消息的幂等性,其实还是与业务有关,像库存,如果出现消息的重复消费,库存数量会不一致。
但是回退优惠券,只需要将订单id设置null,优惠券状态改一下,优惠券时间设置为null就行了,就算出现消息的重复消费,也没啥影响。
@RocketMQMessageListener(topic = "${mq.order.topic}",
consumerGroup ="${mq.order.consumer.group.name}",
messageModel = MessageModel.BROADCASTING //广播模式
)
@Component
@Slf4j
public class CancelMQListener implements RocketMQListener<MessageExt> {
@Autowired
private TradeCouponMapper couponMapper;
@Override
public void onMessage(MessageExt message) {
try {
//1. 解析消息内容
String body = new String(message.getBody(), "UTF-8");
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
log.info("接收到消息");
//2. 查询优惠券信息
if (mqEntity.getCouponId()!=null) {
TradeCoupon coupon = couponMapper.selectByPrimaryKey(mqEntity.getCouponId());
//3.更改优惠券状态
coupon.setUsedTime(null);
coupon.setIsUsed(ShopCode.SHOP_COUPON_UNUSED.getCode());
coupon.setOrderId(null);
couponMapper.updateByPrimaryKey(coupon);
}
log.info("回退优惠券成功");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("回退优惠券失败");
}
}
}
回退余额(消费者)
@RocketMQMessageListener(topic = "${mq.order.topic}",
consumerGroup ="${mq.order.consumer.group.name}",
messageModel = MessageModel.BROADCASTING //广播模式
)
@Component
@Slf4j
public class CancelMQListener implements RocketMQListener<MessageExt> {
@Autowired
private IUserService userService;
@Override
public void onMessage(MessageExt messageExt) {
try {
//1.解析消息
String body = new String(messageExt.getBody(), "UTF-8");
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
log.info("接收到消息");
//已付金额不为0,才回退
if(mqEntity.getUserMoney()!=null && mqEntity.getUserMoney().compareTo(BigDecimal.ZERO)>0){
//2.调用业务层,进行余额修改
TradeUserMoneyLog userMoneyLog = new TradeUserMoneyLog();
userMoneyLog.setUseMoney(mqEntity.getUserMoney());
userMoneyLog.setMoneyLogType(ShopCode.SHOP_USER_MONEY_REFUND.getCode());
userMoneyLog.setUserId(mqEntity.getUserId());
userMoneyLog.setOrderId(mqEntity.getOrderId());
userService.updateMoneyPaid(userMoneyLog);
log.info("余额回退成功");
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("余额回退失败");
}
}
}
取消订单(消费者)
该回退的都回退了,最后得把订单状态改一下。
@RocketMQMessageListener(topic = "${mq.order.topic}",
consumerGroup ="${mq.order.consumer.group.name}",
messageModel = MessageModel.BROADCASTING //广播模式
)
@Component
@Slf4j
public class CancelMQListener implements RocketMQListener<MessageExt> {
@Autowired
private TradeOrderMapper orderMapper;
@Override
public void onMessage(MessageExt messageExt) {
try {
//解析消息
String body = new String(messageExt.getBody(), "UTF-8");
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
log.info("接受消息成功");
//查询订单
TradeOrder tradeOrder = orderMapper.selectByPrimaryKey(mqEntity.getOrderId());
//更新订单状态
tradeOrder.setOrderStatus(ShopCode.SHOP_ORDER_CANCEL.getCode());
orderMapper.updateByPrimaryKey(tradeOrder);
log.info("订单状态设置为取消");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.info("订单取消失败");
}
}
}
ok,完事,保证了分布式事务的一致性。