RocketMQ幂等性顺序性实战
1. rocketmq源码安装
参考官方文档:http://rocketmq.apache.org/docs/quick-start/
安装好jdk和maven rocketmq安装包:https://pan.baidu.com/s/1I3CqWaxFnxtUX1kJpIJkcQ 密码: vu5m
代码:https://github.com/2466845324/repository/tree/master/rocketmq
1. 解压源码包 unzip rocketmq-all-4.4.0-source-release.zip
2. 重命名并进目录 mv rocketmq-all-4.4.0 rocketmq && cd rocketmq
3. 编译打包(会下载很多依赖,大概10分钟) mvn -Prelease-all -DskipTests clean install -U
4. 启动nameserver服务 cd distribution/target/apache-rocketmq/bin && sh mqnamesrv
* 默认配置是4g, 如果你的服务器配置较低则会报错, 则要修改
vim runbroker.sh 和 vim runserver.sh 这两个文件,具体参数自己设定。
例如配置:JAVA_OPTAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=320m"
5. 后台启动 nohup sh mqnamesrv & tail -f nohup.out
6. 启动brokerserver服务 nohup sh mqbroker -n localhost:9876 &
7. 查看状态 jps
rocketmq控制台安装
1. 解压安装包 unzip rocketmq-externals-master.zip && cd rocketmq-externals-master
2. 修改pom文件版本号 cd rocketmq-console && vim pom.xml 修改为 <rocketmq.version>4.4.0</rocketmq.version>
3. 指定机器地址 vim src/main/resources/application.properties 修改为rocketmq.config.namesrvAddr=127.0.0.1:9876
4. 在rocketmq-console目录下编译 mvn clean package -Dmaven.test.skip=true
5. 启动 cd target && nohup java -jar rocketmq-console-ng-1.0.0.jar &
6. 查看日志 tail -f nohup.out 启动测试 192.168.100.100:8080
2. 消息幂等性
生产者发送消息之后,为了确保消费者消费成功 我们通常会采用手动签收方式确认消费,MQ就是使用了消息超时、重传、确认机制来保证消息必达。
场景:
1. 订单服务(生产者),点击结算订单之后需要付款,这时就会发送一条“结算”的消息到mq的broker中。
2. 此时支付服务(消费者)监听到这条消息之后就会处理结算扣款的逻辑,然后手动签收订单告诉mq我已经消费完成了。
3. 如果在结算的过程中程序出现了异常,我们就返回mq一个消费失败的状态,此时mq就会重新发送这条消息;
或者是由于网络波动支付服务一直没有响应消息的消费状态,mq也照样会重新发送这条消息。
4. 那么这种情况下,支付服务(消费者)就会重复收到这条消息,如果不做任何判断就有可能会重复消费出现多次扣款的情况。
解决方案:
在发送消息的时候,我们可以生成一个唯一ID标识每一条消息,将消息处理成功和去重日志通过事物的形式写入去重表或缓存中。每次消费之前都先查一遍,如果存在就说明消费过了直接返回消费成功。
3. 消息顺序性
消息有序是指按照消息的发送顺序来消费。例如我们有这样一个需求:我们通过读取sql的日志文件得到它的所有sql,然后通过发送消息给其他服务去执行这些sql来同步数据。这个时候顺序性就显得尤为重要了。比如我们先修改这条数据,然后删除数据。倘若消费的时候先删除了再修改这时候得到的数据就不一致了。
解决方案:
我们往一个topic里面发送消息时,它默认会维护几个队列,是随机发送到这些队列里面的。消费者集群消费时,实际上是一个服务监听一个队列。我们发送消息时,对于同个数据的操作指定发送到一个队列就好了,这时消费者就是按照顺序来消费的。
4. 顺序幂等案例实战
比如我们现在我们有下面9条消息要发送,分别是对用户、订单、商品的操作,最终的结果应该是“用户被删除”,“订单已完成”,“商品被下架”。如果就条消息随机指定队列发送的话,就可能用户先被删除了,然后进行修改;也有可能支付订单的过程比较慢一直没反馈,从而收到多条消息而重复支付。
生产者对同一数据的操作发送到同一队列
public List<Data> getOrderList(){ List<Data> list = new ArrayList<Data>(); list.add(new Data(1,"注册用户")); list.add(new Data(2,"创建订单")); list.add(new Data(1,"修改用户")); list.add(new Data(2,"支付订单")); list.add(new Data(1,"删除用户")); list.add(new Data(3,"商品上架")); list.add(new Data(2,"完成订单")); list.add(new Data(3,"商品打折")); list.add(new Data(3,"商品下架")); return list; } /** * @Title:顺序发送 * @author:吴磊 * @date:2019年5月4日 下午10:28:31 */ @RequestMapping("/send") public Object send() throws Exception{ // 1. 获取操作数据 List<Data> list = getOrderList(); Message message = null; for(int i=0; i<list.size(); i++) { Data data = list.get(i); // keys是唯一标识, 用作重试幂等判断. String keys = data.getId()+""; // 2. 装载消息 (topic, 标签, key, 消息体) message = new Message(PayOrderlyProducer.TOPIC,"test_tag",keys,data.getType().getBytes()); /* 3. 投递消息 * 每一个topic默认为4个queue, 我们可以指订队列发送. * send(Message msg, MessageQueueSelector selector, Object arg),会将arg作为队列下标传 * 给MessageQueueSelector中MessageQueue的arg,从而选出具体的queue. */ SendResult sendResult = payOrderlyProducer.getProducer().send(message, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { int id = (int) arg; int index = id % mqs.size();//对queue总数取模 return mqs.get(index); } },data.getId()); System.out.printf("发送结果=%s, sendResult=%s ,orderid=%s, type=%s\n", sendResult.getSendStatus(), sendResult.toString(),data.getId(),data.getType()); } return "OK"; }
消费者手动签收消息,如果消费失败就重复消费,如果重试次数过多就通知运营人员手动处理。
public PayOrderlyConsumer() throws MQClientException { consumer = new DefaultMQPushConsumer(consumerGroup); // 指定mq服务器地址 consumer.setNamesrvAddr(NAME_SERVER); // 指定消费策略:这里是从最后一条消费 consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); //默认是集群方式,可以更改为广播,但是广播方式不支持重试 //consumer.setMessageModel(MessageModel.CLUSTERING); consumer.subscribe(TOPIC, "test_tag"); /** * MessageListenerOrderly是单线程的消费 */ consumer.registerMessageListener( new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { MessageExt msg = msgs.get(0); int times = msg.getReconsumeTimes(); try { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msgs.get(0).getBody())); //模拟异常 String str = new String(msgs.get(0).getBody()); if(str.contains("订单")) { int i=1/0; } //做业务逻辑操作 System.out.println("消费成功"); return ConsumeOrderlyStatus.SUCCESS; } catch (Exception e) { System.out.println("重试次数"+times); //如果重试2次不成功,则记录,人工介入 if(times >= 2){ System.out.println("重试次数大于2,记录数据库,发短信通知开发人员或者运营人员"); return ConsumeOrderlyStatus.SUCCESS; } e.printStackTrace(); return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; } } }); consumer.start(); System.out.println("consumer start ..."); }
执行结果:可以看到id相同的数据(对同一数据的操作),都发送到同一队列了,然后消费者对每一个队列进行顺序消费,消费失败就会触发重试机制。