大家好,我是Tom。几天前,我和一个朋友聊天。我很沮丧。刚刚被老板骂了……差点丢了饭碗。还好老板没说“卷轴”。对于今年的就业市场,我热泪盈眶。小伙伴在初创公司,团队规模小,老板为了节省成本也没有配置什么豪华阵容。他的工作时间不长,负责交易订单。日前,他接到用户投诉,称“我的订单列表”中存在多个相同的订单。虽然没有造成资金损失,但严重影响了用户体验。看到这里,有经验的同学可能会猜到应该是界面没有反重控制吧。在日常开发中,重复提交也是比较常见的问题。比如:用户提交一个表单,鼠标点的太快了,刚好是前端的新人,没有任何控制,瞬间就会向后台系统发送多个请求。如果后端同学不做自下而上的计划,悲剧就会发生。一种常见的解决方案是利用数据库本身的“唯一索引约束”来保证数据的准确性。这种方案一般用在插入场景比较多。作为变体,考虑创建一个单独的反重表。这篇文章的案例有点特殊。订单号由后台系统生成,前后两次请求无法区分重复状态,所以系统会创建两条订单ID不同的记录,绕过“唯一索引约束”的限制,这...。.另外,MySQL的性能也有点弱。单机QPS在“千”维。如果是面对高并发的接口,性能也有点吃紧。接下来说一下如何使用Redis实现接口防重提交。技术方案首先我们来看一下排序过程,如下图所示:大致步骤:1.客户端向服务器发送请求。2.服务器接收到请求,然后从请求参数中提取唯一标识符。这个标志没有任何特殊的商业意义,可以由客户随机生成。3、服务器系统首先尝试将唯一标识写入Redis缓存中,可以认为是加锁操作。4.如果加锁失败,说明请求还在处理中。这是一个重复的请求,可以丢弃。5.如果加锁成功,继续正常的业务逻辑处理。6.业务逻辑处理完成后,删除锁定标记。7、最后将处理成功的结果返回给客户端。注意:在重复提交的情况下,在很短的时间内同时发送多个请求(例如:重复页面表单提交),我们只将第一个请求视为有效请求。锁用完后,记得手动删除。为了防止锁被正常释放,我们可以给锁设置一个很短的过期时间(比如10秒)。项目实战1.引入redis组件的实战项目是用SpringBoot搭建的,这里需要引入Redis相关的依赖。<依赖>org.springframework.bootspring-boot-starter-data-redisredis.clientsjedis2、redis变量配置application.properties配置文件,添加redis相关服务配置。spring.redis.host=127.0.0.1spring.redis.port=63793。定义注解类定义一个注解,配置在需要防止重复的接口方法上,提高开发效率,降低代码的耦合度。@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})@Documentedpublic@interfaceIdempotentRule{/***业务自定义前缀*/Stringprefix()default"";/***业务重复标识*/Stringkey()default"";}4.接口拦截器上定义了IdempotentRule注解,需要通过拦截器拦截正常的业务方法,并添加一些特殊的逻辑处理。@Aspect@Component@Slf4jpublicclassIdempotentAspect{@AutowiredprivateRedisTemplateidempotentRedisTemplate;@Around("execution(public**(..))&&@annotation(com.onyone.idempotent.annotation.IdempotentRule)")publicObjectlimit(ProceedingJoinPointpjp){MethodSignaturesignature=(MethodSignature)pjp.getSignature();对象[]参数=pjp.getArgs();String[]paramNames=signature.getParameterNames();方法method=signature.getMethod();IdempotentRuleidempotentRule=method.getAnnotation(IdempotentRule.class);Stringkey=idempotentRule.key();Stringprefix=idempotentRule.prefix();ExpressionParser解析器=newSpelExpressionParser();EvaluationContextcontext=newStandardEvaluationContext();context.setVariable(paramNames[0],params[0]);字符串repeatKey=(String)parser.parseExpression(key).getValue(cont分机);try{//先在缓存中做个标记BooleanlockResult=idempotentRedisTemplate.opsForValue().setIfAbsent(prefix+repeatKey,"Processing....",20,TimeUnit.SECONDS);if(lockResult){//业务逻辑处理returnpjp.proceed();}else{thrownewException("重复提交......");}}catch(Throwablee){e.printStackTrace();}finally{//处理完成后,删除标记idempotentRedisTemplate.delete(prefix+repeatKey);}返回空值;}}这里,比较特殊的是提取请求的唯一标识,由于不同的业务,请求的唯一标识是不同的。这里,SPEL表达式用于开放规则设置能力,由业务方定义,例如:@IdempotentRule(key="#userParam.cardNumber",prefix="repeat_")。拦截器根据SPEL表达式(如“#userParam.cardNumber”)和请求参数对象,计算出当前请求的唯一标识的值,并将该值写入Redis,并设置时间。如果设置成功,则表示是第一次请求,继续后面的业务逻辑处理;否则判断为重复请求,直接丢弃。5.上层业务接口@RestController@RequestMapping("/user")publicclassUserController{/***创建新用户*/@RequestMapping(value="/create_user")@IdempotentRule(key="#userParam.cardNumber",prefix="repeat_")publicStringcreateUser(@RequestBodyUserParamuserParam){//模拟业务处理return"创建用户成功!";}}@DatapublicclassUserParam{privateStringcardNumber;privateStringname;}测试结果1,构造客户端请求,第一次处理成功。2、在Redis缓存中,可以找到请求设置的锁标志。3、模拟重复,连续多次提交请求,请求会被拦截,并抛出异常。