什麼是接口的冪等性,如何實現接口冪等性? (一)冪等性概念 (二)冪等性的解決方案 (三)token機制的實現 (四)結果驗證

推薦閱讀:

什麼是接口的冪等性,如何實現接口冪等性?

(一)冪等性概念

冪等性原本是數學上的概念,用在接口上就可以理解爲:同一個接口,多次發出同一個請求,必須保證操作只執行一次。 調用接口發生異常並且重複嘗試時,總是會造成系統所無法承受的損失,所以必須阻止這種現象的發生。 比如下面這些情況,如果沒有實現接口冪等性會有很嚴重的後果: 支付接口,重複支付會導致多次扣錢 訂單接口,同一個訂單可能會多次創建。

(二)冪等性的解決方案

唯一索引 使用唯一索引可以避免髒數據的添加,當插入重複數據時數據庫會拋異常,保證了數據的唯一性。

樂觀鎖 這裏的樂觀鎖指的是用樂觀鎖的原理去實現,爲數據字段增加一個version字段,當數據需要更新時,先去數據庫裏獲取此時的version版本號

select version from tablename where xxx

更新數據時首先和版本號作對比,如果不相等說明已經有其他的請求去更新數據了,提示更新失敗。

update tablename set count=count+1,version=version+1 where version=#{version}

悲觀鎖 樂觀鎖可以實現的往往用悲觀鎖也能實現,在獲取數據時進行加鎖,當同時有多個重複請求時其他請求都無法進行操作

分佈式鎖 冪等的本質是分佈式鎖的問題,分佈式鎖正常可以通過redis或zookeeper實現;在分佈式環境下,鎖定全局唯一資源,使請求串行化,實際表現爲互斥鎖,防止重複,解決冪等。

token機制 token機制的核心思想是爲每一次操作生成一個唯一性的憑證,也就是token。一個token在操作的每一個階段只有一次執行權,一旦執行成功則保存執行結果。對重複的請求,返回同一個結果。token機制的應用十分廣泛。

(三)token機制的實現

這裏展示通過token機制實現接口冪等性的案例:github文末自取 首先引入需要的依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.1、配置請求的方法體和枚舉類

首先配置一下通用的請求返回體

public class Response {
    private int status;
    private String msg;
    private Object data;
    //省略get、set、toString、無參有參構造方法
}

以及返回code

public enum ResponseCode {
    // 通用模塊 1xxxx
    ILLEGAL_ARGUMENT(10000, "參數不合法"),
    REPETITIVE_OPERATION(10001, "請勿重複操作"),
    ;
    ResponseCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    private Integer code;
    private String msg;
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

3.2 自定義異常以及配置全局異常類

public class ServiceException extends RuntimeException{
    private String code;
    private String msg;
    //省略get、set、toString以及構造方法
}

配置全局異常捕獲器

@ControllerAdvice
public class MyControllerAdvice {
    @ResponseBody
    @ExceptionHandler(ServiceException.class)
    public Response serviceExceptionHandler(ServiceException exception){
        Response response=new Response(Integer.valueOf(exception.getCode()),exception.getMsg(),null);
        return response;
    }
}

3.3 編寫創建Token和驗證Token的接口以及實現類

@Service
public interface TokenService {
    public Response createToken();
    public Response checkToken(HttpServletRequest request);
}

具體實現類,核心的業務邏輯都寫在註釋中了

@Service
public class TokenServiceImpl implements TokenService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public Response createToken() {
        //生成uuid當作token
        String token = UUID.randomUUID().toString().replaceAll("-","");
        //將生成的token存入redis中
        redisTemplate.opsForValue().set(token,token);
        //返回正確的結果信息
        Response response=new Response(0,token.toString(),null);
        return response;
    }

    @Override
    public Response checkToken(HttpServletRequest request) {
        //從請求頭中獲取token
        String token=request.getHeader("token");
        if (StringUtils.isBlank(token)){
            //如果請求頭token爲空就從參數中獲取
            token=request.getParameter("token");
            //如果都爲空拋出參數異常的錯誤
            if (StringUtils.isBlank(token)){
                throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(),ResponseCode.ILLEGAL_ARGUMENT.getMsg());
            }
        }
        //如果redis中不包含該token,說明token已經被刪除了,拋出請求重複異常
        if (!redisTemplate.hasKey(token)){
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
        //刪除token
        Boolean del=redisTemplate.delete(token);
        //如果刪除不成功(已經被其他請求刪除),拋出請求重複異常
        if (!del){
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
        return new Response(0,"校驗成功",null);
    }
}

3.4 配置自定義註解

這是比較重要的一步,通過自定義註解在需要實現接口冪等性的方法上添加此註解,實現token驗證

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}

接口攔截器

public class ApiIdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod= (HandlerMethod) handler;
        Method method=handlerMethod.getMethod();
        ApiIdempotent methodAnnotation=method.getAnnotation(ApiIdempotent.class);
        if (methodAnnotation != null){
            // 校驗通過放行,校驗不通過全局異常捕獲後輸出返回結果
            tokenService.checkToken(request);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

3.5 配置攔截器以及redis

配置webConfig,添加攔截器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor());
    }

    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor() {
        return new ApiIdempotentInterceptor();
    }
}

配置redis,使得中文可以正常傳輸

@Configuration
public class RedisConfig {
    //自定義的redistemplate
    @Bean(name = "redisTemplate")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        //創建一個RedisTemplate對象,爲了方便返回key爲string,value爲Object
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        //設置json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new
                Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
        //string的序列化
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        //key採用string的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //value採用jackson的序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hashkey採用string的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //hashvalue採用jackson的序列化方式
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

最後是controller

@RestController
@RequestMapping("/token")
public class TokenController {
    @Autowired
    private TokenService tokenService;

    @GetMapping
    public Response token(){
        return tokenService.createToken();
    }

    @PostMapping("checktoken")
    public Response checktoken(HttpServletRequest request){
        return tokenService.checkToken(request);
    }
}

(四)結果驗證

首先通過token接口創建一個token出來,此時redis中也存在了該token

在jmeter中同時運行50個請求,我們可以觀察到,只有第一個請求校驗成功,後續的請求均提示請勿重複操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章