SpringCloudAlibaba - 阿里系消息中间件RocketMQ

关于消息中间件及各种MQ对比,见博主之前的博客:ActiveMQ/RabbitMQ;本文讲解阿里开源中间件RocketMQ。

一. 环境搭建

上传最新的RocketMQ安装包 rocketmq-all-4.6.0-bin-release。

1. 解压配置文件

unzip rocketmq-all-4.6.0-bin-release.zip

报错:-bash: unzip: 未找到命令;解决办法:yum install -y unzip zip

2. 修改bin目录下NameServer、Broker服务器内存  默认为4g内存

# runserver.sh
JAVA_OPT="${JAVA_OPT} -server –Xms128m –Xmx128m –Xmn128m"

# runbroker.sh
JAVA_OPT="${JAVA_OPT} -server –Xms128m –Xmx128m –Xmn128m"

3. 启动NameServer

nohup sh bin/mqnamesrv &

4. 启动mqbroker

  虚拟机环境:

nohup sh bin/mqbroker -c ./conf/broker.conf -n 127.0.0.1:9876 &

  阿里云环境需要指定外网IP:

echo "brokerIP1=47.104.xx.xxx" > broker.properties
nohup sh bin/mqbroker -n ${namesrvIp}:9876 -c ./broker.properties &

5. Rocketmq-console控制端使用

  去官网http://rocketmq.apache.org/下载RocketMQ源码,把rocketmq-console导入IDEA,修改配置文件一行代码即可:

rocketmq.config.namesrvAddr=47.104.xx.xxx:9876

二. SpringBoot简单整合RocketMQ


    org.apache.rocketmq
    rocketmq-spring-boot-starter
    2.0.3
rocketmq:
  # nameServer连接地址
  name-server: 47.104.xx.xxx:9876
  producer:
    group: my_producer #生产者必须要有分组,可能是自带的一个bug
server:
  port: 8088
@Data
@AllArgsConstructor
@ToString
public class OrderEntity  implements Serializable {
    private String orderId;
    private String orderName;
}
@RestController
public class ProducerController {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @RequestMapping("/sendMsg")
    public String sendMsg() {
        OrderEntity orderEntity = new OrderEntity("123456","腾讯视频永久会员");
        rocketMQTemplate.convertAndSend("my-topic", orderEntity);
        return "success";
    }
}
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;

@Service
@RocketMQMessageListener(topic = "my-topic", consumerGroup = "my-topic-consume-group")
public class OrderConsumer implements RocketMQListener {
    @Override
    public void onMessage(OrderEntity o) {
        System.out.println("o:" + o.toString());
    }
}

    

可以发现,生产者发送消息后,消费者能够实时监听,注意:生产者和消费者的topic须保持一致。

三. RocketMQ进阶

1. RocketMQ集群架构

SpringCloudAlibaba - 阿里系消息中间件RocketMQ_第1张图片  SpringCloudAlibaba - 阿里系消息中间件RocketMQ_第2张图片

RocketMQ 四种集群部署方式

① 单个Master节点, 缺点就是如果宕机之后可能整个服务不可用;

②  多个Master节点,分摊存放我们的消息,缺点:没有Slave节点,主的Master节点宕机之后消息数据可能会丢失的;

③ 多个Master和Slave节点 采用异步形式 效率非常高 数据可能短暂产生延迟(毫秒级别的,建议,如上图)

④ 多个Master和Slave节点 采用同步形式, 效率比较低、数据不会产生延迟。

集群方式环境搭建:参考博客:https://www.cnblogs.com/kevingrace/p/9015836.html,https://blog.csdn.net/leexide/article/details/80035470

brokerClusterName = myCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
#namesrvAddr=192.168.0.5:9876;192.168.0.6:9876

brokerClusterName = myCluster
brokerName = broker-b
brokerId = 0
deleteWhen = 04
fileReservedTime 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
#namesrvAddr=192.168.0.5:9876;192.168.0.6:9876

上面是双主模式(没有从),保证brokerClusterName,brokerName不同即可
且nameServer没有做集群,都用的105,所以注释掉namesrvAddr,如果双nameServer,则打开注释
1,先启动192.168.0.5的nameserver
2.启动192.168.0.5的broker
3.启动192.168.0.6的broker

2. RocketMQ消息顺序性问题(其实这种场景很少,除非要求十分苛刻的业务)

RocketMQ中,topic是队列的集合,队列是先进先出,只要有一个消费者,肯定是能够保证顺序的;不过RocketMQ不同,因为一个Broker分了4个队列,消息时存到不同队列的,相当于分摊存放到不同队列,消费者消费时无法拿到全局数据。

【顺序消息】最大的问题是消费者集群,队列集群(1个Broker分为4个队列,极大提高吞吐量)

解决顺序问题的核心思路:
① 相同的业务逻辑一定要放在同一个队列中(比如,新增,修改,删除等,是有顺序的,把这3个业务放在同一个队列)
② 每个队列必须要对应同一个消费者
③ RocketMQ中队列和消费者比例为1:1

  代码实现:(以下代码场景:单台RocketMQ,即一个Broker,4个队列)

@RestController
public class ProducerController {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @RequestMapping("/sendMsg")
    public String sendMsg() throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
        Long orderId = System.currentTimeMillis();
        String insertSql = getSqlMsg("insert", orderId);
        String updateSql = getSqlMsg("update", orderId);
        String deleteSql = getSqlMsg("delete", orderId);
        Message insertMsg = new Message("zb-topic", insertSql.getBytes());
        Message updateMsg = new Message("zb-topic", updateSql.getBytes());
        Message deleteMsg = new Message("zb-topic", deleteSql.getBytes());
        DefaultMQProducer producer = rocketMQTemplate.getProducer();

        rocketMQTemplate.getProducer().send(insertMsg, new MessageQueueSelector() {
            @Override
            public MessageQueue select(List mqs, Message msg, Object arg) {
                // 该消息存放到队列0中
                return  mqs.get(0);
            }
        }, orderId);
        rocketMQTemplate.getProducer().send(updateMsg, new MessageQueueSelector() {
            @Override
            public MessageQueue select(List mqs, Message msg, Object arg) {
                return  mqs.get(0);
            }
        }, orderId);
        rocketMQTemplate.getProducer().send(deleteMsg, new MessageQueueSelector() {
            @Override
            public MessageQueue select(List mqs, Message msg, Object arg) {
                return  mqs.get(0);
            }
        }, orderId);

        return orderId + "";
    }
    public String getSqlMsg(String type, Long orderId) {
        JSONObject dataObject = new JSONObject();
        dataObject.put("type", type);
        dataObject.put("orderId", orderId);
        return dataObject.toJSONString();
    }
}
/**
 * ConsumeMode.ORDERLY 相当于一个队列对应一个线程
 * consumeThreadMax 可以设为4,四个线程去消费broker的信息,也能保证顺序问题,只不过线程不同
 */
@Service
@RocketMQMessageListener(topic = "zb-topic", consumerGroup = "zbTopic", 
        consumeMode = ConsumeMode.ORDERLY, consumeThreadMax = 1)
public class OrdeConsumer implements RocketMQListener {

    @Override
    public void onMessage(MessageExt message) {
        System.out.println(Thread.currentThread().getName() + "," +
                "队列" + message.getQueueId() + "," + new String(message.getBody()));
    }
}

四. RocketMQ解决分布式事务

分布式事务解决方案有很多很多,博主在之前的博客也讲解了几个,如RabbitMQ最终一致性,LCN等。本次案例为:

SpringCloudAlibaba - 阿里系消息中间件RocketMQ_第3张图片

如图所示,相信我们都定过外卖,当提交订单后会在数据库生成一条订单,然后等待分配骑手送餐。

该业务在SpringCloud微服务架构拆分为两个服务,订单服务service-order和派单服务service-distribute,订单服务添加订单后,通过feign客户端调用派单服务的接口进行分配骑手,那么分布式事务问题就来了,当订单服务调用完第二行代码,派单接口执行完毕,咔嚓,第三行报了个错,那么订单接口会回滚,而派单则已提交事务,那么就造成数据不一致问题,本文用博主推荐的第三种方式RocketMQ来解决该分布式事务问题。

  首先进行service-order服务编写生产者,即订餐者:

@RestController
public class ProducerController {
    @Autowired
    private ProducerService producerService;

    @RequestMapping("/sendOrder")
    public String sendOrder() {
        return producerService.saveOrder();
    }
}
@Service
public class ProducerService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /** 提交订单业务逻辑 */
    public String saveOrder() {
        // 提前生成我们的订单id
        String orderId = System.currentTimeMillis() + "";
        /**
         * 1.提前生成我们的半消息
         * 2.半消息发送成功之后,在执行我们的本地事务
         */
        OrderEntity orderEntity = this.createOrder(orderId);
        String msg = JSONObject.toJSONString(orderEntity); //需转为JSONString类型
        MessageBuilder stringMessageBuilder = MessageBuilder.withPayload(msg);
        stringMessageBuilder.setHeader("msg", msg);
        Message message = stringMessageBuilder.build();
        // 该api(sendMessageInTransaction)即为事务消息,俗称半消息
        rocketMQTemplate.sendMessageInTransaction("orderProducerGroup",
                "orderTopic", message, null);
        // 一旦发送成功,直接去执行监听器SyncProducerListener的executeLocalTransaction方法
        return orderId;

    }
    /** 封装订单实体类 */
    public OrderEntity createOrder(String orderId) {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setName("阿堡仔炸鸡汉堡-丁豪广场店");
        orderEntity.setOrderCreatetime(new Date());
        orderEntity.setOrderMoney(15d); // 价格是15元
        orderEntity.setOrderState(0); // 状态为未支付
        Long commodityId = 30L;
        orderEntity.setCommodityId(commodityId); // 模拟商品id为30
        orderEntity.setOrderId(orderId);
        return orderEntity;
    }
}

  RocketMQ事务监听器

@Component
@RocketMQTransactionListener(txProducerGroup = "orderProducerGroup")
public class SyncProducerListener implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private TransactionUtils transactionUtils;

    /**
     * 执行我们订单的事务(本地事务)
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {

        MessageHeaders headers = msg.getHeaders();
        Object object = headers.get("msg");
        if (object == null) {
            return RocketMQLocalTransactionState.ROLLBACK; //rollback,删除半飙戏
        }
        String orderMsg = (String) object;
        OrderEntity orderEntity = JSONObject.parseObject(orderMsg, OrderEntity.class);
        TransactionStatus transactionStatus = null;
        try {
            transactionStatus = transactionUtils.begin();
            int result = orderMapper.addOrder(orderEntity);
            transactionUtils.commit(transactionStatus); // 本地事务提交即可,不用管result
            if (result == 0) {
                return RocketMQLocalTransactionState.ROLLBACK; //RocketMQ回滚,只要是rollback,则直接删除半消息
            }
            // 告诉我们的Broker可以消费者该消息
            // 一旦RocketMQ提交后,直接走到OrderConsumer的监听器,不会走下面checkLocalTransaction方法
            return RocketMQLocalTransactionState.COMMIT;
//            return null; 如果返回null,或者Rocket..State.UNKNOWN,则等待60s走下面的checkLocalTransaction方法,
        } catch (Exception e) {
            if (transactionStatus != null) {
                transactionUtils.rollback(transactionStatus);
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        }
        return null;
    }

    /**
     * 提供给Broker定时检查
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        MessageHeaders headers = msg.getHeaders();
        Object object = headers.get("msg");
        if (object == null) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        String orderMsg = (String) object;
        OrderEntity orderEntity = JSONObject.parseObject(orderMsg, OrderEntity.class);
        String orderId = orderEntity.getOrderId();
        // 直接查询我们的数据库(即用业务判断事务结果)
        OrderEntity orderDbEntity = orderMapper.findOrderId(orderId);
        if (orderDbEntity == null) {
            return RocketMQLocalTransactionState.UNKNOWN; //不确认,继续重试
        }
        return RocketMQLocalTransactionState.COMMIT;
    }
}

  由于需要手动提交/回滚事务,贴上一段事务工具类:

@Service
public class TransactionUtils {

    @Autowired
    public DataSourceTransactionManager transactionManager;

    public TransactionStatus begin() {
        TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionAttribute());
        return transaction;
    }
    public void commit(TransactionStatus transaction) {
        transactionManager.commit(transaction);
    }
    public void rollback(TransactionStatus transaction) {
        transactionManager.rollback(transaction);
    }
}

  最后开始进行service-distribute服务,编写派单监听器

@Service
@RocketMQMessageListener(topic = "orderTopic", consumerGroup = "myTopicGroup")
public class OrderConsumer implements RocketMQListener {

    @Autowired
    private DispatchMapper dispatchMapper;

    @Override
    public void onMessage(String msg) {
        OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
        String orderId = orderEntity.getOrderId();
        // 模拟userId为136
        DispatchEntity dispatchEntity = new DispatchEntity(orderId, 136L);
        // 添加派单(具体逻辑肯定没这么简单,根据业务编写相应代码即可)
        dispatchMapper.insertDistribute(dispatchEntity);
    }
}

  【RocketMQ解决分布式事务总结】:

SpringCloudAlibaba - 阿里系消息中间件RocketMQ_第4张图片

 Rocketmq解决分布式事务的核心思路:

1>  生产者向我们的Broker(MQ服务器端)发送我们派单消息设置为半消息,该消息不可以被消费者消费。

2>  在执行我们的本地的事务,将本地执行事务结果提交或者回滚告诉Broker

3>  Broker获取本地事务的结果如果是为提交的话,将该半消息设置为允许被消费者消费,如果本地事务执行失败的情况下,将该半消息直接从Broker中移除

4>  如果我们的本地事务没有将结果及时通知给我们的Broker(网络波动等原因),这时候我们Broker会主动定时(默认60s)查询本地事务结果,最多重试15次,超时则删除半消息。

5>  如何获取本地事务执行结果?直接查询下订单在数据库是否存在即可,本地事务结果实际上就是一个回调方法,根据自己业务场景封装本地事务结果。

至此,SpringCloudAlibaba阿里系消息中间件RocketMQ基本结束,需要源码的小伙伴私聊我即可,免费赠送 ~ 

你可能感兴趣的:(SpringCloudAlibaba - 阿里系消息中间件RocketMQ)