咱们小伙伴应该都据说够消息中间件MQ,如:RabbitMQ,RocketMQ,Kafka等。引入中间件的好处能够起到抗高并发,削峰,业务解耦的做用。redis
如上图:算法
(1)订单服务投递消息给MQ中间件(2)物流服务监听MQ中间件消息,从而进行消费
咱们这篇文章讨论一下,如何保障订单服务把消息成功投递给MQ中间件,以RabbitMQ举例。sql
小伙伴们对此会有些疑问,订单服务发起消息服务,返回成功不就成功了吗?以下面的伪代码:数据库
上面代码中,通常发送消息就是这么写的,小伙伴们以为有什么问题吗?缓存
下边说一个场景,若是MQ服务器忽然宕机了会出现什么状况?是否是咱们订单服务发过去的消息所有没有了吗?是的,通常MQ中间件为了提升系统的吞吐量会把消息保存在内存中,若是不做其余处理,MQ服务器一旦宕机,消息将所有丢失。这个是业务不容许的,形成很大的影响。服务器
有经验的小伙伴会说,我知道一个方法就是把消息持久化,RabbitMQ中发消息的时候会有个durable参数能够设置,设置为true,就会持久化。网络
这样的话MQ服务器即便宕机,重启后磁盘文件中有消息的存储,这样就不会丢失了吧。是的这样就必定几率的保障了消息不丢失。并发
但还会有个场景,就是消息刚刚保存到MQ内存中,但尚未来得及更新到磁盘文件中,忽然宕机了。(我靠,这个时间这么短,也会出现,几率过低了吧),这个场景在持续的大量消息投递的过程当中,会很常见。异步
那怎么办?咱们如何做才能保障必定会持久化到磁盘上面呢?分布式
上面问题出如今,没有人告诉咱们持久化是否成功。好在不少MQ有回调通知的特性,RabbitMQ就有confirm机制来通知咱们是否持久化成功?
confirm机制的原理:
(1)消息生产者把消息发送给MQ,若是接收成功,MQ会返回一个ack消息给生产者;(2)若是消息接收不成功,MQ会返回一个nack消息给生产者;
上面的伪代码,有两个处理消息方式,就是ack回调和nack回调。
这样是否是就能够保障100%消息不丢失了呢?
咱们看一下confirm的机制,试想一下,若是咱们生产者每发一条消息,都要MQ持久化到磁盘中,而后再发起ack或nack的回调。这样的话是否是咱们MQ的吞吐量很不高,由于每次都要把消息持久化到磁盘中。写入磁盘这个动做是很慢的。这个在高并发场景下是不可以接受的,吞吐量过低了。
因此MQ持久化磁盘真实的实现,是经过异步调用处理的,他是有必定的机制,如:等到有几千条消息的时候,会一次性的刷盘到磁盘上面。而不是每来一条消息,就刷盘一次。
因此comfirm机制实际上是一个异步监听的机制,是为了保证系统的高吞吐量,这样就致使了仍是不可以100%保障消息不丢失,由于即便加上了confirm机制,消息在MQ内存中尚未刷盘到磁盘就宕机了,仍是无法处理。
说了这么多,仍是无法确保,那怎么办呢???
其实本质的缘由是没法肯定是否持久化?那咱们是否是能够本身让消息持久化呢?答案是能够的,咱们的方案再一步的演化。
上图流程:
(1)订单服务生产者再投递消息以前,先把消息持久化到Redis或DB中,建议Redis,高性能。消息的状态为发送中。(2)confirm机制监听消息是否发送成功?如ack成功消息,删除Redis中此消息。(3)若是nack不成功的消息,这个能够根据自身的业务选择是否重发此消息。也能够删除此消息,由本身的业务决定。(4)这边加了个定时任务,来拉取隔必定时间了,消息状态仍是为发送中的,这个状态就代表,订单服务是没有收到ack成功消息。(5)定时任务会做补偿性的投递消息。这个时候若是MQ回调ack成功接收了,再把Redis中此消息删除。
这样的机制其实就是一个补偿机制,我无论MQ有没有真正的接收到,只要个人Redis中的消息状态也是为【发送中】,就表示此消息没有正确成功投递。再启动定时任务去监控,发起补偿投递。
固然定时任务那边咱们还能够加上一个补偿的次数,若是大于3次,仍是没有收到ack消息,那就直接把消息的状态设置为【失败】,由人工去排查究竟是为何?
这样的话方案就比较完美了,保障了100%的消息不丢失(固然不包含磁盘也坏了,能够作主从方案)。
不过这样的方案,就会有可能发送屡次相同的消息,颇有可能MQ已经收到了消息,就是ack消息回调时出现网络故障,没有让生产者收到。
那就要要求消费者必定在消费的时候保障幂等性!
咱们先了解一下什么叫幂等?在分布式应用中,幂等是很是重要的,也就是相同条件下对一个业务的操做,无论操做多少次,结果都是同样。
为何要有幂等这种场景?由于在大的系统中,都是分布式部署,如:订单业务 和 库存业务有可能都是独立部署的,都是单独的服务。用户下订单,会调用到订单服务和库存服务。
由于分布式部署,颇有可能在调用库存服务时,由于网络等缘由,订单服务调用失败,但其实库存服务已经处理完成,只是返回给订单服务处理结果时出现了异常。这个时候通常系统会做补偿方案,也就是订单服务再此放起库存服务的调用,库存减1。
这样就出现了问题,其实上一次调用已经减了1,只是订单服务没有收处处理结果。如今又调用一次,又要减1,这样就不符合业务了,多扣了。
幂等这个概念就是,无论库存服务在相同条件下调用几回,处理结果都同样。这样才能保证补偿方案的可行性。
借鉴数据库的乐观锁机制,如:
根据version版本,也就是在操做库存前先获取当前商品的version版本号,而后操做的时候带上此version号。咱们梳理下,咱们第一次操做库存时,获得version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version仍是1,再执行上面的sql语句时,就不会执行;由于version已经变为2了,where条件就不成立。这样就保证了无论调用几回,只会真正的处理一次。
原理就是利用数据库主键去重,业务完成后插入主键标识
上面的sql语句:
好处:实现简单
坏处:高并发下数据库瓶颈
解决方案:根据ID进行分库分表进行算法路由
利用redis的原子操做,作个操做完成的标记。这个性能就比较好。但会遇到一些问题。
第一:咱们是否须要把业务结果进行数据落库,若是落库,关键解决的问题时数据库和redis操做如何作到原子性?
这个意思就是库存减1了,但redis进行操做完成标记时,失败了怎么办?也就是必定要保证落库和redis 要么一块儿成功,要么一块儿失败
第二:若是不进行落库,那么都存储到缓存中,如何设置定时同步策略?
这个意思就是库存减1,不落库,直接先操做redis操做完成标记,而后由另外的同步服务进行库存落库,这个就是增长了系统复杂性,并且同步策略如何设置
若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:787707172,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。