设计模式学习笔记 - 项目实战二:设计实现一个通用的接口幂等框架(分析)

本文介绍了在设计和实现一个通用接口幂等框架时,如何处理超时重试问题,包括幂等号的使用、功能性需求和非功能性需求的考虑,以及如何通过简化配置和代码实现接口的幂等化。
摘要由CSDN通过智能技术生成

概述

前面三篇文章,带你分析、设计、实现了一个接口限流框架。

  • 分析阶段,讲到需求分析的两大方面,功能性需求和非功能性需求分析。
  • 设计阶段,讲了如何通过合理的设计,实现功能性需求的前提下,满足易用、易扩展、灵活、高性能、高容错等非功能性需求。
  • 实现阶段,讲了如何利用设计思想、原则、模式、编码规范等,编写可读、可扩展的高质量的代码实现。

本章,我们来实现一个新的项目,开发一个通用的接口幂等框架。跟限流框架一样,还是分为分析、设计、实现三个部分,对应三篇文章来讲解。


需求场景

还记得之前讲到的限流框架的项目背景吗?为了复用代码,我们把通用的功能设计成了公共服务平台。公司内部的其他金融产品的后台系统,会调用公共服务平台的的服务,不需要完全从零开始开发。公共服务平台提供的 RESTful 接口。为了简化开发,调用方一般使用 Feign 框架(一个 HTTP 框架)来访问公共服务平台的接口。

调用方访问公共服务平台的接口,可能会有三种结果:成功、失败和超时。前两种结果非常明确,调用方可以自己决定收到结果之后如何处理。结果为 “成功”,万事大吉。结果为 “失败”,一般情况下,调用方会将失败的结果,反馈给用户(移动端 APP),让用户自行决定是否重试。

但是,当接口请求超时时,处理起来就没那么容易了。有可能业务逻辑已经执行成功了,只是公共服务平台返回结果给调用方时超时了,但也有可能业务逻辑没有执行成功,比如,因为数据库当时在集中写入,导致部分数据写入超时。总之,超时对应地执行结果是未决的。那调用方调用接口超时时(基于 Feign 框架开发的话,一般是收到 Timeout 异常),该如何处理呢?

如果接口只包含查询、删除、更新这些操作,那接口天然是幂等的。所以,超时之后,重新再执行一次,也没有任何副作用。不过,这里有两点需要特殊说明下。

删除操作要当心 ABA 问题。删除操作超时了,又触发一次删除,但在删除之前,又有一次新的插入。后一次删除操作删除了新插入的数据,而新插入的数据本不应该删除。不过,大部分业务都可以容忍 ABA 问题。对于少数不能容忍的业务场景,我们可以针对性的特殊处理。

此外,细究起来,update x = x + delta 这样格式的更新操作并非幂等,只有 update x=y 这样格式的更新操作才是幂等。不过,后者也存在着跟删除同样的 ABA 问题。

如果接口包含修改操作(插入操作、update x = x + delta 更新操作),多次重复执行有可能会导致业务上的错误,这是不能接受的。如果插入的数据包含数据库唯一键,可以利用数据库唯一键的排他性,保证不会重复插入。此外,一般我会建议按照这样几种方式来处理。

  • 第一种处理方式是,调用方访问公共服务平台接口超时时,返回清晰明确的提醒给用户,告知执行结果未知,让用户自己判断是否重试。不过,你可能会说,如果用户看到了超时提醒,但是还是重新发起了操作,比如重新发起了转账、充值等操,那该怎么办呢?实际上,对这种情况,技术是无能为力的。因为两次操作都是用户发起的,我们无法判断第二次的转账、充值是新的操作,还是基于上一次超时的重试行为。
  • 第二种处理方式是,调用方调用其他接口,来查询操作操作的结果,明确超时操作对应的业务,是执行成功了还是失败了,然后再基于明确的结果做处理。但是这种处理方式存在一个问题,那就是并不是所有的业务,都方便查询操作结果。
  • 第三张处理方式是,调用方在遇到接口超时后,直接发起重试操作。这就需要接口支持幂等。我们可以选择在业务代码中触发重试,也可以将重试操作放到 Feign 框架中完成。因为偶尔发生的超时,在正常的业务逻辑中编写一大坨补救代码,这样做会影响到代码的可读性,有点划不来。当然,如果项目中需要支持超时重试的业务不多,那对于仅有几个业务,特殊处理一下也未尝不可。但是,如果项目中需要支持超时重试的业务比较多,那最好是把超时重试这些非业务相关的逻辑,统一在框架层面解决。

对于响应时间敏感的调用方来说,它们服务的是移动端用户,过长的时间等待,还不如直接返回超时给用户。所以,这种情况下,第一种处理方式是比较推荐的。但是,对于响应事件不敏感的调用方来说,比如 Job 类的调用方,我们推荐选择后两种处理方式,能够提高处理的成功率。而第二种处理方法,本身有一定的局限性,因为并不是所有业务操作都方便查询是否执行成功的。第三种保证接口幂等的处理方式,是比较通用的解决方案。所以,我们针对这种处理方式,抽象出一套统一的幂等框架,简化幂等接口的开发。

需求分析

刚刚介绍了幂等框架的需求背景:超时重试需要接口幂等的支持。接下来,我们在对需求进行更加详细的分析和整理,这其中就包括功能性需求和非功能性需求。

不过,在此之前,需要先搞清楚一个重要的概念:幂等号。

前面多次提到 “幂等”,那 “幂等” 到底是什么意思呢?放到接口调用的这个场景里,幂等的意思是,针对同一个接口,多次发起同一业务请求,必须保证业务只执行一次。那如何判定两次接口请求是同一业务请求呢?比如,两次调用转账接口,尽管转账用户、金额等参数都一样,但我们也无法判断这两个转账请求就是重试关系。

实际上,要确定重试关系,我们就需要给同一业务请求一个唯一标识,也就是 “幂等号” !如果两个接口请求,带有相同的幂等号,那我们就判段它们是重试关系,是同一个业务请求,不要重复执行。

幂等号需要保证全局唯一性。它可以有业务含义,比如用户手机号是唯一的,对于用户注册接口来说,可以拿它作为幂等号。不过,这样就会导致幂等框架的实现,无法脱离具体的业务。所以,我们更加倾向于,通过某种算法来随机生成没有业务含义的幂等号

幂等号的概念搞清楚了,再来看下框架的功能性需求。

前面也介绍了一些需求分析整理方法,比如画线框图、写用户用例、基于测试驱动开发等。跟限流框架类似,这里我们也记住用户用例和测试驱动开发的思想,先去思考,如果框架最终开发出来之后,它会如何被使用。下面写了一个框架使用的 Demo。

 使用方式一:在业务代码中处理幂等 
// 接口调用方
Idempotence idempotence = new Idempotence();
String idempotenceId = idempotence.createId();
Order order = createOrderWithIdempotence(..., idempotenceId);

// 接口实现方
public class orderController {
	private Idempotence idempotence; // 依赖注入
	
	public Order createOrderWithIdempotence(..., String idempotenceId) {
		// 前置操作
		boolean existed = idempotence.check(idempotenceId);
		if (existed) {
			// 两种处理方式:
			// 1. 查询order,并返回
			// 2.返回duplication operation Exception
		}
		idempotence.record(idempotenceId);
		
		// 执行正常业务逻辑
	}

	public Order createOrder(...) {
		// ...
	}
}

 使用方式二:在框架层面处理幂等 
// 接口调用方
Idempotence idempotence = new Idempotence();
String idempotenceId = idempotence.createId();
// 通过feign框架将幂等号添加到http header中..

// 接口实现方
public class orderController {
	@IdempotenceRequired
	public Order createOrder(...) {
		// ...
	}
}

// 在AOP切面中处理幂等
@Aspect
public class IdempotenceSupportAdvice {
	@Autowired
	private Idempotence idempotence;

	@Pointcut("@annotation(com.example.idempotence.annotation.IdempotenceRequired)")
	public void controllerPointcut() {
	}
	
	@Around(value = "controllerPointcut")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		// 从HTTP header 中获取幂等号 idempotenceId
		
		// 前置操作
		boolean existed = idempotence.check(idempotenceId);
		if (existed) {
			// 两种处理方式:
			// 1. 查询order,并返回
			// 2.返回duplication operation Exception
		}
		idempotence.record(idempotenceId);
		
		Object result = joinPoint.proceed();
		return result;
	}
}

结合刚刚的 Demo,从使用者角度来说,幂等框架的主要处理流程是这样的。

  • 接口调用方生成幂等号,并跟随接口请求,将幂等号传递给接口实现方。
  • 接口实现方接收到接口请求之后,按照约定,从 HTTP header 或者接口参数中,解析出幂等号,然后通过幂等号查询幂等框架。如果幂等号已经存在,说明业务已经执行或者正在执行,直接返回;如果幂等号不存在,说明业务没有执行过,则记录幂等号,继续执行业务。

对于幂等框架,它都有哪些非功能性需求

在易用性方面,我们希望框架接入简单方便,学习成本低。只需要编写简单的配置以及少许代码,就能完成接入。此外,框架最好对业务代码低侵入松耦合,在统一的地方(比如 Spring AOP 中)接入幂等框架,而不是将它耦合在业务代码中。

在性能方面,针对每个幂等接口,在正式处理业务逻辑之前,我们都要添加保证幂等的处理逻辑。这或多或少地会增加接口请求的响应时间。而对于响应时间比较敏感的接口服务来说,我们要让幂等框架尽可能低延迟,尽可能减少对接口请求本身响应时间的影响。

在容错性方面,跟限流框架相同,不能因为幂等框架本身的异常,导致接口响应异常,影响服务本身的可用性。所以,幂等框架要有高度的容错性。比如,存储幂等号的外部存储挂掉了,幂等逻辑无法正常运行,这个时候业务接口也要能正常服务才行。

总结

本章,我们介绍了幂等框架的一个需求场景,那就是接口超时重试。大部分情况下,如果接口只包含查询、删除、更新这些操作,那接口天然是幂等的。此外,如果接口包含修改操作(插入操作或 update x=x+delta 更新操作),保证接口幂等性就需要做一些额外的工作。

现在开源的恭喜很多,但幂等框架非常少见。原因是幂等的保证是业务强相关的。大部分保证幂等性的方式都是针对具体的业务具体处理,比如,利用业务数据中的唯一 ID 唯一性来处理插入操作的幂等性。但是,针对每个需要幂等的业务逻辑,单独编写代码处理,一方面对程序员的开发能力要求比较高,另一方面开发成本也比较高。

为了简化接口幂等的开发,我们希望开发一套统一的幂等框架,脱离具体业务,让程序员通过简单的配置和少量代码,就能将非幂等接口改造成幂等接口。

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值