RocketMQ入门(三)

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,完事,保证了分布式事务的一致性。

猜你喜欢

转载自blog.csdn.net/weixin_42412601/article/details/107403430
今日推荐