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

一口气说完四种幂等方案后,面试官笑了~

时间:2023-04-01 15:01:07 Java

什么是幂等?幂等性是数学和计算机科学中的一个概念。当数学中的元运算是幂等的时,它对任何元素两次的影响将与其作用一次的结果相同。在计算机编程中,幂等操作的特点是任意数量的执行与一次执行具有相同的影响。幂等函数或方法是可以使用相同参数重复执行并获得相同结果的函数或方法。这些函数不会影响系统的状态,无需担心重复执行对系统造成的改变。什么是接口幂等性?在HTTP/1.1中,定义了幂等性。它描述了对资源的一次或多次请求对资源本身应该有相同的结果(网络超时等问题除外),即第一次请求对该资源有副作用,但后续请求不会重复。对资源的副作用。这里的副作用不是破坏结果或产生意想不到的结果。也就是说,其任何数量的执行都将对资源本身产生与一次执行相同的影响。为什么需要实现幂等?一般情况下,调用接口时,可以正常返回信息,不会重复提交,但是以下几种情况可能会出现问题,例如:前端重复提交表单:填写某些表单时,用户填写提交,很多时候会因为网络波动,用户没有及时回复提交成功,导致用户认为提交不成功,然后一直点击提交按钮。这时候就会出现重复的表单提交请求。用户恶意刷单:例如在实现用户投票功能时,如果用户重复为某个用户提交投票,这将导致接口接收到用户重复提交的投票信息,从而使投票结果与事实严重不符。接口超时重复提交:很多情况下,HTTP客户端工具默认开启超时重试机制,尤其是第三方调用接口时,为了防止网络波动超时导致请求失败,会增加重试机制,导致多次提交请求。第二次评价。消息重复消费:在使用MQ消息中间件时,如果消息中间件出错,没有及时提交消费信息,就会出现重复消费。使用幂等性最大的好处就是让接口保证任何操作都是幂等的,避免系统因为重试导致的未知问题。幂等性的引入对系统有什么影响?幂等性是为了简化客户端的逻辑处理,可以放置重复提交等操作,但是增加了服务端的逻辑复杂度和成本。主要原因是并行执行功能改为串行执行,降低了执行效率。增加了控制幂等性的业务逻辑,使业务功能复杂化;因此,在使用时需要考虑引入幂等性的必要性。根据实际业务场景具体分析,除特殊业务需求外,一般不需要引入幂等接口。RestfulAPI接口的幂等性如何?流行的Restful推荐的几种HTTP接口方法中,有幂等的方法和不能保证幂等的方法,如下:√满足幂等x不满足幂等可能满足也可能不满足幂等,根据实际业务逻辑关于方案一:如何实现数据库唯一主键的幂等性?数据库唯一主键的实现主要是利用了数据库中主键唯一约束的特点。一般来说,唯一主键更适合“插入”的幂等性,可以保证一张表中只能存在一条具有唯一主键的记录。在使用数据库唯一主键完成幂等性时,需要注意的是主键一般不是数据库中的自增主键,而是分布式ID作为主键,这样才能保证全局唯一分布式环境中的ID性。适用操作插入操作删除操作使用限制需要生成一个全局唯一的主键ID;主流程主流程如下:客户端执行创建请求,调用服务端接口。服务端执行业务逻辑,生成分布式ID,将ID作为待插入数据的主键,然后执行数据插入操作,运行相应的SQL语句。服务器将这条数据插入数据库。如果插入成功,说明接口没有被重复调用。如果抛出重复主键异常,说明该记录已经存在于数据库中,返回错误信息给客户端。方案二:数据库乐观锁是如何做到幂等的?数据库乐观锁方案一般只适用于执行更新操作的进程。我们可以提前在相应的数据表中增加一个额外的字段,作为当前数据的版本标识。这样,每次更新数据库表中的数据时,都以版本标识为条件,取值为上次要更新的数据中版本标识的值。适用操作Update操作使用限制需要在数据库对应的业务表中添加额外的字段说明。例如,在下面的数据表中:为了防止每次更新时重复更新,所以确定更新的内容一定是要更新的内容。我们通常会加上一个version字段记录当前的记录版本,这样更新的时候就带上这个值,这样只要执行了更新操作就可以确定一定要更新某个对应版本下的信息。这样,每次更新时,都要指定要更新的版本号,version=5的信息可以通过如下操作准确更新:UPDATEmy_tableSETprice=price+50,version=version+1WHEREid=1ANDversion=5复制代码上方的WHERE后跟条件id=1ANDversion=5。执行完后,id=1的版本更新为6,所以如果重复执行SQL语句,是不会生效的,因为id=1ANDversion=5的数据已经不存在了,这样幂等性可以保持更新,多次更新不影响结果。解决方案三:如何实现反重代币的幂等性?对于客户端连续点击或者调用者超时重试的情况,比如提交订单,这种操作可以使用Token机制来防止重复提交。简单来说,调用者在调用接口时,首先向后端请求一个全局ID(Token),并在请求中携带这个全局ID(Token最好放在Headers中),后端需要验证Token作为Key,将用户信息作为Value发送给Redis,进行key-value内容验证。如果Key存在且Value匹配,则执行删除命令,然后正常执行后续业务逻辑。如果没有对应的Key或者Value不匹配,会返回错误提示重复执行,保证幂等操作。适用操作插入操作更新操作删除操作限制条件需要生成全局唯一的Token字符串需要使用第三方组件Redis进行数据校验主要流程:服务端提供获取Token的接口,Token可以是序列号,也可以是分布式的ID或UUID字符串。客户端调用接口获取Token,此时服务端生成一个Token字符串。然后将字符串存入Redis数据库,使用Token作为Redis的key(注意设置过期时间)。将Token返回给客户端,客户端拿到后,要存放在表单的隐藏域中。客户端在提交表单时,将Token存放在Headers中,并通过Headers执行业务请求。服务端收到请求后,从Headers中获取Token,然后根据Token检查redis中是否存在该key。服务器根据key在redis中是否存在进行判断,存在则删除key,然后正常执行业务逻辑。如果不存在,则抛出异常,并返回重复提交的错误信息。注意,在并发情况下,Redis执行查找数据和删除操作需要保证原子性,否则并发下很可能无法保证幂等性。它的实现方式可以使用分布式锁或者使用Lua表达式取消查询和删除操作。方案四:如何通过向下游传递唯一序列号实现幂等?所谓请求流水号,其实就是在短时间内,对每一个请求给服务器附加一个唯一的、唯一的流水号。序列号可以是订单ID,也可以是订单号,一般由下游生成。用于身份验证的序列号和ID附加到上游服务器接口。上游服务器收到请求信息后,将序列号与下游的认证ID组合成操作Redis的Key,然后检查Redis中是否存在与该Key对应的键值对。根据结果??:如果存在,说明下游请求序列号已经处理完毕,此时可以直接响应重复请求的错误信息。如果不存在,则将该Key作为Redis的键,将下游的键信息作为存储值(比如下游业务传递的一些业务逻辑信息),将键值对存储在Redis中,然后执行对应的业务通常只是逻辑。适用操作插入操作更新操作删除操作使用限制需要第三方传递唯一序列号;需要使用第三方组件Redis进行数据校验;主进程的下游服务生成一个分布式ID作为流水号,然后执行请求调用上游接口带有唯一的流水号和请求的认证凭证ID。上游服务进行安全检查,检测下游传递的参数中是否存在序列号和凭证ID。上游服务去Redis检查是否有对应的序列号和认证ID组成的Key。如果存在则抛出异常信息重复执行,然后向下游响应相应的错误信息。如果不存在,则将序列号和认证ID的组合作为Key,将下游的key信息作为Value,存储到Redis中,然后正常执行传入的业务逻辑。上述步骤往Redis中插入数据时,必须设置过期时间。这样可以保证在这个时间范围内,如果接口被重复调用,可以判断识别。如果不设置过期时间,很可能会导致Redis中存储的数据量不受限制,导致Redis不能正常工作。接口幂等性实现示例这里是反重Token令牌方案,可以保证不同请求动作下的幂等性。实现逻辑可以看上面写的“反重Token代币”方案,然后写下这段逻辑代码的实现。Maven引入相关依赖。在这里,Maven工具用于管理依赖项。这里在pom.xml中引入了SpringBoot、Redis、lombok相关依赖。org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-data-redisorg.apache.commonscommons-pool2org.projectlomboklombok复制代码配置连接Redis的参数在应用配置文件中配置连接Redis的参数,如下:spring:redis:ssl:falsehost:127.0.0.1port:6379database:0超时:1000密码:生菜:池:最大活动:100最大等待:-1最小空闲:0最大空闲:20复制代码创建并验证Token工具类创建一个操作Token的Service类,其中包含Token创建和验证方法,其中:Token创建方法:使用UUID工具创建Token字符串,设置为“idempotent_token:”+“Tokenstring”作为Key,用户信息作为Value,信息存储在Redis中。Token验证方式:接收Token字符串参数,加上Key前缀组成Key,然后传入value值,执行Lua表达式(Lua表达式可以保证命令执行原子性)找到对应的Key和删除操作,执行完成后,对命令的返回结果进行校验,如果结果不为空且不为零,则校验成功,否则它失败。@Slf4j@ServicepublicclassTokenUtilService{@AutowiredprivateStringRedisTemplateredisTemplate;/***Redis中存储的Token键的前缀*/privatestaticfinalStringIDEMPOTENT_TOKEN_PREFIX="idempotent_token:";/***创建一个Token并存储到Redis中,并返回用于辅助验证的Token**@paramvalue值*@return生成的Token字符串*/publicStringgenerateToken(Stringvalue){//实例化ID生成工具对象Stringtoken=UUID.randomUUID().toString();//设置Redis中存储的KeyStringkey=IDEMPOTENT_TOKEN_PREFIX+token;//将Token存入Redis,并设置过期时间为5分钟redisTemplate.opsForValue().set(key,value,5,TimeUnit.MINUTES);//returnTokenreturntoken;}/***验证Token的正确性**@paramtokentokenstring*@paramvaluevalue辅助验证信息存储在Redis中*@return验证结果*/publicbooleanvalidToken(Stringtoken,Stringvalue){//设置Lua脚本,其中KEYS[1]为key,KEYS[2]为valueStringscript="ifredis.call('get',KEYS[1])==KEYS[2]thenreturnredis.call('del',KEYS[1])elsereturn0end";RedisScriptredisScript=newDefaultRedisScript<>(script,Long.class);//根据Key前缀拼接KeyStringkey=IDEMPOTENT_TOKEN_PREFIX+token;//执行Lua脚本Longresult=redisTemplate.execute(redisScript,Arrays.asList(key,value));//根据返回结果判断是否匹配成功,删除Redis键值对。如果结果不为空或0,则验证通过if(result!=null&&result!=0L){log.info("验证token={},key={},value={}成功",token,key,value);返回真;}log.info("验证token={},key={},value={}失败",token,key,value);returnfalse;}}复制代码4.创建一个用于测试的controller类创建一个用于测试的controller类,包含一个获取Token的接口,并测试接口的幂等性,内容如下:@Slf4j@RestControllerpublicclassTokenController{@AutowiredprivateTokenUtilServicetokenService;/***获取Token接口**@returnTokenstring*/@GetMapping("/token")publicStringgetToken(){//获取用户信息(这里使用模拟数据)//注意:这里存储的内容只是一个例子,其作用是辅助验证,使其验证逻辑更加安全。比如这里存储用户信息的目的是://-1)使用“token”验证Redis是否有对应的Key//-2)、使用“用户信息”验证Redis的值是否匹配StringuserInfo="mydlq";//获取Token字符串并返回returntokenService.generateToken(userInfo);}/***接口幂等性测试接口**@paramtokenidempotentTokenstring*@return执行结果*/@PostMapping("/test")publicStringtest(@RequestHeader(value="token")Stringtoken){//获取用户信息(这里使用模拟数据)StringuserInfo="mydlq";//去Redis根据Token和用户相关信息验证是否有对应信息booleanresult=tokenService.validToken(token,userInfo);//根据验证结果响应不同的信息returnresult?"Normalcall":"Repeatedcall";}}Copythecode最后总结一下,幂等性是开发中非常普遍和重要的需求,尤其是对于支付、订单等与金钱挂钩的服务,保证接口的幂等性尤为重要。在实际开发中,我们需要针对不同的业务场景灵活选择幂等的实现方式:对于订单等唯一主键,我们可以使用“唯一主键方案”来实现。对于更新订单状态相关的更新场景操作,使用“乐观锁方案”更容易。对于上下游来说,下游请求上游更合理,上游服务可以使用“下游传输唯一序列号方案”。类似前端重复提交、重复下单、没有唯一ID号的场景,通过Token和Redis结合的“防复制Token方案”可以更快速的实现。以上只是给一些建议。再次强调一下,要实现幂等,首先要了解自己的业务需求,按照业务逻辑去实现才是合理的。只有处理好每个节点的细节,完善整体的业务流程设计,才能更好地为系统的正常运行提供良好的保障。最后做一个简单的总结,然后本篇博文到此结束,如下: