springboot+rocketMQ事务

1.rocketmq安装与启动

1.1下载地址

link.
![选择版本](https://img-blog.csdnimg.cn/c211bca3b73247388ab046ba5a362911.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASnV6aXBpMTIzNA==,size_20,color_FFFFFF,t_70,g_se,x_16

下载二进制文件后解压至 E:\rocketMQ

1.2配置环境变量

系统环境变量配置
变量名:ROCKETMQ_HOME
变量值:MQ解压路径\MQ文件夹名(E:\rocketMQ)

1.3.启动

1.3.1启动 nameserve
进入rocketMQan安装的bin目录下启动dos命令窗口,输入以下命令
start mqnamesrv.cmd

当窗口出现“The Name Server boot success. serializeType=JSON”表示启动成功

1.3.2启动broker
进入rocketMQan安装的bin目录下启动dos命令窗口,输入以下命令
start mqbroker.cmd -n 192.168.103.156:9876 autoCreateTopicEnable=true

当窗口出现“The broker[DESKTOP-RD1L52M, “your IP”:10911] boot success. serializeType=JSON and name server is “your IP”:9876”表示启动成功

2.下载安装rocketmq的可视化工具(控制台)

2.1下载地址

方式一、git下载,执行如下命令
git clone https://github.com/apache/rocketmq-externals.git

方式二、直接下载,访问如下地址即可
https://github.com/apache/rocketmq-externals/archive/master.zip

2.2配置控制台

下载之后,进入‘rocketmq-console\src\main\resources’文件夹,打开‘application.properties’进行配置。
根据需要配置端口号(避免端口占用即可)

2.3 启动编译

进入‘rocketmq-console’文件夹,执行‘mvn clean package -Dmaven.test.skip=true’,编译生成target目录。

编译成功
编译成功之后,进入‘target’文件夹,执行‘java -jar rocketmq-console-ng-版本号.jar’,启动刚刚编译生成的jar包。
控制台启动成功
控制台启动成功后打开浏览器输入:http://localhost:8090即可登录(此处8090即刚刚在application.properties文件配置的server.port=8090)
在这里插入图片描述

3.整合springboot

源码:gitee

3.1添加依赖

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.9.2</version>
        </dependency>

3.2 生产者发送消息函数

/**
     * @description 利用rocketmq发送消息
     * @author wd
     * @date 2021-10-27 15:50:09
     */
public String transfer(MessageContent messageContent) {
        TransactionMQProducer producer = new TransactionMQProducer("wangdinew");
        producer.setNamesrvAddr("localhost:9876");
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = newThread(r);
                thread.setName("wangdi-transaction");
                return thread;
            }
        });
        producer.setExecutorService(executorService);
        producer.setVipChannelEnabled(false);
        //TransactionListener 需要自己写一个实现类TransactionListenerImpl
        TransactionListener transactionListener = new TransactionListenerImpl();
        producer.setTransactionListener(transactionListener);
        TransactionSendResult sendResult = null;
        try {
            producer.start();
            String jsonstr = JSON.toJSONString(messageContent);
            Message msg = new Message("newwangdi-topic", "newwangdi-tag", jsonstr.getBytes(RemotingHelper.DEFAULT_CHARSET));
            sendResult = producer.sendMessageInTransaction(msg,null);
            return sendResult.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return " ";

    }

实现TransactionListener接口来监听prepare消息发送并执行本地事务和实现回查函数

package com.example.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.example.dao.domain.MessageContent;
import com.example.factory.SpringJobBeanFactory;
import com.example.service.BankService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Service;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author :wd
 * @program:RocketMQ
 * @description: 监听
 * @date :2021-10-28 09:03
 */
//@Component
@Service
@Slf4j
public class TransactionListenerImpl implements TransactionListener {

    /**
     * @description 存储事务状态信息 key:事务id value:当前事务执行的状态
     * @author wd
     * @date 2021-10-28 09:49:23
     */
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();


    /**
     * @description 执行本地事务
     * @author wd
     * @date 2021-10-28 09:49:39
     */
    @Override
//    @Transactional
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        BankService bankService = SpringJobBeanFactory.getBean(BankService.class);
        String transactionId = message.getTransactionId();
        localTrans.put(transactionId, 0);
        String body = new String(message.getBody());
        MessageContent messageContent = JSONObject.parseObject(body, MessageContent.class);
        log.info("---------executeLocalTransaction-----------");
        String value=bankService.del(messageContent);
        log.info("-----Local Transaction executed "+value);
        if(value.equals("")){
            localTrans.put(transactionId, 0);
        }else if("success".equals(value)){
            localTrans.put(transactionId, 1);
            return LocalTransactionState.COMMIT_MESSAGE;
        }else if("fail".equals(value)){
            localTrans.put(transactionId, 2);
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return LocalTransactionState.UNKNOW;
    }

    /**
     * @description 回查本地事务的执行状态
     * @author wd
     * @date 2021-10-28 09:49:53
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        log.info("回查-------getTransactionId="+status);
        if (null != status) {
            switch (status) {
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                default:
                    return LocalTransactionState.UNKNOW;
            }
        }
        return LocalTransactionState.UNKNOW;
    }

}

其中遇到问题是监听器无法注入service和mapper导致本地事务无法执行
解决方法是不用spring自动注入而从上下文获取bean的方法。

package com.example.factory;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author :wd
 * @program: rocketMQ-new
 * @description: 上下文获取service
 * @date :2021-11-03 16:34
 */
@Component
public class SpringJobBeanFactory implements ApplicationContextAware {


    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringJobBeanFactory.applicationContext=applicationContext;

    }
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        if (applicationContext == null){
            return null;
        }
        return (T)applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T>  name) throws BeansException {
        if (applicationContext == null){
            return null;
        }
        return applicationContext.getBean(name);
    }
}

至此事务的生产者完成

3.3消费者

package com.example.rocketMQ;


import com.example.dao.domain.MessageContent;
import com.example.service.BankService;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

/**
 * @author :wd
 * @program:RocketMQ
 * @description: 消费者
 * @date :2021-10-27 15:08
 */
@Component
@Slf4j
public class Consumer {

    @Autowired
    BankService bankService;

    @PostConstruct
    public void consumer() {
        log.info("-----init defaultMQPushConsumer-----");
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("wangdi-new-consumer");
        consumer.setNamesrvAddr("localhost:9876");
        try {
            consumer.subscribe("l-topic", "l-tag");
            consumer.registerMessageListener((MessageListenerConcurrently) (list, context) -> {
                try {
                    for (MessageExt messageExt : list) {
                        log.info("消费消息: " + new String(messageExt.getBody()));//输出消息内容
                        //此处为拿到消息后的本地事务处理逻辑,包括消息解析和执行本地的数据库事务
                        *Gson gson = new Gson();
                        Map<String, String> map = new HashMap<>();
                        map = gson.fromJson(new String(messageExt.getBody()), map.getClass());
                        MessageContent messageContent = new MessageContent(map.get("userName"),map.get("operationCode"),map.get("money"));
                        bankService.add(messageContent);*
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //稍后再试
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //消费成功
            });
            consumer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

4.总结

rocketMQ事务消息有四个对象
一是要有一个生产者
二是要有一个生产者监听器,监听生产者发送消息并执行本地事务和实现回查函数
三是有一个消费者
四是有一个消费者的消息监听器

4.1.生产者

生成一个生产者:
TransactionMQProducer producer = new TransactionMQProducer(“wangdinew”);

该语句生成了一个rockermq消息的生产者。并且给这个生产者配置了组,组名是“wangdinew”.所有组名为“wangdinew”的生产者都生产同一个topic的消息,并且在事务消息中,如果一个生产者组里的某个生产者发了一个prepare消息后还没收到本地事务状态就宕机了,那同组的其他生产者可以顶上,来执行后续的接收本地事务状态和提交或者回滚prepare消息的功能。

配置这个生产者的某些属性:
生成一个生产者后这个生产者很多属性都有自己的默认值,并不需要配置,有的属性最好配置一下。

producer.setNamesrvAddr(“localhost:9876”);
此处nameserve属性配置成了本地的rocketmq的地址和端口。因为我本地启动了rocketmq的nameserve服务,生产者需要与一个nameserve保持着长连接,定时查询topic配置信息,比如生产者要发送某个topic的消息该往哪个broker发这个broker的路径信息。当一个nameserve挂掉生产者会自动连接下一个nameserver。默认时,生产者每30秒从nameserver获取一下所有topic的最新情况,这意味着如果某个broker宕机,生产者最多要30秒才能知道,在这个期间,发往这个宕机broker的消息发送失败。轮询时间由DefaultMQProducer的pollNameServerInteval参数决定,可以手动配置。

producer.setExecutorService(executorService);
这是配置生产者发送完prepare消息后本地事务执行器

producer.setVipChannelEnabled(false);
配置关闭vip通道

producer.setTransactionListener(transactionListener);
配置监听器!!必须要配置呀!
还有很多属性可以配置,如果没有特殊需求一般就默认了。

启动生产者
producer.start();

发送消息
//讲消息体转为json
String jsonstr = JSON.toJSONString(messageContent);
//生成一条rocketmq消息,topic=newwangdi-topic,tag=newwangdi-tag 消息内容以byte格式传递
Message msg = new Message(“newwangdi-topic”, “newwangdi-tag”, jsonstr.getBytes(RemotingHelper.DEFAULT_CHARSET));
//发送消息msg;其中第二个参数会传递给监听器executeLocalTransaction函数
sendResult = producer.sendMessageInTransaction(msg,null);

4.2生产者监听器

重写executeLocalTransaction
主要就是根据本地事务的执行结果返回三个状态
TransactionStatus.CommitTransaction: 提交事务,它意味着本地事务执行成功,允许消费者消费此消息。
TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。

重写checkLocalTransaction
broker服务器端有一个定时任务,会定时去prepare消息队列检查看看那个消息还没有收到确认消息,如果查到某个消息还没有确认的话就会从远程调用该方法来查看那个消息对应的本地事务是否执行成功。

4.3消费者

创建一个消费者对象

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(“wangdi-new-consumer”);
该语句生成了一个rocketmq的消费者并配置了该消费者所在的组为“wangdi-new-consumer”。消息的消费与否是面对着消费组的。
有消费组A和消费组B
每组各有两个消费者A1,A2,B1,B2
topic-a的消息a1若被A1消费了则A2便不能再消费这条消息。但是消费组B的B1或者B2可以消费这个消息,同理若被B1消费则B2不能再消费。

配置消费者属性

consumer.setNamesrvAddr(“localhost:9876”);
配置消费者的nameserve地址。
单个消费者和一台nameserver保持长连接,定时查询topic配置信息,如果该nameserver挂掉,消费者会自动连接下一个nameserver,直到有可用连接为止,并能自动重连。默认情况下,消费者每隔30秒从nameserver获取所有topic的最新队列情况,这意味着某个broker如果宕机,客户端最多要30秒才能感知。该时间由DefaultMQPushConsumer的pollNameServerInteval参数决定,可手动配置。

consumer.subscribe(“l-topic”, “l-tag”);
配置该消费者要消费的主题和tag

consumer.registerMessageListener();
配置消费者的消息监听器,应该实现一个消息监听器,此处偷懒监听器为斜体部分。
consumer.registerMessageListener((MessageListenerConcurrently) (list, context) -> {
try {
for (MessageExt messageExt : list) {
log.info("消费消息: " + new String(messageExt.getBody()));//输出消息内容
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();
map = gson.fromJson(new String(messageExt.getBody()), map.getClass());
MessageContent messageContent = new MessageContent(map.get(“userName”),map.get(“operationCode”),map.get(“money”));
bankService.add(messageContent);
}
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //稍后再试
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //消费成功
}
);

下面还有示例配置:

// 批量消费,每次拉取10条
consumer.setConsumeMessageBatchMaxSize(10);
//设置广播消费
consumer.setMessageModel(MessageModel.BROADCASTING);
//设置集群消费
consumer.setMessageModel(MessageModel.CLUSTERING);
// 如果非第一次启动,那么按照上次消费的位置继续消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

启动消费者
consumer.start();

4.4 消息监听器

(MessageListenerConcurrently) (list, context) -> {
try {
//遍历消息
for (MessageExt messageExt : list) {
//消费消息
log.info("消费消息: " + new String(messageExt.getBody()));//输出消息内容

                }
            } catch (Exception e) {
                e.printStackTrace();
                //返回消费消息的状态--消费失败
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //稍后再试
            }
           //消费消息的状态--消费成功
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //消费成功
        }

问题:

如果在生产者发送消息语句后加一句produce.shutdown()
executeLocalTransaction函数提交状态为unknow时,发送语句就算是执行完了并且会调用shutdown关闭channel,此时消息仍然是prepare状态,当broker端的定时任务来回查状态时因为找不到连接就会报错,而发送的消息就永远是prepare—版本4.9.2
报错:
2021-11-03 09:04:38 WARN Transaction-msg-check-thread - Check transaction failed, channel table is empty. groupId=wangdinew

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值