RocketMQ--消息类型

《RocketMQ实战与原理解析》学习笔记
https://help.aliyun.com/document_detail/29533.html

名词解释

topic && tag

Topic : 消息主题,一级消息类型,通过 Topic 对消息进行分类。
Tag : 消息标签,二级消息类型,用来进一步区分某个 Topic 下的消息分类。 RMQ 允许消费者按照 Tag对消息进行过滤,确保消费者最终只消费到他关注的消息类型。

//制定topic && tag 进行消息过滤
consumer.subscribe("topic1", "TagA || TagC || TagD");

到底什么时候该用 Topic,什么时候该用 Tag?
以天猫交易平台为例,订单消息,支付消息属于不同业务类型的消息,

  • 分别创建 Topic_Order 和 Topic_Pay 两个topic
  • 其中订单消息根据商品品类以不同的 Tag 再进行细分,如电器类、男装类、女装类、化妆品类,最后他们都被各个不同的系统所接收。

通过合理的使用 Topic 和 Tag,可以让业务结构清晰,更可以提高效率。

Message

Message : 消息,消息队列中信息传递的载体。
Message ID: 消息的全局唯一标识,由消息队列 RMQ 系统自动生成,唯一标识某条消息。
Message Key: 消息的业务标识,由消息生产者(Producer)设置,唯一标识某个业务逻辑

普通消息 (集群消息)
一个 Group ID 所标识的所有 Consumer 平均分摊消费消息。
例如某个 Topic 有 9 条消息,一个 Group ID 有 3 个 Consumer 实例,那么在集群消费模式下每个实例平均分摊,只消费其中的 3 条消息

广播消息
一个 Group ID 所标识的所有 Consumer 都会各自消费某条消息一次。例如某个 Topic 有 9 条消息,一个 Group ID 有 3 个 Consumer 实例,那么在广播消费模式下每个实例都会各自消费 9 条消息.

定时消息 && 延时消息
定时消息
Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。

延时消息
Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

顺序消息
消息队列 RocketMQ 提供的一种按照顺序进行发布和消费的消息类型,分为全局顺序消息分区顺序消息

全局顺序消息
对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。

分区顺序消息
对于指定的一个 Topic,所有消息根据 Sharding Key 进行区块分区。同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding Key是顺序消息中用来区分不同分区的关键字段,和普通消息的 Message Key是完全不同的概念。

事务消息
消息队列 RocketMQ 提供类似 X/Open XA 的分布事务功能,通过消息队列 RocketMQ 的事务消息能达到分布式事务的最终一致.


不同类型消息的发送&消费

普通消息

普通消息是指消息队列 RMQ 中无特性的消息,区别于有特性的在这里插入代码片定时/延时消息、顺序消息和事务消息。

发送普通消息(3种方式)

  • 可靠同步发送: 同步发送是指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。

在这里插入图片描述

  • 可靠异步发送: 异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。 消息队列 RocketMQ 的异步发送,需要用户实现异步发送回调接口(SendCallback)。消息发送方在发送了一条消息后,不需要等待服务器响应即可返回,进行第二条消息发送。发送方通过回调接口接收服务器响应,并对响应结果进行处理。

在这里插入图片描述

  • 单向(Oneway)发送: 单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答在这里插入图片描述
    下表概括了三者的特点和主要区别。
    在这里插入图片描述

demo

public class DifferentWaySend {
	private static int TASK_NUM = 100;

	public static void main(String[] args) throws Exception {
		DefaultMQProducer producer = new DefaultMQProducer("gp1");
		producer.setNamesrvAddr("s157:9876;s158:9876");
		producer.start();

		List<Message> messageList = new ArrayList<>();
		IntStream.rangeClosed(1,TASK_NUM).forEach(taskNo ->{
			try {
				messageList.add( new Message(
						"topic1",
						"tag1_1",
						("Hello RocketMQ " + taskNo).getBytes(RemotingHelper.DEFAULT_CHARSET)));
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		});


		sync(producer,messageList);
		async(producer,messageList);
		oneway(producer,messageList);


		producer.shutdown();
	}

	public static void sync(DefaultMQProducer producer , List<Message> messageList) throws Exception {
		Instant start= Instant.now();
		for(Message msg : messageList) {
			//同步发送消息
			SendResult sendResult = producer.send(msg);
//			System.out.println("sync:"+sendResult.getMsgId());
		}
		Instant end= Instant.now();
		System.out.println("-----------------------------------sync 共耗时:" + ChronoUnit.MILLIS.between(start,end) + "ms");
	}

	public static void async(DefaultMQProducer producer , List<Message> messageList) throws Exception {
		CountDownLatch countDownLatch = new CountDownLatch(TASK_NUM);

		Instant start= Instant.now();
		for(Message msg : messageList) {
			producer.send(msg,new SendCallback() {
				@Override
				public void onSuccess(SendResult sendResult) {
//					System.out.println("async:"+sendResult.getMsgId());
					countDownLatch.countDown();
				}

				@Override
				public void onException(Throwable e) {
					System.out.println("async:"+e.getMessage());
					countDownLatch.countDown();
				}
			});
		}

		countDownLatch.await();
		Instant end= Instant.now();
		System.out.println("-----------------------------------async 共耗时:" + ChronoUnit.MILLIS.between(start,end) + "ms");

	}

	public static void oneway(DefaultMQProducer producer , List<Message> messageList) throws Exception {
		Instant start= Instant.now();
		for(Message msg : messageList) {
			// 由于在 oneway 方式发送消息时没有请求应答处理,一旦出现消息发送失败,则会因为没有重试而导致数据丢失。若数据不可丢,建议选用可靠同步或可靠异步发送方式。
			producer.sendOneway(msg);
		}

		Instant end= Instant.now();
		System.out.println("-----------------------------------oneway 共耗时:" + ChronoUnit.MILLIS.between(start,end) + "ms");

	}

}

执行结果:
在这里插入图片描述

消费消息(集群模式&&广播模式)

//设置为广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);

//集群模式(默认)
consumer.setMessageModel(MessageModel.CLUSTERING);

consumer.registerMessageListener(new MessageListenerConcurrently() {
		@Override
		public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
			for (MessageExt message : messages) {
				System.out.println(message);
			}
			return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
		}
	});

定时&延时消息

定时消息与延时消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到消息队列 RocketMQ 服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。

  • 发送定时消息需要明确指定消息发送时间点之后的某一时间点作为消息投递的时间点。
  • 发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。

setStartDeliverTime()

// 延时消息,单位毫秒(ms),在指定延迟时间(当前时间之后)进行投递,例如消息在 3 秒后投递
long delayTime = System.currentTimeMillis() + 3000;
// 设置消息需要被投递的时间
msg.setStartDeliverTime(delayTime);


// 定时消息,单位毫秒(ms),在指定时间戳(当前时间之后)进行投递,例如 2019-08-01 16:21:00 投递。如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者。
long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-08-01 16:21:00").getTime();
msg.setStartDeliverTime(timeStamp);

开源版本中,RMQ并不支持精度为秒级别的延迟消息

setDelayTimeLevel()
开源版本中,只支持特定的延时级别level
在服务器端(rocketmq-broker端)的属性配置文件中加入以下行:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
来设置默认级别(上述为默认配置).

 //level=0 级表示不延时,level=1 表示 1 级延时,level=2 表示 2 级延时,以此类推。
//level==3 ,表示10s后投递任务
message.setDelayTimeLevel(3);

收发定时&&延时消息
与普通消息的收发方式相同, 不同的是消息的属性不同。

顺序消息

顺序消息指消息发布和消息消费都按顺序进行。

  • 顺序发布:对于指定的一个 Topic,客户端将按照一定的先后顺序发送消息。
  • 顺序消费:对于指定的一个 Topic,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。

全局顺序
对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。
在这里插入图片描述

示例
在证券处理中,以人民币兑换美元为 Topic,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO 的方式进行发布和消费。

分区顺序
对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。
Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。

在这里插入图片描述

示例
例一:用户注册需要发送发验证码,以用户 ID 作为 sharding key, 那么同一个用户发送的消息都会按照先后顺序来发布和消费。

例二:电商的订单创建,以订单 ID 作为 sharding key,那么同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息都会按照先后顺序来发布和消费。

消息类型对比
在这里插入图片描述
发送方式对比
在这里插入图片描述
发送顺序消息-MessageQueueSelector()

String orderId = "Order_0000001";

//msg-key: "PAY_201907151223001" 标识此条消息业务id
//msg-key: 以方便您在无法正常收到消息情况下,可通过控制台查询消息并补发。
String payId = "PAY_201907151223001";
Message msg = new Message("pay", "TAG1", "PAY_201907151223001" ,
		("支付消息,内容为:xxxxx " ).getBytes(RemotingHelper.DEFAULT_CHARSET));

 // 分区顺序消息中区分不同分区的关键字段,sharding key 于普通消息的 key 是完全不同的概念。
// 全局顺序消息,该字段可以设置为任意非空一个字符串常量即可。
String shardingKey = orderId;
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
	@Override
	public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
		//arg为后续传递的shardingKey,可以根据hash算法or其他方法来计算出id;
		//可参考hashmap的hash算法;
		int id = hash(arg);
		int index = id % mqs.size();
		return mqs.get(index);
	}
}, shardingKey);

接收顺序消息-MessageListenerOrderly()

consumer.registerMessageListener(new MessageListenerOrderly() {

       @Override
       public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
                                                  ConsumeOrderlyContext context) {
          //处理消息...
          return ConsumeOrderlyStatus.SUCCESS;

       }
    });

多知道一点—分布式事务

引用: https://www.jianshu.com/p/c26b3af5880f

微服务倡导将复杂的系统拆分为若干个简单、职责单一、松耦合的服务,可以降低开发难度,便于敏捷开发。而对大多数中小型公司来说,实施微服务架构面临以下困难:

  • 单体应用拆分为分布式系统后,应用间的通讯和故障处理机制变得复杂
  • 微服务化后,一个简单的功能需要调用多个服务并操作多个数据库实现,数据一致性难以保障
  • 大量的微服务,导致其测试、维护、部署变得困难

为了保障微服务架构下数据的一致性,通常需要引入分布式事务来解决,当前比较流行的分布式解决方案如下。

基于二阶段提交的XA协议

  • 第一阶段:协调者询问所有参与者是否可以执行提交操作,参与者执行准备工作,例如为资源上锁,预留资源,写undo/redo log。
  • 第二阶段:若所有参与者回应“可提交”,则向所有参与者发送正式提交命令;若某个参与者回应“拒绝提交”,则向所有参与者发送回滚命令。
    在这里插入图片描述
    XA协议保障了事务的强一致性,然而由于其采用的阻塞协议带来的巨大性能开销,难以达到较高的系统吞吐量。

TCC模式
TCC提供了一种全局事务解决方案,业务系统只需实现下面三个操作,即可完成分布式事务:

  • TRY:完成参与者业务检查并预留业务资源
  • CONFIRM:使用TRY阶段的预留业务资源,并执行业务
  • CANCEL:释放TRY结算预留的业务资源

在这里插入图片描述
TCC模式可以让业务更灵活地定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能,然而它对业务的侵入度较高,实现难度较大

事务消息
通过消息的异步事务,可以保证本地事务和消息发送同时执行成功或失败,从而保证了数据的最终一致性。

  • 发送prepare消息,该消息对Consumer不可见
  • 执行本地事务
  • 若本地事务执行成功,则向MQ提交消息确认发送指令;若本地事务执行失败,则向MQ发送取消指令
  • 若MQ长时间未收到确认发送或取消发送的指令,则向业务系统询问本地事务状态,并做补偿处理

RMQ事务消息

在这里插入图片描述
其中:

  1. 发送方向消息队列 RocketMQ 服务端发送消息。
  2. 服务端将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
  3. 发送方开始执行本地事务逻辑。
  4. 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
  5. 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
  6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  7. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半消息进行操作。

说明:事务消息发送对应步骤 1、2、3、4,事务消息回查对应步骤 5、6、7

关键代码

  • producer
TransactionListener transactionListener = new DeducationTransactionListenerImpl();
//`producer`需要绑定transactionListener
producer.setTransactionListener(transactionListener);

//`producer`需要sendMessageInTransaction方法发送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
  • TransactionListener
public class DeducationTransactionListenerImpl implements TransactionListener {

	//当发送prepare(half)消息成功后,会执行此逻辑
	@Override
	public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
		LocalTransactionState state ;
		//todo 执行业务方法,并根据执行结果,返回state 
		return state;
	}

	/**
	 * 当没有回应prepare(half)消息时,brokder会检查此条消息的状态
	 * @param msg
	 * @return
	 */
	@Override
	public LocalTransactionState checkLocalTransaction(MessageExt msg) {
		LocalTransactionState state ;
		//todo 查看订单bizNo的状态,并返回state 
		return  state;
	}
}

TransactionStatus

  • TransactionStatus.CommitTransaction 提交事务,允许订阅方消费该消息。
  • TransactionStatus.RollbackTransaction 回滚事务,消息将被丢弃不允许消费。
  • TransactionStatus.Unknow 暂时无法判断状态,期待固定时间以后消息队列 RocketMQ 服务端向发送方进行消息回查。

Message设置消息回查时间

/**
 * 	在消息属性中添加第一次消息回查的最快时间,单位秒。
 * 	例如,以下设置实际第一次回查时间为 120 秒 ~ 125 秒之间
 *
 * 	以上方式只确定事务消息的第一次回查的最快时间,实际回查时间向后浮动0~5秒;
 * 	如第一次回查后事务仍未提交,后续每隔5秒回查一次。
	 */
msg.putUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS,"120");
  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值