自信和希望是青年的特权。
——大仲马
我在《12.项目中为什么要使用消息队列》中列举了两个使用消息队列的例子。
(1)收银系统,确认收款成功,通过MQ通知给物流系统发货。
(2)消费积分,用户每消费一笔给用户增加一定积分,京东豆,信用卡积分,2020年如果还没倒闭的电商平台中,可以100%的确定订单系统和积分/奖励系统不是耦合在一起的。
这些都是很典型的使用消息队列的场景。
那么问题来了,想象一下,积分系统收到同一个用户同一个订单两条相同的消息会怎样?积分会被加两遍吗?针对这个问题,面试官又开始一轮三连问,你还能扛得过去吗?
这不是“面试造火箭,工作拧螺丝”,消息重复,消息积压这类问题是你入职后工作中真真切切会遇到的,不是面试官故意刁难你。
问题分析:
还是拿上面的例子分析,积分系统收到同一个用户同一个订单两条相同的消息会怎样,先不管因为什么原因消息发了两次,积分会被加两遍吗?
产品经理说: 那肯定不行呀,花100块给100个积分,积分没有买一送一服务。
订单系统RD说: 我这边没办法100%保证积分广播只发一次,万一出个bug同一笔消费积分,消息可能发了几百次也不好说。
我:
产品说不行,订单RD说他不保证消息不重复,Kafka架构RD也说无法保证消息不重复,那怎么办?我是负责积分系统的,针对消息重复问题,我会针对积分累计接口做**“幂等”**设计,这个问题,首先我们应该从上游就做消息去重处理,但是我们不能100%相信上游系统一定可靠,我是消息消费端,只有我这边做了幂等设计才能完全避免这种和钱相关的bug,毕竟如果依赖上游,真的出了用户消费100元最后加了100w积分,这锅重要还是我们背。
我可以根据用户订单号或者流水号做强幂等,每成功操作一次加积分就记录下来,即使消息重负了,我只要判断同一个订单号已经操作加分了,后续我们就不会再做任何操作了。
随手写了一段伪代码给面试官:
//没收到给用户消费通知,先判断这个orderId时候已经有加过积分的历史记录,如果没有操作过,则增加。如果已经操作过,直接返回不做任何处理。
List lists = userPointDao.queryHistory(orderId);
if(CollectionUtils.isNotEmpty(lists)){
//1.加100分。
userPoint.add(pointCount,orderId);
//2.保存增加记录
userPoint.addHistory(orderId);
}else{
log.info("该订单已经操作过积分操作")
return null;
}
Tip:如果幂等还不明白可以看我写的《谈谈怎么理解接口幂等设计,项目中如何保证接口幂等》,上面的代码加积分和保存增加记录要保证事务性,如果你不知道ACID千万别给自己挖坑,被面试官逮住ACID一顿问。
面试官:这个问题相对不难,有解决思路问题就不大了。
问题分析:
这个问题什么意思呢,比如一个消息Producer发送顺序是1 2 3,那Consumer接收到的消息也是 1 2 3 ,这就比较为难工程师了,但是还是有办法的,想要实现消息有序就要牺牲点什么东西 ---- 性能/可靠性。
我:
这个问题从三个角度考虑:
但是这些方法都会牺牲掉系统的性能和稳定性,顺序性问题非要使用MQ来做,那也没有太好的办法了。
问题分析:
说真的,工作中要求消息顺序消费的业务场景真的挺少见的,用到的时候少,你可以不用深入研究这一块,知道方法就行,到时候真的遇到了知道从哪个方向下手。
我:
用当前比较流行的RocketMQ和Kafka举例。
Tip:
Topic就是一个字符串,给同一类消息取个名字加以区分,如:topic.com.xxx.order.orderId,大多数用户都可以通过message key来定义,因为同一个key的message可以保证只发送到同一个partition,比如说key是user id,table row id等等。
关于消息重复和消息顺序消费问题解决思路比较简单,都是一些小技巧,虽然内容比较枯燥,但是我已经尽力说得通俗易懂。
如果用两句话概括这一接的内容:
当然面试的时候你可别这么干巴巴两句话,那显得你太没水平了,面试最理想的效果就是无论多简单的问题你都能滔滔不绝,让面试官无话可说。