《RocketMQ实战与原理解析》学习笔记
https://help.aliyun.com/document_detail/29533.html
Topic : 消息主题,一级消息类型
,通过 Topic 对消息进行分类。
Tag : 消息标签,二级消息类型
,用来进一步区分某个 Topic 下的消息分类。 RMQ 允许消费者按照 Tag
对消息进行过滤,确保消费者最终只消费到他关注的消息类型。
//制定topic && tag 进行消息过滤
consumer.subscribe("topic1", "TagA || TagC || TagD");
到底什么时候该用 Topic,什么时候该用 Tag?
以天猫交易平台为例,订单消息,支付消息属于不同业务类型的消息,
两个topic
不同的 Tag
再进行细分,如电器类、男装类、女装类、化妆品类,最后他们都被各个不同的系统所接收。通过合理的使用 Topic 和 Tag,可以让业务结构清晰,更可以提高效率。
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)
。消息发送方在发送了一条消息后,不需要等待服务器响应即可返回,进行第二条消息发送。发送方通过回调接口接收服务器响应
,并对响应结果进行处理。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,所有消息按照严格的先入先出(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协议
阻塞协议
带来的巨大性能开销,难以达到较高的系统吞吐量。TCC模式
TCC提供了一种全局事务解决方案,业务系统只需实现下面三个操作,即可完成分布式事务:
TCC模式可以让业务更灵活地定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能,然而它对业务的侵入度较高,实现难度较大
。
事务消息
通过消息的异步事务,可以保证本地事务和消息发送同时执行成功或失败,从而保证了数据的最终一致性。
Consumer不可见
说明:事务消息发送对应步骤 1、2、3、4,事务消息回查对应步骤 5、6、7
。
关键代码
TransactionListener transactionListener = new DeducationTransactionListenerImpl();
//`producer`需要绑定transactionListener
producer.setTransactionListener(transactionListener);
//`producer`需要sendMessageInTransaction方法发送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
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");