如何保证用户重试操作的幂等性

如何,保证,用户,重试,操作 · 浏览次数 : 367

小编点评

1. **使用多种校验机制**:除了接口校验外,还可以考虑其他校验机制,例如前端验证、数据库检查等,以提高服务稳定性。 2. **利用可扩展的架构**:设计可扩展的架构,能够根据实际需求进行调整,例如可扩展的服务器、缓存等。 3. **关注服务可靠性的监控**:建立完善的服务监控系统,实时监控服务性能、可用性等指标,及时发现并解决问题。 4. **通过测试发现问题**:使用测试用例和模拟场景,对服务进行测试,以提前发现并解决问题。 5. **提供异常处理机制**:设计完善的异常处理机制,以捕获并处理各种异常,确保服务正常运行。 6. **进行性能分析**:对服务性能进行分析,确定性能瓶颈,并针对性能瓶颈进行优化。

正文

服务不稳定是一类常态,面对此类场景恰当的应对策略应该是什么?退一步说,即使我们能够确保第一方服务的稳定性,我们又应该如何面对网络延迟以及掌控以外的不确定性?这都是本篇文章会谈到的内容

本文是团队内部分享的文字版,敏感信息已经抹去或者重写。我们通过三个实际的线上问题来看看在今后的开发过程中可以如何避免此类问题

校验是可选还是必选

用例1:学生可以在网站选择指定的日期和时间预约老师进行会议,老师也需要设定在某一时间段内可以并行服务学生的数量,毕竟她的带宽有限。但线上出现了老师在同一时间内被多个学生预约成功的情况,即预约数超出了她可以提供服务的上限。

用例2:在用户第一次访问网站前,他需要签署一系列协议。但我们发现有些协议被连续签署了多次,导致后续的功能出现了异常。在重现问题的过程中我们得知,确实可以通过复制浏览器标签的方式来重复签署同一份协议

这两个问题的修复方式是显而易见的:给后端有关接口添加校验。但问题是,它们是否可以算作开发功能的失误?用“九转大肠”问句就是:是故意的还是不小心的?

经典的风险应对模型告诉我们,根据风险的危害和发生概率,我们可以使用四种策略来处理问题:avoid、reduce、retain、transfer

在我看来模型传达给我们的不止于此;

  • 对于 retain,我认为它更想表达的不仅仅接纳(什么都不做),而是尽可能用低成本的方式去做;
  • 对于 avoid,你可能无法完美 avoid,但也许你可以把风险往其他象限转移,毕竟降低风险也是一种策略

回到这段开头的两个 case 上,我认为在功能设计之初,考虑到有限的使用频率和可承受的风险,以及无从考证的交付压力,不去接口校验没有问题。(我们一直以来缺乏对于数据增长的监控,很多问题的产生,尤其是性能问题都是在稍不留神间达到了代码能够支撑的阈值,这个问题之后再谈)。但我们真就可以什么都不用做了吗?至少我们可以让代码变得灵活一些:不需要去预测未来发生什么,让代码可能应对未来的变化即可:

于是,我们倾向于将演进能力构建到软件中,如果项目可以轻松应对变化,那么架构师就不再需要水晶球 ——《演进式架构》人民邮电出版社

关键在于,你并不需要去预测什么会变化,你需要知道的是,变化必然会发生。程序应该保证尽可能的灵活性,这样,不管未来发生什么变化,都可以应付得了——《简约之美:软件设计之道》人民邮电出版社

更复杂的问题

如果说前两个用例的症结和方案都清晰可见的话,下面这个用例也许可以带来一些思考。

假设我们需要在页面上展示申请处理进展,进展由步骤(step)构成。步骤的类型分为主步骤(step)和子步骤(sub step),可以混合使用进行串联,如下图所示

顾名思义,进展允许前进也就允许回滚。两类步骤分别有属于自己的回滚接口:

  • step 回滚:使用 PUT method 调用 /{progressID}/back
  • sub step 回滚:使用 PUT method 调用 /{progressID}/back,但是需要在 payload 里加上需要回滚的 sub step 所属的 step ID

假设目前存在一个如下图所示的步骤序列,当前的步骤位置处于尾声

如果想要把这一系列步骤正确回滚,接口的调用顺序如下:

但在排查一个问题时,我们发现用户侧的实际调用顺序是这样的:

这便导致当中的某个 sub step 被略过,数据没有被正常清除

而为什么会出现这种情况?通过 Application Insights 我们发现,用户在从点击选择发送回滚请求到服务器接收到请求,存在12秒的网络延迟,实际代码只花费了 276ms 来处理这个请求

而恰好 UI 又允许用户在等待请求的返回过程中选择重新取消等待界面,重新点击发送

于是用户在等待的过程中选择不断的重试

问题在哪

允许重试?

重试没有罪,恰恰相反,重试是我们最重要的机制。服务不稳定是一个常态,重试可以帮助我们解决相当一部分问题。例如我在排查死锁问题时,发现一旦死锁给用户带来负面影响,用户会选择刷新页面“自助”解决问题

甚至重试是应该根植在我们代码中,无论前端还是后端,用于网络请求的 client 应该对于首次失败的请求默认进行重试,无需额外的代码。

好的“基础设施”(例如日志、鉴权、重试,以及这里的重试)代码应该是毫无存在感的,很容易、甚至无意识的让人做对很多事

关于重试策略,一篇来自 AWS 社区的文章非常值得我们参考《Timeouts, retries, and backoff with jitter》,重试时我们不仅需要加入 backoff(延迟) 和 jitter(波动) 参数,还需要考虑重试给服务器带来的压力等情况

接口不够幂等?

不同的 HTTP method 是自带幂等属性的,例如 GET 天然幂等,而 POST 天然就是不幂等的。对于采用 PUT method 的 back 接口而言,也许幂等性没有做好。但是幂等性不是所有问题的挡箭牌。

想象这么一个场景:假如我们有一个用于上传特殊文件的 POST 接口 A,和只有在文件上传成功之后才能工作的功能 B。如果 B 工作时只能允许有一份上传成功的文件存在,而这个时候又是因为网络原因导致用户选择上传两遍,那么出错的是谁?

  • 用户?用户迟迟得不到反馈于是选择重新上传我不认为有什么错
  • 接口?上传文件用的 POST 接口天生不就是不幂等的吗?

除此之外幂等性也是需要代价的,在我看来一个幂等接口的完美实现可以参考这篇同样是来自 AWS 的文章《Making retries safe with idempotent APIs》,他们在请求中加入了 unique client request identifier 作为
标识符,用于后续服务判断是否已经处理过相同的请求。

上面覆盖的只是其中一类场景,实际的业务场景可能更复杂,例如要应对资源竞争的情况,如果想要了解更多接口的幂等实现,可以参考这篇文章《How to ensure idempotence》

用户行为的幂等性

如何解决此类问题,尤其是在我们解决做解决方案的时候,需要注意保证用户行为(或者说业务操作)的幂等性,而不是仅仅关注接口本身,因为一个操作通常是由多个请求,甚至前后端的配合同时完成的,例如一个 step 可不可以被回滚多次?假如一个回滚操作需要调用多个接口,部分成功会不会有任何的风险?

如何实现此类幂等性,我的建议是从以下这几个维度考虑:

  • 什么都不做优于去做些什么:我们是不是真的需要去保证幂等性?考虑到风险、概率、交付压力,什么都不做也是可以接受的

  • 预防问题优于事后补救:优先考虑从输入侧解决问题,比如从前端 UI 上控制,或者接口入口处进行校验。因为待问题出现之后再考虑修复数据的代价通常是不可控的,快速失败很重要。

  • 低成本优于高成本:如果真的要做幂等性校验,我们是不是要做端到端的整套功能?大可不必。如果风险不大,我们可以只在日志中抛出错误而不进行 UI 提示。某些校验甚至可以通过建立数据库约束来解决

  • 转移成本:GIGO (Garbage in, garbage out) 原则,不要尝试去猜测并且修复用户数据。校验失败之后我们可以把数据的修复工作交还给用户。举个不恰当的例子,假如某个后续功能需要与一个身份证件相关联,代码如果发现了多个身份证件,我们应该抛出的问题是:“我们发现了多个多个身份证件,请删除额外的多个身份证件 再重试”,而不是“我们发现了 4 个多个身份证件,请问你需要选用哪一个?”


你可能也会喜欢:

与如何保证用户重试操作的幂等性相似的内容:

如何保证用户重试操作的幂等性

服务不稳定是一类常态,面对此类场景恰当的应对策略应该是什么?退一步说,即使我们能够确保第一方服务的稳定性,我们又应该如何面对网络延迟以及掌控以外的不确定性?这都是本篇文章会谈到的内容

vivo 手机云服务建设之路-平台产品系列04

手机云服务目前作为每家手机厂商必备的一项基础服务,其服务能力和服务质量对用户来说可以说是非常重要。用户将自己大量的信息数据存储在云端,那我们的云端服务如何保证服务的稳定和数据的安全,以及如何应对越来越多用户群体的使用?本文将主要介绍 vivo 手机云服务系统的建设历程。

内核不中断前提下,Gaussdb(DWS)内存报错排查方法

摘要:本文主要讲解如何在内核保证操作不能中断采取的特殊处理,理论上用户执行的sql使用的内存(dynamic_used_memory) 是不会大范围的超过max_dynamic_memory的内存的 本文分享自华为云社区《Gaussdb(DWS)内存报错排查方法》,作者: fighttingman。

学习文章:“浅析数据安全之密态化计算”

本文学习文章:“浅析数据安全之密态化计算” 数据安全的必要性 数据安全应保证数据产生、存储、传输、访问、使用、销毁、公开等全生命周期安全,并且需要做到保证数据处理过程的保密性、完整性、可用性。如何安全采集用户数据,并且实现安全地对用户数据进行使用,主要包括在整个数据周期中保证安全,即在数据的生产、传

从工具到实践:如何在GitHub上保障开源项目安全?

1998年,Christine Peterson创造了 “开源软件”这个词。她解释道:“这是刻意为之,为了让其他人更容易理解这个领域”。同年,O’Reilly组织了首届“开源峰会”。 开源软件受到更多人青睐原因在于,用户对软件拥有更多的控制权因为他们可以检查代码。对于长期项目来说,开源软件被认为是稳

如何在微服务下保证事务的一致性

微服务架构是将单个服务拆分成一系列小服务,且这些小服务都拥有独立的进程,彼此独立,很好地解决了传统单体应用的上述问题,但是在微服务架构下如何保证事务的一致性呢?

CopyOnWriteArrayList 是如何保证线程安全的?

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 前言 大家好,我是小彭。 在上一篇文章里,我们聊到了ArrayList 的线程安全问题,其中提到了 CopyOnWriteArrayList 的解决方法。那么 CopyOnWriteArrayList 是如何

[转帖]RabbitMQ 如何保证交换机中的消息不丢失

我们知道,生产者会先将消息发送给交换机,但是如果交换机此时没有匹配到相关的队列时,交换机中的消息就会出现丢失的问题。 那么,如何保证交换机中的消息不丢失呢? mandatory 参数 当 basicPublish 方法的 mandatory 参数设为 true 时,如果交换器无法匹配到绑定的队列,那

[转帖]Redis的高并发及高可用,到底该如何保证?

https://zhuanlan.zhihu.com/p/404481762 一、redis如何通过读写分享来承载读请求QPS超过10万+ 1、redis高并发跟整个系统的高并发之间的关系 redis,你要搞高并发的话,不可避免,要把底层的缓存搞得很好 mysql,高并发,做到了,那么也是通过一系列

MQ系列10:如何保证消息幂等性消费

MQ系列1:消息中间件执行原理 MQ系列2:消息中间件的技术选型 MQ系列3:RocketMQ 架构分析 MQ系列4:NameServer 原理解析 MQ系列5:RocketMQ消息的发送模式 MQ系列6:消息的消费 MQ系列7:消息通信,追求极致性能 MQ系列8:数据存储,消息队列的高可用保障 M