前言:
insert into order(num) value 0;
update order set num = 100,where orderId = 20;
这两条sql要是执行顺序不一致会发生啥问题?下了订单迟迟不支付,堆压一堆未支付的信息在后台线程扫描?会出现什么问题?
1.为什么基于rocketmq进行订单库数据同步时会消息乱序?
1.1.大数据团队同步订单数据库的技术方案回顾:大数据系统直接跑复杂的大sql在订单系统的数据库上出一些数据报表,是会严重影响订单系统的性能的,所以基于canal这样的中间件去监听订单数据库的binlog,就是一些增删改操作的日志,然后将这些binlog发送到mq中去;
1.2.问题:订单数据库的binlog消息乱序:
例如: insert into order(num) value 0;
update order set num = 100,where orderId = 20;如果这两条语句出现乱序问题,出来的数据完全就是不一样的结果;
1.3.乱序的原因:
每个topic可以指定多个messagequeue,也就是当写入i奥西的时候,会将topic消息均匀的分布给不同的message queue的,假设insert 的binlog写入一个messagequeue中,update的binlog写入另外一个messagequeue中,接着大数据系统在获取binlog的时候,可能会部署多台机器组成一个consumer group,对于consumer group中的每台机器都会负责消费一部分messagequeue的消息,所以可能一台从consumer01获取令insert log,另一台从consume queue02中获取倒来update binlog,完全就有可能先执行的update,再执行的inset;
1.4.如何解决这个问题呢?
让同一个订单的binlog进入一个message queue中。
1.4.1考虑问题:
mysql数据的binlog一定也是有顺序的,例如先执行了insert,然后是update,当从MySQL数据库中获取binlog的时候此时也必须要按照binog的顺序来获取的,也就是说比如canal作为一个中间件从MySQL那里监听和获取binlog,那么binlog传输到canal那里监听和获取binlog,那么binlog传递到canal的时候,也必然是先后顺序的,先insert再update;接着将binlog发送给mq的时候,必须将一个订单的binlog都发送到一个message queue中去,而且发送过去的时候也必须严格按照顺序发送,只有这样才能让一个订单的binlog进入同一个messagequeue,而且还是有序的
1.5:消息处理原理分析:
一个consumer可以处理多个messagequeue消息,但是一个messagequeue只能给一个consume进行处理,所以一个订单的binlog只会有序的交给一个consumer来进行处理,这样的话一个大数据系统就可以获取到一个订单的有序binlog,然后有序的根据binlog将数据还原到自己的存储中;【消息乱序解决初稿】
1.6:可能引发问题:
consumer处理消息的时候,可能出现很多原因执行失败,此时返回RECONSUME_LATER状态,broker会稍后重试,那这样不又乱序了?
1.7:1.6问题解决分析:
对于有序消息的方案中,如果遇到消息处理失败的场景,就必须要返回SUSPEND_CURRENT_QUEUE_A-MOMENT这个状态,意思是先等一会,一会再处理这一整批消息,而不是直接将消息放入重试队列中,而是直接处理下一批消息先;
2:基于订单数据库同步场景,来分析rocketMQ的顺序消息机制的代码实现Demo
2.1:1.如何让一个订单的binlog进入一个messagequeue?
SendResult sendResult = producr.send(
message,
new MessageQueueSelector(){
@Overrid
public MessageQueue select(
List<Messagequeue> mqs,
Message msg,Object aeg
){
Long orderId = (Long)arg;//根据订单id选择发送queue
Long index = id % mqs.size();//用订单id对message queue数量取模
return mqs.get((int) index); //返回一个message queue
}
},
orderId//传入订单id
)
代码片段中可以看到,关键的因素就是两个,一个是发送消息的时候传入一个messageQueueSelector,在这个里面要根据订单id和MessageQueue数量去选择这个订单id的数据进入哪个Messagequeue,同时在发送消息的时候除了带上消息自己以外,还要带上订单id,如何messagequeueSelector就会根据订单id去选择一个messagequeue发送过去,就可以保证一个订单多个binlog都会进入一个messagequeue中去。
2.2:消费者如何保证按照顺序来获取一个messagequeue中的消息?
consumer.registerMessageListener(
new MessageListenerOrderly(){
@Override
public ConsumeOrderlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeOrderlyContext context){
context.setAutoCommit(true);
try{
for(MessageExt msg:msgs){
//对有序消息进行消息处理
}
return ConsumeOrderlyStatus.SUCCESS;
}catch(Exception e){
//如果消息处理有问题,返回一个状态,暂停一会再继续发送这批消息
return SUSPEND_CUPRENT_QUEUE_A_MOMENT;
}
}
}
)
MessageListenerOrderly这个Orderly的意思也就是说consumer会对每一个consume queue,都仅仅用一个线程来处理其中的消息。如果交给多线程处理的话,还是会出现乱序的;
3.基于rocketmq的数据过滤机制,提升订单数据库同步的处理效率:tags
一个数据库会包含很多张表的数据,比如说订单数据库,除了订单,还包含了很多其他的表,所以在进行数据库binlog同步的时候,很有可能是将一个数据库所有表的binlog都推送到mq中去的,所以在mq的某个人topic中,可能是混杂了订单数据库中的几个甚至十几个表的binlog数据的,不一定仅仅包含了想要推动表的binlog数据;
3.1:场景举例:
假设大数据系统仅仅关注订单数据库中的表A的binlog,并不关注其他表的binlog,那么大数据系统可能需要在获取到所有表的binlog之后,对每条binlog判断一下,是不是表A的binlog,如果不是表A的binlog直接丢弃不处理,如果是表A的binlog,再进行处理,这样的话必然就会导致大数据系统处理很多不关注的表的binlog,也会很浪费时间,降低消息的效率;【引入tag数据过滤机制】
3.2:问题解决方案:
发送消息的时候,给消息设置tag和属性,针对这个问题,可以采用
rocketMQ支持的数据过滤机制,来让大数据系统仅仅关注他想要的表的binlog数据即可
;
Message msg = new Message(
"TopicOrderDbData",//订单数据库写入的topic
“tableA”,//这条数据的tag,可以是表的名称
("binlog").getBytes(RemotingHelper.DEFAULT_CHARSET)//这是一条binlog数据
);
//给消息设置一些属性
msg.putUserProperty("a",10);
msg.putUserProperty("b","abc");
其实是可以给消息设置tag/属性等等多个附加信息的,在消费数据的时候根据tag和属性进行过滤
,片段demo:
可以在消费的时候根据tag和属性进行过滤,例如可以通过下面的代码去指定
例如只要tag = tableA和tag = tableB的数据
consumer.subscribe(“TopicOrderDbData","tableA||tableB”)
也可以使用语法指定:例如:
consumer.subscribe(
"TopicOrderDbData"
MessageSelecor.bysql("a>5 AND b = 'abc')
)
4.基于延迟消息机制优化大量订单的定时退款扫描问题+订单定时退款场景
【订单扫描积压场景:简洁场景图】
问题:订单系统的后台线程必须要不停的扫描各种未支付的订单:
1.未支付的订单可能是比较多的,然后还需要不停的扫描,每个未支付的订单可能都要被扫描N多变,才会发现已经超过了30分钟没有支付了;
2.另外一个是很难去分布式并行扫描订单,业务假设订单数据量特别的多,打算部署订单扫描服务,但是问题,每台机器扫描哪些订单?怎么扫描?什么时候扫描?这都是一些麻烦的问题;
4.1:MQ的延迟消息出场,针对扫描问题
在实际的项目中,mq的延迟消息往往使用的是很多的,所谓的延迟消息,也就是说,在订单系统中创建了一个订单,可以发送一条消息到MQ中去,指定这条消息是延迟消息,例如设置等待30分钟后,才能被订单扫描服务给消费到;
【一图惊醒梦中人:可能只有如花能做到了】
不爽可以继续优化一下:如果订单数量很多,完全可以让订单扫描服务多部署一些机器,mq中的topic多指定一个messagequeue,这样每个订单扫描服务的机器作为一个consumer都会处理一部分订单的查询任务;
4.2:生产案例:订单定时退款场景,分析rocketMQ的延迟消息代码实现Demo:
producer生产者:
public class ScheduledMessageProducer{
public static void main(String[] args)throws Exception{
//订单系统的生产者
DefaultMQPruducer producer = new DefaultMQProducer("OrderSystemProducerGroup");
//启动生产者
producer.start();
Message message = new Message(
"CreateOrderInfoformTopic",//创建订单通知topic
orderInfoObject.getBytes()//订单信息的json串
);
//设置消息为延迟消息,延迟级别为3
message.setDelayTimeLevel(3);
//发送消息
producer.send(message);
}
}
consumer消费者:
public class ScheduledMessageConsumer
public static void main(String[] args)throws Exception{
//订单消息服务的消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderScanServletConsumer");
//订阅订单创建通知topic
consumer.subscribe("CreateOrderInfoformTopic","*");
//注册消息监听者
consumer.registerMessageListener(new MessageListenerConcurrentiy(){
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages,consumeConcurrentlyContext context){
for(MessageExt message:messages){
//打印消息存储时间到消费时间的差值
.........................
}
}
})
}
rocketMq进行一个小总结《暂时更新到这里,后续还会持续更新一些企业型的权限问题和消息的链路追踪等等》:
1.灵活运用的tags来过滤数据,合理规划topic和里面的tags来过滤数据,
2.基于消息的key来定位消息是否丢失 如何从mq中查消息是否丢失?基于消息key就可以实现了,例如通过下面的方式设置一个消息的key为订单id:message.setKeys(orderId),这样这个消息就具备一个key了,接着这个消息到broker上面,会基于key构建hash索引,这个hash索引就放在indexFile索引文件中,通过mq提供的命令去根据key 查询这个消息例如 mqadmin queryMsgByKey -n 127.0.0.1:9876 -t SCANRECORD -K orderId
3.消息零丢失方案补贴 一般金融级别,银行,支付系统等等,一般假设mq机器彻底奔溃了,生产者就应该把消息写入到本地磁盘文件进去持久化,或者是写入数据库,等待mq恢复之后,再把持久化的消息继续投递到mq中去。
4.三种提高消费者的吞吐量:
第一种,部署更多的consumer机器,但是要注意,如果部署了5台consumer,topic的messagequeue就要对应增加,要不然consumer有一台是空闲的;
第二种就是增加consumer的线程数量,可以设置consumer端的参数:consumeThreadMin/consumeThreadMax,这样一台consumer机器尚消费线程多,消费速度就越快。
第三种:还可以开启消费者的批量消费功能,就是设置consumeMessageBatchMaxSize参数,默认是1,可以根据需求多设置一些,那么一次就会交给你的回调函数一批消息来处理,通过批量处理消息的方式,也可以大幅度提升消息消费的进度;
5.要不要消费历史消息 consumer支持设置从哪里开始消费消息的,两种:第一种就是从topic的第一条数据开始消费,一个是从最后一次消费国的消息之后开始消费,
对应的是: CONSUME_FROM_LAST_OFFSET CONSUME_FROM_FIRST_OFFSET
一般正常情况下都是选择从最后一次消费过的消息之后开始消费:CONSUME_FROM_FIRST_OFFSET,后续每一次重启,都是从上一次消费到的位置继续往后消费。
RocketMq暂时完结散花了,谢谢!