从零搭建开发脚手架保证服务的幂等性和防

什么是幂等?

重复请求原因

解决方案

方案一:前端同步阻塞按钮置灰

方案二:前后端搭配干活,预生成订单号

方案三:通用方案,锁模式

实现

自定义注解限制重复提交

自定义切面拦截过滤处理

使用示例

什么是幂等?

多次执行的结果和一次执行的结果相同,例如查询操作天然就是幂等的。

重复请求原因

我们以电商场景中的下单来举例,造成下单重复一般有以下几个原因:

用户手抖点快了,导致多次重复下单。网络抖动导致失败或者超时重传,例如nginx、Fegin、RPC框架等解决方案方案一:前端同步阻塞按钮置灰

前端同步阻塞按钮置灰,用户点击“发布”按钮后,在网络请求没有返回,或者超时之前,用户都不可以继续点击“发布按钮”,界面可以将按钮置灰或者转圈。

优点:实现成本极低

缺点:

只能防御用户手抖的误操作。

确防不住远程调用的重试以及恶意重放。

方案二:前后端搭配干活,预生成订单号

可以通过预先生成订单号(在进入下单页面的时候生成订单号),然后利用数据库中订单号的唯一约束这个特性,避免重复写入订单。

时序图如下:

细节如下:

订单号生成时机

是在进入订单页面,而不是提交订单的时候。

订单号生成规则

小规模系统完全可以用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{LFUCacheObject,ObjectLFUCACHE=CacheUtil.newLFUCache(,60*0);

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*0);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);throwe;}finally{LFUCACHE.remove(key);}}privatestaticfinalParameterNameDiscovererNAME_DISCOVERER=newDefaultParameterNameDiscoverer();privatestaticfinalExpressionParserPARSER=newSpelExpressionParser();privateStringgetLockKey(ProceedingJoinPointjoinPoint,RepeatSubmitLimitrepeatSubmitLimit){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://www.sonphie.com/lcbx/14531.html

网站简介| 发布优势| 服务条款| 隐私保护| 广告合作| 网站地图| 版权申明

当前时间: