当前位置: 首页 > 科技观察

从零搭建开发脚手架 保证服务的幂等性和防止重复请求

时间:2023-03-18 18:48:24 科技观察

从头搭建开发脚手架,保证服务幂等性,防止重复请求转载本文,请联系Java大厂面试官公众号。什么是幂等性?重复请求的原因方案一:前端同步屏蔽按钮变灰方案二:前后端协同,预生成订单号方案三:通用方案,锁定方式实现自定义注解限制重复提交自定义aspects拦截过滤处理示例什么是幂等性?多次执行的结果与一次执行的结果相同。例如,查询操作自然是幂等的。重复请求的原因我们以电商场景下单为例。一般重复下单的原因有以下几种:用户手抖的有点太快,导致多次重复下单。网络抖动导致失败或超时重传,如nginx、Fegin、RPC框架等。解决方法一:前端同步阻塞按钮灰显。前端同步阻塞按钮是灰色的。用户点击“发布”按钮后,网络请求没有返回,或者超时前,用户无法继续点击“发布”按钮,界面可以让按钮变灰或翻转。优点:实现成本极低缺点:只能防止用户握手导致的误操作。它不能防止远程调用重试和恶意重播。解决方案2:使用前端和后端。预生成订单号通过预生成订单号(进入订单页面时生成订单号),然后利用订单号在数据库中的唯一性约束,避免重复写入订单。时序图如下:详情如下:订单号是在进入订单页面时生成的,不是在提交订单时生成的。订单号生成规则小型系统可以使用MySQL的Sequence或者Redis来生成。大型系统也可以采用雪花算法等方法分布式生成GUID。最好在订单号中包含一些类别、时间等信息,方便业务处理。不能是单纯的自增ID,否则别人很容易根据订单号推算出你的大概销售额,所以订单号的生产算法保证准确。在重复的前提下,一般会在其中添加很多业务规则。订单号是否为主键方法一:以订单号为主键。如果序号不递增,可能会导致频繁分页,导致并发高时性能下降,所以要保证序号全局递增。方法二:有自增主键和订单号列,设置唯一索引。因为订单号不是主键,根据订单号查询会再次返回表,如果订单号不自增,二级订单号索引也会出现分页。订单号可以前端生成吗?不行,订单号必须在后台生成。后端生成可保证全局唯一性,可用于安全认证。非后台下发的订单号将不予处理。提交订单时,一种是先查订单号的库,让业务代码检查是否存在,或者直接使用库表主键的唯一约束抛出异常。这两种处理方式哪种性能更好?如果选择后者,当检查库确认不存在后再插入时,数据可能发生变化,顺序存在,但仍然会抛出异常,检查意义不大。方案三:通用方案,锁方式使用锁来控制一段时间内的重复请求,注意:锁的粒度是用户+业务。请求流程如下:1.请求接口时,获取锁粒度:同一个用户相同的操作逻辑锁名规则:业务名+用户ID2.设置锁的过期时间为10秒,防止业务逻辑执行错误。锁定3.如果锁定,返回“处理中,请勿重复提交”4.如果没有锁定,执行正常逻辑。逻辑完成后,删除锁。第三种方案的实现如下:自定义注解限制重复提交@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic@interfaceRepeatSubmitLimit{/***业务key,比如order*/StringbusinessKey();/***业务参数,用来做更细粒度的锁,比如锁定到特定的订单id#orderId*/StringbusinessParam()default"";/***默认是否开启用户隔离*/booleanuserLimit()defaulttrue;/***锁定时间默认为10s*/inttime()default10;}自定义切面拦截过滤@Component@Aspect@Slf4jpublicclassLimitSubmitAspect{LFUCacheLFUCACHE=CacheUtil.newLFUCache(100,60*1000);@Pointcut("@annotation(RepeatSubmitLimit)")privatevoidpointcut(){}@Around("pointcut()")publicObjecthandleSubmit(ProceedingJoinPointjoinPoint)throwsThrowable{Methodmethod=((MethodSignature)joinPoint.getSignature()).getMethod();//获取注解信息RepeatSubmitLimitrepeatSubmitLimit=method.getAnnotation(RepeatSubmitLimit.class);intlimitTime=repeatSubmitLimit.time();Stringkey=getLockKey(joinPoint,repeatSubmitLimit);Objectresult=LFUCACHE.get(key,false);if(result!=null){thrownewBusinessException("请勿重新访问!");}LFUCACHE.put(key,StpUtil.getLoginId(),limitTime*1000);try{Objectproceed=joinPoint.proceed();returnproceed;}catch(Throwablee){log.error("Exceptionin{}.{}()withcause=\'{}\'andexception=\'{}\'",joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName(),e.getCause()!=null?e.getCause():"NULL",e.getMessage(),e);throw;}finally{LFUCACHE.remove(key);}}privatestaticfinalParameterNameDiscovererNAME_DISCOVERER=newDefaultParameterNameDiscoverer();privatestaticfinalExpressionParserPARSER=newSpelExpressionParser();privateStringgetLockKey(ProceedingJoinPointjoinPoint,RepeatSubmitLimitrepeat){StringbusinessKey=repeatSubmitLimit.businessKey();booleanuserLimit=repeatSubmitLimit.userLimit();StringbusinessParam=repeatSubmitLimit.businessParam();if(userLimit){businessKey=businessKey+":"+StpUtil.getLoginId();}if(StrUtil.isNotBlank(businessParam)){Methodmethod=((MethodSignature)joinPoint.getSignature()).getMethod();EvaluationContextcontext=newMethodBasedEvaluationContext(null,method,joinPoint.getArgs(),NAME_DISCOVERER);Stringkey=PARSER.parseExpression(businessParam).getValue(context,String.class);businessKey=businessKey+":"+key;}returnbusinessKey;}}使用示例@RepeatSubmitLimit(businessKey="tokenInfo",businessParam="#name")@GetMapping("/api/v1/tokenInfo")publicResponsetokenInfo(Stringname){}请求示例:http://localhost:8080/api/v1/tokenInfo?name=123锁粒度为:taokeninfo:1:123反重作用:{code:"500",msg:"请勿重复访问!"}参考:后端存储实战教程