艺术~如何保证数据100%发送、幂等设计方案

如何保证数据100%发送

比如我们现在有一个订单服务消息,我们如何保证将这个消息100%发送给物流服务呢?

下面说说我的解决思路。
在这里插入图片描述
(1)订单服务投递消息给MQ中间件
(2)物流服务监听MQ中间件消息,从而进行消费

  • 以RabbitMQ举例。

下边说一个场景,如果MQ服务器突然宕机了会出现什么情况?是不是我们订单服务发过去的消息全部没有了吗?是的,一般MQ中间件为了提高系统的吞吐量会把消息保存在内存中,如果不作其他处理,MQ服务器一旦宕机,消息将全部丢失。这个是业务不允许的,造成很大的影响。

那这个问题的解决办法就是将这个订单服务消息持久化,那么即使MQ宕机了,重启后还能正常让物流监听到。

  • 持久化

有经验的小伙伴会说,我知道一个方法就是把消息持久化,RabbitMQ中发消息的时候会有个durable参数可以设置,设置为true,就会持久化。

这样的话MQ服务器即使宕机,重启后磁盘文件中有消息的存储,这样就不会丢失了吧。是的这样就一定概率的保障了消息不丢失。

但还会有个场景,就是消息刚刚保存到MQ内存中,但还没有来得及更新到磁盘文件中,突然宕机了。(我靠,这个时间这么短,也会出现,概率太低了吧),这个场景在持续的大量消息投递的过程中,会很常见。
那怎么办?我们如何做才能保障一定会持久化到磁盘上面呢?这就需要mq的一个confirm机制。

  • confirm机制

上面问题出现在,没有人告诉我们持久化是否成功。好在很多MQ有回调通知的特性,RabbitMQ就有confirm机制来通知我们是否持久化成功?

confirm机制的原理:

(1)消息生产者把消息发送给MQ,如果接收成功,MQ会返回一个ack消息给生产者;
(2)如果消息接收不成功,MQ会返回一个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)select查询天然幂等;
2)delete删除也是幂等,删除同一个多次效果一样;
3)update直接更新某个值的,幂等;
4)update更新累加操作的,非幂等;
5)insert非幂等操作,每次新增一条。

产生原因

由于重复点击或者网络重发

1)点击提交按钮两次;
2)点击刷新按钮;
3)使用浏览器后退按钮重复之前的操作,导致重复提交表单;
4)使用浏览器历史记录重复提交表单;
5)浏览器重复的HTTP请;
6)nginx重发等情况;
7)分布式RPC的try重发等。

什么时候需要

业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况,这种情况应该保证幂等。

还有用户在APP上连续点击了多次提交订单,后台应该只产生一个订单。

向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。

所以很显然,声明幂等的服务应该认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。

幂等的不足

幂等是为了简化客户端逻辑处理,却增加了服务提供者的逻辑和成本,是否有必要,需要根据具体场景具体分析,因此除了业务上的特殊要求外,尽量不提供幂等的接口。

  1. 增加了额外控制幂等的业务逻辑,复杂化了业务功能;

  2. 把并行执行的功能改为串行执行,降低了执行效率

解决方案

页面重定向

在前端表单提交后执行页面重定向,转到提交成功信息页面。
这能避免用户按键导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。

优点:实现简单
缺点:可靠性不足

乐观锁

一般通过mysql存储version字段来做乐观锁,这样既能保证执行效率,又能保证幂等。
例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version#
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。
我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传递的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。

优点:实现较为简单
缺点:效率不是很好

分布式锁

比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号,继续后续操作,如果已经存在这个订单号那么就放弃这个请求的操作。
查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

优点:实现简单
缺点:ID一旦重复就会有很大的问题

token令牌

这种方式分成两个阶段:申请token阶段和支付阶段。 第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。 第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。 实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。不足是需要系统间交互两次,流程较上述方法复杂。

优点:实现简单
缺点:token令牌的生成算法选择很重要

猜你喜欢

转载自blog.csdn.net/Shangxingya/article/details/115056971