您现在的位置是:亿华云 > IT科技类资讯

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

亿华云2025-10-02 18:41:46【IT科技类资讯】9人已围观

简介本文转载自微信公众号「Java大厂面试官」,作者laker。转载本文请联系Java大厂面试官公众号。 什么是幂等? 重复请求原因 解决方案 方案

本文转载自微信公众号「Java大厂面试官」,从零作者laker。搭建转载本文请联系Java大厂面试官公众号。脚手架保  什么是证服重复幂等? 重复请求原因 解决方案 方案一:前端同步阻塞按钮置灰 方案二:前后端搭配干活,预生成订单号 方案三:通用方案,性和锁模式 实现 自定义注解限制重复提交 自定义切面拦截过滤处理 使用示例

什么是防止幂等?

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

重复请求原因

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

用户手抖点快了,搭建导致多次重复下单。脚手架保 网络抖动导致失败或者超时重传,证服重复例如nginx、性和Fegin、防止RPC框架等

解决方案

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

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

优点:实现成本极低

缺点:

只能防御用户手抖的误操作。 确防不住远程调用的重试以及恶意重放。

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

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

时序图如下:

细节如下:

订单号生成时机

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

订单号生成规则

小规模系统完全可以用MySQL的Sequence或者Redis来生成。大规模系统也可以采用类似雪花算法之类的方式分布式生成GUID。 订单号中最好包含一些品类、时间等信息,便于业务处理,它不能是一个单纯自增的ID,否则别人很容易根据订单号计算出你大致的销量,所以订单号的生产算法在保证不重复的前提下,一般都会加入很多业务规则在里面。

订单号是否是主键

方式一:使用订单号做主键

如果订单号不是递增的可能造成频繁页分裂,导致并发高的时候性能降低,所以要保证订单号全局递增。

方式二:有自增主键和订单号列并设置唯一索引

因为订单号不是主键,云服务器所以根据订单号查询会多一次回表操作,且如果订单号不递增二级订单号索引也会有页分裂。

订单号可以由前端生成吗

不可以,订单号一定是在后端生成,后端生成可以保证全局唯一,且可以用于做安全认证,不是后端颁发的订单号不予处理。

提交订单的时候,一种是先拿着订单号去查库,让业务代码校验是否存在,另一种是直接利用库表主键唯一约束抛异常,这两种处理方式哪种性能更好?

选后者,等查完库确定不存在再插入的时候,可能数据已经变化了,订单存在了,还是要抛异常,检查意义不大。

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

使用锁来控制一段时间内的重复请求,注意: 锁的粒度为用户+业务。

请求流程如下:

1.请求接口时,获取一个锁 锁的服务器托管粒度 :同一用户的同一操作逻辑 锁名称规则:业务名称+用户ID 2.给锁设置过期时间10秒,防止业务逻辑执行错误,用户一直被锁住 3.如果被锁了,返回“正在处理,请勿重复提交” 4.没有被锁,执行正常逻辑,在逻辑结束后,删掉锁

实现

针对方案三实现如下:

自定义注解限制重复提交

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RepeatSubmitLimit {      /**      * 业务key,例如下单业务 order      */     String businessKey();     /**      * 业务参数,用于做更细粒度锁,例如锁到具体 订单id #orderId      */     String businessParam() default "";     /**      * 是否用户隔离,默认启用      */     boolean userLimit() default true;     /**      * 锁时间 默认10s      */     int time() default 10; } 

自定义切面拦截过滤处理

@Component @Aspect @Slf4j public class LimitSubmitAspect {      LFUCache<Object, Object> LFUCACHE = CacheUtil.newLFUCache(100, 60 * 1000);     @Pointcut("@annotation(RepeatSubmitLimit)")     private void pointcut() {      }     @Around("pointcut()")     public Object handleSubmit(ProceedingJoinPoint joinPoint) throws Throwable {          Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();         //获取注解信息         RepeatSubmitLimit repeatSubmitLimit = method.getAnnotation(RepeatSubmitLimit.class);         int limitTime = repeatSubmitLimit.time();         String key = getLockKey(joinPoint, repeatSubmitLimit);         Object result = LFUCACHE.get(key, false);         if (result != null) {              throw new BusinessException("请勿重复访问!");         }         LFUCACHE.put(key, StpUtil.getLoginId(), limitTime * 1000);         try {              Object proceed = joinPoint.proceed();             return proceed;         } catch (Throwable e) {              log.error("Exception in { }.{ }() with cause = \{ }\ and exception = \{ }\", joinPoint.getSignature().getDeclaringTypeName(),                     joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL", e.getMessage(), e);             throw e;         } finally {              LFUCACHE.remove(key);         }     }     private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();     private static final ExpressionParser PARSER = new SpelExpressionParser();     private String getLockKey(ProceedingJoinPoint joinPoint, RepeatSubmitLimit repeatSubmitLimit) {          String businessKey = repeatSubmitLimit.businessKey();         boolean userLimit = repeatSubmitLimit.userLimit();         String businessParam = repeatSubmitLimit.businessParam();         if (userLimit) {              businessKey = businessKey + ":" + StpUtil.getLoginId();         }         if (StrUtil.isNotBlank(businessParam)) {              Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();             EvaluationContext context = new MethodBasedEvaluationContext(null, method, joinPoint.getArgs(), NAME_DISCOVERER);             String key = PARSER.parseExpression(businessParam).getValue(context, String.class);             businessKey = businessKey + ":" + key;         }         return businessKey;     } } 

使用示例

@RepeatSubmitLimit(businessKey = "tokenInfo", businessParam = "#name")   @GetMapping("/api/v1/tokenInfo")   public Response tokenInfo(String name) {    } 

请求示例:http://localhost:8080/api/v1/tokenInfo?name=123

锁粒度为:taokeninfo:1:123

防重效果:

{   code: "500",  msg: "请勿重复访问!" } 

参考:

后端存储实践课

很赞哦!(92738)