当前位置: 首页 > 后端技术 > Java

基于幂等表思想的幂等实践

时间:2023-04-02 01:28:01 Java

1.为什么需要幂等?在分布式场景下,在多个业务系统之间实现一个强一致的协议是极其困难的。最简单且可实现的假设之一是确保最终一致性,这要求服务器在处理重复请求时给出相同的响应,同时不会对持久化数据造成副作用(即多次操作和单次操作的结果需要是从业务角度来看是一致的)。如果API是幂等的,调用的发起者可以安全地重试。这符合我们的一般假设。提供幂等性是服务提供者必须做的事情。具有幂等性可以保证我们的接口不会受到各种异常重试或者恶意请求锁的影响。2、在幂等方式不同的场景下(常见的有【接口与后端接口交互场景】和【接口与接口交互场景】),幂等方式有很多种,各不相同,各有局限和缺点!!!可以基于【业务key+业务状态机+乐观锁】做幂等实现(一般适用于比较简单的更新场景)。例如:在更新订单状态为finished的场景下,先根据订单号查询订单,判断订单状态是否为finished,如果不是,则更新为finished。可以基于【业务键+分布式锁+业务状态机】做幂等实现。以用户ID号作为查询条件查询用户,如果用户不存在则添加。如果用户存在,幂等处理方案一般有一个固定的流程:【一次锁,两次判断,三次执行】可以基于【业务key+唯一索引】进行幂等实现(一般适用于新数据场景)例如:在添加用户信息的场景下,以用户ID号为唯一键,建立唯一索引。添加新用户时,可以基于【Redis+token方式】通过捕获唯一键冲突异常(DuplicateKeyException)等实现实现幂等控制(多用于界面与界面交互,不适合界面与界面交互,此方案也是比较常见的方案,但不在本次讨论范围内)例如:用户提交填写好的表单信息,当用户重复提交表单时保证有保障。只执行一次,其余都是幂等的,返回相同的结果。可以基于【幂等表】做幂等实现(比较通用的方案,具体效率取决于存储幂等记录的存储介质)例如:消费MQ消息,为了避免消息重复消费,可以插入一个幂等在消费消息之前记录,然后执行消费逻辑。消费完成后修改幂等记录的幂等状态即可消费成功!接口互调的情况类似3.幂等设计原则一个幂等的服务要求无论出现多么极端的重复请求,都必须是一致的。这时,它必须满足:external:返回完全相同的结果internal:自身状态没有变化。对于服务商:严格来说,请求中的字段必须完全一致,服务商认为是重复请求。但是在实际环境中,我们可能没有这么严格的要求。我们一般认为,只要关键业务参数相同,那么就是重复请求,应该进行幂等处理。对于服务调用者:需要做好幂等结果处理,多个返回相同结果的请求需要正确处理幂等设计应该尽可能简单、可靠、高效(过多的幂等逻辑会影响可用性和性能)从一个简单的观点:幂等的流程和逻辑应该尽可能简单可靠:不仅是在正常运行的情况下,在某些异常场景下也是如此。否则,幂等的设计意义会大大降低效率:幂等逻辑执行不能耗时,对于一些高并发的接口,需要尽量减少幂等逻辑执行的耗时。通用的幂等组件设计也易于使用和扩展重要4.常见幂等场景示例【MQ消息消费场景】,因为MQ为了保证消息传递成功可能会发起多次重试,消费者需要保证重复的消息能够被处理通过幂等性(例如:监听用户支付成功消息生成支付订单)【接口及接口交互场景】,前端重复提交数据,后台接口需要保证只执行一次,其余的重复请求幂等返回(例如:用户重复提交订单,重复提交输入的用户信息)【在接口互调的场景下】,调用方可能因为各种原因收不到响应结果而发起一次重试(如:数据同步、库存扣减等),此时被调用方需要保证调用是重复的幂等处理5.幂等实践为了设计尽可能通用的幂等的通用通用需求,我们这里使用【幂等表+幂等状态机】来实现。该方案可以适用于大部分【接口】+接口交互】和【接口到接口交互】模式如果项目只是接口与接口交互模式,那么采用【Redis+token】方案也是一个不错的选择。当然,软件工程几乎没有灵丹妙药,也很难有完美适用于所有场景1.设计流程调用发起请求,请求到达服务提供方获取指定业务key为唯一的幂等键,并建立一个幂等记录(此时幂等记录的状态为处理中(processing)),然后尝试将幂等记录写入存储介质(可以是Redis或者MySQL或者其他存储中等的)。如果幂等记录写入成功,则执行业务逻辑,通过唯一键修改幂等记录的状态。如果幂等记录写入失败,说明该幂等记录已经存在(之前已经执行过该业务key对应的数据),需要进行如下处理:通过幂等唯一键查询幂等记录,如果幂等记录的状态判断为成功(success),表示该业务上次已经执行完毕,本次无需重复执行,获取上次执行的结果即可(如果需要)并幂等地返回如果状态正在处理(processing),表示已经有其他线程在处理业务数据或者极端情况下应用宕机导致的异常情况。这时候需要判断【请求处于processing(处理中)状态的时间长度】,结合应用配置【允许最大业务执行时间】判断处于processing状态的时间已经超过了配置【允许的最大业务执行时间】,然后尝试以乐观锁的形式重新修改幂等记录,如果修改成功,则执行业务逻辑,否则,抛出并发异常。如果处于处理状态的时间没有超过配置的【业务最大允许执行时间】,则直接抛出并发请求异常。2、需要思考的问题在上述幂等实现过程中,在极端情况下,需要考虑和注意以下几点在极端情况下,如果幂等记录插入成功,业务流程正常执行,则幂等状态更新成功时出现异常(比如存放幂等记录的存储介质宕机),有必要吗?处理异常,还是抛出异常中断进程???如果抛出异常会怎样?catch异常会怎样?解决方案1.抛出异常:如果抛出异常中断流程,调用方应该感知到调用失败,但实际上业务流程已经执行完毕。这种情况下如果调用方发起重试,幂等会失败(同一个业务代码执行两次)方案二:不抛异常:如果不抛异常,接口会继续执行,然后返回数据给呼叫者,召集者。如果调用者收到返回的数据,则不会发起重新发起。试了一下,不会有幂等的问题。但是此时幂等记录的状态还在处理中,当指定了业务的最大执行时间后,如果调用方在指定的最大执行时间后再次发起重试,幂等还是会失败(当然我们不需要指定业务的最大执行时间)对比以上两种情况,我们一般更倾向于第二种方案,自己处理异常,做一层【覆盖策略】(比如告警或者记录异常)幂等数据信息等,后面可以手动查数据),这个方案更稳定适用如果业务逻辑执行失败,是不是要删除之前创建的幂等记录?根据幂等性的严格含义,我们应该保留这个幂等记录,并且将幂等记录的状态改为Exception或者failed,在后续重试请求进来的时候返回failed给调用者(保证多次调用得到相同的)结果)。但在真实环境中,业务执行异常可能是因为数据校验失败,接口调用外部系统失败(例如外部系统正在发布版本(没有做优雅发布)等)。在这些情况下,调用者可以修改数据并重试。或者隔一段时间重试,那么这个时候最好有一定的自愈能力,而不是每次都把这种数据转给人工处理(有些场景会增加人工成本,比如某个系统,经常会出现调用方传递的业务参数有问题或者接口调用外部系统失败)当然这两种策略需要根据具体情况来选择,没有优劣之分坏的。需要设置【最长处理时间】吗?例如,我们期望接口的最大处理时间为1小时(即幂等记录处理处理状态的最大时间为1小时)。如果超过了这个时间,那么就是不正常了,应该在下次重试请求的时候尝试恢复业务执行。这也是一个有两个方面的选择问题,需要根据实际项目情况进行权衡