简历旅游网站开发经验,网站仿站大多少钱,品牌建设的最高境界,郴州新网官网应用的幂等是在分布式系统设计时必须要考虑的一个方面#xff0c;如果对幂等没有额外的考虑#xff0c;那么在消息失败重新投递#xff0c;或者远程服务重试时#xff0c;可能会出现许多诡异的问题。本文一起来看一下#xff0c;在消息队列应用中#xff0c;如何处理因为…应用的幂等是在分布式系统设计时必须要考虑的一个方面如果对幂等没有额外的考虑那么在消息失败重新投递或者远程服务重试时可能会出现许多诡异的问题。本文一起来看一下在消息队列应用中如何处理因为重复投递等原因导致的幂等问题。
对业务幂等的理解
首先明确一下幂等并不是问题而是业务的一个特性。幂等问题体现在对于不满足幂等性的业务在消息重复消费或者远程服务调用失败重试时出现的数据不一致业务数据错乱等现象。
幂等最早是一个数学上的概念幂等函数指的是对一个函数或者方法使用相同的参数执行多次数据结果是一致的。
以 HTTP 协议为例我们知道 HTTP 协议中定义了交互的不同方法比如 GET 和 POST以及 PUT、DELETE 等其中 GET、DELETE 等方法都是幂等的而 POST 方法不是。
这个很好理解GET 方法用于获取资源不管调用多少次接口结果都不会改变所以是幂等的DELETE 等可以类比。
这里有一点需要注意业务上的幂等指的是操作不影响资源本身并不是每次读取的结果都保证一致。比如通过 GET 接口查询一条订单记录在多次查询的时间段内订单状态可能会有新的更新而发生变化查询到的数据可能不同但是读接口本身仍然是一个幂等的操作。
在业务开发中对数据的操作主要是 CRUD即在做数据处理时的 Create、Read、Update、Delete 这几种操作。很明显这里的 Create 操作不是幂等的Update 操作可能幂等也可能不幂等。例如现在有一个订单表下面的操作就是幂等的
UPDATE order SET status1 WHERE id100下面的这个操作就不符合幂等性的要求
UPDATE order SET priceprice1 WHERE id100对应的Read 和 Delete 操作则是幂等的。
各类中间件对幂等性的处理
幂等处理不好可能会出现很多问题比如使用 binlog 分发进行数据同步如果数据库更新消息被多次消费可能会导致数据的不一致。 远程服务调用的幂等问题
因为存在网络抖动等远程服务调用出现失败一般是通过配置重试保证请求调用成功率提高整体服务的可用性。
以 Apache Dubbo 为例我一直觉得 Dubbo 对容错的支持特别全面它支持多种集群容错的方式并且可以针对业务特性配置不同的失败重试机制包括 Failover 失败自动切换、Failsafe 失败安全、Failfast 快速失败等。比如在 Failover 下失败会重试两次在 Failfast 下失败则不会重试直接抛出异常。
Dubbo 的容错机制考虑了多种业务场景的需求根据不同的业务场景可以选择不同的容错机制进而有不同的重试策略保证业务正确性。
Dubbo RPC 的重试和容错机制不是本课时的重点如果想对 Dubbo 集群容错方式有进一步的了解可以点击查看 Dubbo 官方文档。 消息消费中的重试问题
从本质上来讲消息队列的消息发送重试和微服务中的失败调用重试是一样的都是通过重试的方式解决网络抖动、传输不稳定等导致的偶发调用失败。这两者其实是一个问题两个问题的解决方式也可以互相借鉴。
在分布式系统中要解决这个问题需从中间件和业务的不同层面来保证服务调用的幂等性。下面从消息队列投递语义以及业务中如何处理幂等两个方面进行拆解。
消息投递的几种语义
为了进一步规范消息的调用业界有许多消息队列的应用协议其中也对消息投递标准做了一些约束。 At most once
消息在传递时最多会被送达一次在这种场景下消息可能会丢但绝不会重复传输一般用于对消息可靠性没有太高要求的场景比如一些允许数据丢失的日志报表、监控信息等。 At least once
消息在传递时至少会被送达一次在这种情况下消息绝不会丢但可能会出现重复传输。
绝大多数应用中都是使用至少投递一次这种方式同时大部分消息队列都支持到这个级别应用最广泛。 Exactly once
每条消息肯定会被传输一次且仅传输一次并且保证送达因为涉及发送端和生产端的各种协同机制绝对的 Exactly once 级别是很难实现的通用的 Exactly once 方案几乎不可能存在可以参考分布式系统的「FLP 不可能定理」。
我觉得消息投递的语义和数据库的隔离级别很像不同语义的实现付出的成本也不一样。上面定义的消息投递语义主要在消息发送端在消费端也可以定义类似的消费语义比如消费端保证最多被消费一次至少被消费一次等这两种语义是相对应的可以认为是同一个级别的两种描述。
不同消息队列支持的投递方式
以 RocketMQ 为例我们来看下对应的投递支持。
RocketMQ 支持 At least once 的投递语义也就是保证每个消息至少被投递一次。在 RocketMQ 中是通过消费端消费的 ACK 机制来实现的 在消息消费过程中消费端在消息消费完成后才返回 ACK如果消息已经 pull 到本地但还没有消费则不会返回 ack 响应。 在业务上应用 RcoketMQ 时也可以根据不同的业务场景实现其他级别的投递语义比如最多送达一次等由于篇幅限制这里不展开详细讲解了感兴趣的同学可以查阅 RocketMQ 相关的源码和文档学习。
业务上如何处理幂等
消息消费的幂等和我们在上一课时中提到的时序性一样本质上也是一个系统设计的问题。
消息队列是我们为了实现系统目标而引入的手段之一并且分布式消息队列天然存在消费时序、消息失败重发等问题。所以要保证消息队列的消费幂等还是要回到业务中结合具体的设计方案解决。
天然幂等不需要额外设计
参考上面对 HTTP 协议方法的幂等性分析有部分业务是天然幂等的这部分业务允许重复调用即允许重试在配置消息队列时还可以通过合理的重试来提高请求的成功率。
利用数据库进行去重
业务上的幂等操作可以添加一个过滤的数据库比如设置一个去重表也可以在数据库中通过唯一索引来去重。
举一个例子现在要根据订单流转的消息在数据库中写一张订单 Log 表我们可以把订单 ID 和修改时间戳做一个唯一索引进行约束。
当消费端消费消息出现重复投递时会多次去订单 Log 表中进行写入由于我们添加了唯一索引除了第一条之外后面的都会失败这就从业务上保证了幂等即使消费多次也不会影响最终的数据结果。
设置全局唯一消息 ID 或者任务 ID
还记得我们在第 15 课时「分布式调用链跟踪」中提到的调用链 ID 吗调用链 ID 也可以应用在这里。我们在消息投递时给每条业务消息附加一个唯一的消息 ID然后就可以在消费端利用类似分布式锁的机制实现唯一性的消费。
还是用上面记录订单状态流转消息的例子我们在每条消息中添加一个唯一 ID消息被消费后在缓存中设置一个 Key 为对应的唯一 ID代表数据已经被消费当其他的消费端去消费时就可以根据这条记录来判断是否已经处理过。
总结
本文分享了消息幂等的知识点包括对幂等的理解以及消息队列投递时的不同语义另外简单介绍了业务上处理幂等的两种方式。
西方有一句谚语当你有了一个锤子你看什么都像钉子。在我刚开始学习分布式系统时学习了各种中间件每个中间件都希望能用上这其实脱离了系统设计的初衷。
到这里已经展开了许多分布式系统的常用组件提到这个谚语主要是希望你在做技术方案特别是做分布式系统设计方案时不是为了设计而设计。方案设计的目的是实现业务目标并不是在系统中加入各种高大上的中间件这个方案就是正确的。
我之前读过一本《系统之美》的图书从复杂系统的角度来看系统中的元素越多为了维持系统的平衡需要付出的势能必然也越大。
对应到系统设计中系统拆解的粒度越大对应各个组件之间的耦合就越小但是需要解决的组件协同问题也越多实现数据的一致性也越困难。我们在系统设计时要避免过度设计把握技术方案的核心目的在这个基础上进行针对性设计。
对于本文的内容你可以思考下当前的项目中是如何处理重复消息的有没有考虑消息处理的幂等性欢迎留言分享。