springboot + redis + + interceptor annotation implement interface check idempotent

First, the concept

Idempotency, popular to say that an interface with a request to initiate multiple times, must ensure that the operation can be performed only once
, such as:

  • Orders interfaces, you can not create multiple orders
  • Payment interface, double payments for the same order only once deducted money
  • Alipay callback interface, multiple callbacks may be, have to deal with repeated callbacks
  • Common form submission interfaces, network timeouts and other reasons because click submit multiple times, only once successfully
    , etc.

Second, the common solution

  1. The only index - prevent new dirty data
  2. token mechanisms - to prevent duplicate submission page
  3. Pessimistic locking - time access to data lock (lock or lock table rows)
  4. Optimistic locking - based version version realized, at that moment the update data verification data
  5. Distributed Lock - redis (jedis, redisson) or zookeeper achieve
  6. State Machine - state changes, status update data is determined

Third, the paper implements

In this paper, two kinds of ways, i.e., the interface implemented by checking idempotent redis + token mechanism

Fourth, the realization of ideas

Is a need to ensure that each request idempotency created a unique identifier token, to obtain token, and this tokenis stored redis, when the request interface, this tokenput parameter as a request header or request interface, the backend interface determines whether this redis token:

  • If so, normal processing business logic, and delete from the redis token, then, if the request is repeated, since tokenhas been removed, it can not pass the validation return 请勿重复操作tips
  • If not, explain the parameters illegal or repeat request, return the prompts to

V. Project

  • springboot
  • repeat
  • @ApiIdempotent+ Annotation request interceptor intercepts
  • @ControllerAdvice global exception handler
  • Pressure measurement tools: jmeter

Description:

  • This article focuses on idempotency core implementation, on how to integrate springboot Redis , serverResponse , ResponseCode and other minutiae is not within the scope of this article, the interested junior partner can see my Github project: https://github.com/wangzaiplus/springboot / tree / wxw

Sixth, code implementation

  1. pom
        <!-- Redis-Jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
  1. JedisUtil
package com.wangzaiplus.test.util;

import com.wangzaiplus.test.common.Constant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;

@Component
public class JedisUtil {

    @Autowired
    private JedisPool jedisPool;

    private Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * 设值
     *
     * @param key
     * @param value
     * @return
     */
    public String set(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.set(key, value);
        } catch (Exception e) {
            throw new RuntimeException("set异常: key: " + key + ", cause: " + e.getMessage());
        } finally {
            close(jedis);
        }
    }

    /**
     * 设值
     *
     * @param key
     * @param value
     * @param expireTime 过期时间, 单位: s
     * @return
     */
    public String set(String key, String value, int expireTime) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            String result = jedis.set(key, value);
            if (Constant.Redis.OK.equals(result)) {
                jedis.expire(key, expireTime);
            }
            return result;
        } catch (Exception e) {
            throw new RuntimeException("set-expireTime异常: key: " + key + ", cause: " + e.getMessage());
        } finally {
            close(jedis);
        }
    }

    /**
     * 删除key
     *
     * @param key
     * @return
     */
    public Long del(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.del(key.getBytes());
        } catch (Exception e) {
            throw new RuntimeException("del异常: key: " + key + ", cause: " + e.getMessage());
        } finally {
            close(jedis);
        }
    }

    /**
     * 判断key是否存在
     *
     * @param key
     * @return
     */
    public Boolean exists(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.exists(key.getBytes());
        } catch (Exception e) {
            throw new RuntimeException("exists异常: key: " + key + ", cause: " + e.getMessage());
        } finally {
            close(jedis);
        }
    }

    public void close(Jedis jedis) {
        if (null != jedis) {
            jedis.close();
        }
    }

}

Description: limited space, complete JedisUtilrefer to GitHub

  1. Custom annotation@ApiIdempotent
package com.wangzaiplus.test.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 在需要保证 接口幂等性 的Controller的方法上使用此注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
  1. ApiIdempotentInterceptorInterceptor
package com.wangzaiplus.test.interceptor;

import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.exception.ServiceException;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * 接口幂等性拦截器
 */
public class ApiIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
        if (methodAnnotation != null) {
            checkApiIdempotent(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
        }

        return true;
    }

    private void checkApiIdempotent(HttpServletRequest request) {
        tokenService.checkToken(request);
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}
  1. TokenServiceImpl
package com.wangzaiplus.test.service.impl;

import com.wangzaiplus.test.common.Constant;
import com.wangzaiplus.test.common.ResponseCode;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.exception.ServiceException;
import com.wangzaiplus.test.service.TokenService;
import com.wangzaiplus.test.util.JedisUtil;
import com.wangzaiplus.test.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

@Service
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private JedisUtil jedisUtil;

    @Override
    public ServerResponse createToken() {
        String str = RandomUtil.generateStr(24);
        StrBuilder token = new StrBuilder();
        token.append(Constant.Redis.TOKEN_PREFIX).append(str);

        jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE);

        return ServerResponse.success(token.toString());
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {// header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.isBlank(token)) {// parameter中也不存在token
                throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
            }
        }

        if (!jedisUtil.exists(token)) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        }

        Long del = jedisUtil.del(token);
        if (del <= 0) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
    }

}
  1. TestApplication
package com.wangzaiplus.test;

import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication
@MapperScan("com.wangzaiplus.test.mapper")
public class TestApplication  extends WebMvcConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

    /**
     * 跨域
     * @return
     */
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 接口幂等性拦截器
        registry.addInterceptor(apiIdempotentInterceptor());
        super.addInterceptors(registry);
    }

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

}

OK, so far, check the code is ready, the next test validation

Seven, testing and certification

  1. Acquisition tokenof controlTokenController
package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private TokenService tokenService;

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

}
  1. TestControllerNote @ApiIdempotentannotation, notes can declare this, do not need to check on the method requires idempotency verification of no effect
package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Autowired
    private TestService testService;

    @ApiIdempotent
    @PostMapping("testIdempotence")
    public ServerResponse testIdempotence() {
        return testService.testIdempotence();
    }

}
  1. Obtaintoken
    image.png

View redis
image.png

  1. Test Interface Security: 50 jmeter testing tools simulate concurrent requests, the upper step of the token acquired as a parameter
    image.png

image.png

  1. header parameters are not transmitted or token or token is empty, or chaos filled token value, they were unable to check, such as the token value "abcd"
    image.png

Eight Points of Attention (very important)

image.png

The figure above, can not simply delete token without checking whether the deletion is successful, there will be complicated by security problems, because there are multiple threads may come on line 46, token has not yet been deleted at this time, so continue to under execution, if you do not check jedisUtil.del(token)the deletion results directly released, then there will be duplicate or submit questions, even though in fact only one actually delete operation, to reproduce it below

Slightly modified the code below:
image.png

Request again
image.png

Look at the console
image.png

Although only a truly deleted token, but because there is no check to delete the results, they still have concurrency issues, therefore, must check

Nine, summary

In fact, the idea is very simple, it is guaranteed to be unique for each request, so as to ensure idempotency by interceptor + notes, would not every request to write duplicate code, in fact, can also use spring aop to achieve, it does not matter

If the junior partner have any questions or suggestions welcome

Github
https://github.com/wangzaiplus/springboot/tree/wxw

Guess you like

Origin www.cnblogs.com/wangzaiplus/p/10931335.html