Redis +Token 实现接口幂等性

1.幂等性

幂等:任意多次执行所产生的影响均与一次执行的影响相同

接口幂等性:
在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。

即,任意多次执行对资源本身所产生的影响均与一次执行的影响相同

2.非幂等操作的问题

  • 前端重复提交表单
  • 恶意刷单
  • 接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  • 消息进行重复消费: 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

3.HTTP的幂等性

  • GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性
  • HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性
  • OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的
  • DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调用方可以调用N次而不用担心引起的错误(根据业务需求而变)
  • PUT:用于更新资源,有副作用,但是它应该满足幂等性,比如根据id更新数据,调用多次和N次的作用是相同的(根据业务需求而变)
  • POST:用于添加资源,多次提交很可能产生副作用,比如订单提交,多次提交很可能产生多笔订单

4. 常见解决方案

  • 唯一索引 – 防止新增脏数据
  • token机制 – 防止页面重复提交
  • 悲观锁 – 获取数据的时候加锁(锁表或锁行)
  • 乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
  • 分布式锁 – redis(jedis、redisson)或zookeeper实现
  • 状态机 – 状态变更, 更新数据时判断状态
  • 请求序列号:每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序 ID,也可以是一个订单号,一般由下游生成,在调用上游服务端接口时附加该序列号和用于认证的 ID。

适用场景:

方案 适用方法 缺点
数据库唯一主键 插入、删除 只能用于插入操作、只能用于存在唯一主键的场景
乐观锁 更新 只能用于更新操作、表中需要添加额外的字段
请求序列号 插入、删除、更新 需要保证下游服务生成唯一序列号、需要Redis
token机制 插入、删除、更新 需要Redis及token生成方案

5. TOKEN机制具体实现

  1. 服务端提供了发送token的接口。在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。(微服务肯定是分布式了,如果单机就适用jvm缓存)。
  2. 然后调用业务接口请求时,把token携带过去,一般放在请求头部。
  3. 服务器判断token是否存在redis中,存在表示第一次请求,这时把redis中的token删除,继续执行业务。
  4. 如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。

代码实现:

Service:

package com.example.service;



public interface RedisTokenService { 

    //生成Token
    public String getToken();

    //删除Token
    public Boolean deleteToken(String token);

}

package com.example.service.impl;

import com.example.service.RedisTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/** * 描述: * RedisTokenService * * @author XueGuCheng * @create 2021-02-28 19:36 */
@Service
public class RedisTokenServiceImpl implements RedisTokenService { 

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //生成Token
    @Override
    public String getToken() { 
        //使用UUID生成 token
        String token = UUID.randomUUID().toString();
        //存入Redis,key:token,过期时间3分钟
        stringRedisTemplate.opsForValue().set(token,token,3, TimeUnit.MINUTES);
        return token;
    }

    //删除Token,true表示第一次提交,false表示重复提交
    @Override
    public Boolean deleteToken(String token) { 
        return stringRedisTemplate.delete(token);
    }
}

自定义注解:

package com.example.annotation;

import java.lang.annotation.*;

/** * 防重复提交的注解 * 放在Controller类:表示当前类的所有接口都是幂等性 * 放在方法上:表示当前方法是幂等性 */
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatLimiter { 
}

自定义拦截器和拦截规则:

package com.example.component;

import com.example.annotation.RepeatLimiter;
import com.example.service.RedisTokenService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Objects;

/** * 描述: * 拦截请求头,用来对参数做校验 * * @author XueGuCheng * @create 2021-02-28 19:59 */

public class HeaderIntercept implements HandlerInterceptor { 

    @Autowired
    RedisTokenService redisTokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
        if (handler instanceof HandlerMethod) { 
            //获取方法上的参数
            RepeatLimiter repeatLimiter = AnnotationUtils.findAnnotation(((HandlerMethod) handler).getMethod(), RepeatLimiter.class);
            if (Objects.isNull(repeatLimiter)) { 
                //获取Controller类上的注解
                repeatLimiter = AnnotationUtils.findAnnotation(((HandlerMethod) handler).getBean().getClass(), RepeatLimiter.class);
            }

            //repeatLimiter不为空,即使用了@RepeatLimiter注解,需要进行拦截验证
            if (Objects.nonNull(repeatLimiter)) { 
                //获取请求头携带的token
                //测试所用,故为方便,直接定义参数格式为: token:xxx
                String token = request.getHeader("token");
                System.out.println(token);
                //如果没有携带token,抛异常
                if (StringUtils.isEmpty(token)) { 
                    throw new RuntimeException("重复提交");
                }
                //幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回提示
                Boolean flag = redisTokenService.deleteToken(token);
                if (Boolean.FALSE.equals(flag)) { 
                    //重复提交
                    throw new RuntimeException("重复提交");
                }

            }

        }
        return true;
    }
}

package com.example.component;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/** * 描述: * 拦截规则 * * @author XueGuCheng * @create 2021-02-28 22:05 */
@Configuration
public class HeaderInterceptConfig implements WebMvcConfigurer { 

    @Bean
    HeaderIntercept headerIntercept(){ 
        return new HeaderIntercept();
    }

    //配置拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) { 
        registry.addInterceptor(headerIntercept())
                .addPathPatterns("/RepeatLimiter/add");
    }
}

Controller:

package com.example.controller;

import com.example.annotation.RepeatLimiter;
import com.example.service.RedisTokenService;
import com.example.service.UserService;
import com.example.vo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/** * 描述: * 验证重复提交 * * @author XueGuCheng * @create 2021-02-28 20:58 */
@RestController
@RequestMapping("/RepeatLimiter")
public class RepeatLimiterController { 

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTokenService redisTokenService;


    @PostMapping("/add")
    @RepeatLimiter
    public String add(@RequestBody User user){ 
        //userService.insertUser(user); //有字段唯一性,故注释
        return "添加成功";
    }

    @GetMapping("/token")
    public String getToken(){ 
        String token = redisTokenService.getToken();
        return token;
    }


}

测试效果:

获取token:

无token请求:

有token的第一次请求:

有token的第二次请求:

总结:

请求无token时,视为无效请求
只有第一次携带正确token的请求是有效请求,之后的请求就算携带了正确token也视为无效请求

本文地址:https://blog.csdn.net/xueguchen/article/details/114242205

(0)
上一篇 2022年3月21日
下一篇 2022年3月21日

相关推荐