(Transferir + compartir) SpringBoot + Redis + Anotación + Interceptor para lograr la verificación de idempotencia de la interfaz / Wangzaiplus Code Road

1. Concepto

Idempotence, en términos sencillos, es una interfaz que inicia la misma solicitud varias veces, y debe asegurarse de que la operación solo se pueda realizar una vez, por
ejemplo:

  • Interfaz de pedido, no se puede crear ningún pedido varias veces

  • Interfaz de pago, el mismo pedido solo se puede deducir una vez para pagos repetidos

  • Interfaz de devolución de llamada de Alipay, puede haber múltiples devoluciones de llamada y deben manejarse las devoluciones de llamada repetidas

  • Interfaz de envío de formularios ordinarios, si hace clic para enviar varias veces debido al tiempo de espera de la red y otras razones, solo puede tener éxito una vez,
    etc.

Dos soluciones comunes

  1. Índice único: evita nuevos datos sucios

  2. Mecanismo de token: evita el envío repetido de páginas

  3. Bloqueo-bloqueo pesimista al adquirir datos (bloquear tabla o bloquear fila)

  4. Bloqueo optimista basado en la versión del número de versión para verificar los datos en el momento en que se actualizan los datos

  5. Implementación distribuida de lock-redis (jedis, redisson) o zookeeper

  6. Máquina de estado: cambios de estado, juzgando el estado al actualizar los datos

En tercer lugar, la realización de este artículo.

     Este artículo adopta el segundo método para lograr la verificación de idempotencia de la interfaz a través del mecanismo de token redis +.

Cuatro, realización de ideas

        Cree un token único para cada solicitud que necesite garantizar la idempotencia, obtenga el token primero y almacene este token en redis, cuando solicite la interfaz, coloque este token en el encabezado o solicite la interfaz como parámetro de solicitud, y el back-end interfaz juzga redis ¿Existe este token en:

  • Si existe, la lógica empresarial se procesa normalmente y el token se elimina de redis. Luego, si es una solicitud repetida, porque el token se eliminó, no puede pasar la verificación y volver al mensaje No repetir operación.

  • Si no existe, significa que el parámetro es ilegal o que la solicitud se repite, simplemente regrese al indicador

5. Introducción al proyecto

  • springboot

  • repetir

  • @ApiIdempotent anotación + interceptor intercepta la solicitud

  • Manejo de excepciones globales de @ControllerAdvice

  • Herramienta de medición de presión: jmeter

Descripción:

  • Este artículo se centra en la implementación central de idempotence. Los detalles sobre cómo Springboot integra redis, ServerResponse, ResponseCode, etc. están más allá del alcance de este artículo. Los amigos interesados ​​pueden consultar mi proyecto Github: https://github.com/wangzaiplus/ springboot / árbol / wxw

Seis, implementación de código

1.pom

<!-- Redis-Jedis --><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><!--lombok 本文用到@Slf4j注解, 也可不引用, 自定义log即可--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.10</version></dependency>

2.JedisUtil

 

package com.wangzaiplus.test.util;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;@Component@Slf4jpublic 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) {
   
               log.error("set key:{} value:{} error", key, value, e);            return null;        } 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();            return jedis.setex(key, expireTime, value);        } catch (Exception e) {
   
               log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 取值     *     * @param key     * @return     */    public String get(String key) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.get(key);        } catch (Exception e) {
   
               log.error("get key:{} error", key, e);            return null;        } 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) {
   
               log.error("del key:{} error", key, e);            return null;        } 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) {
   
               log.error("exists key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 设值key过期时间     *     * @param key     * @param expireTime 过期时间, 单位: s     * @return     */    public Long expire(String key, int expireTime) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.expire(key.getBytes(), expireTime);        } catch (Exception e) {
   
               log.error("expire key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 获取剩余时间     *     * @param key     * @return     */    public Long ttl(String key) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.ttl(key);        } catch (Exception e) {
   
               log.error("ttl key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    private void close(Jedis jedis) {
   
           if (null != jedis) {
   
               jedis.close();        }    }}

3. Anotación personalizada @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 {
   
   }

Interceptor 4.ApiIdempotentInterceptor

package com.wangzaiplus.test.interceptor;import com.wangzaiplus.test.annotation.ApiIdempotent;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) {
   
               check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示        }        return true;    }    private void check(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 {
   
       }}

5.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;@Servicepublic class TokenServiceImpl implements TokenService {
   
       private static final String TOKEN_NAME = "token";    @Autowired    private JedisUtil jedisUtil;    @Override    public ServerResponse createToken() {
   
           String str = RandomUtil.UUID32();        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());        }    }}

6.aplicación de prueba

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();    }}De acuerdo, hasta ahora, el código de verificación está listo, luego pruebe y verifique.

Siete, verificación de prueba

1. Obtenga el controlador de token TokenController

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();    }}

2.TestController, preste atención a la anotación @ApiIdempotent, simplemente declare esta anotación en el método que requiere verificación de idempotencia, sin necesidad de verificación

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")@Slf4jpublic class TestController {
   
       @Autowired    private TestService testService;    @ApiIdempotent    @PostMapping("testIdempotence")    public ServerResponse testIdempotence() {
   
           return testService.testIdempotence();    }}

3. Obtén la ficha

 

Ver redis

 

4. Pruebe la seguridad de la interfaz: use la herramienta de prueba jmeter para simular 50 solicitudes simultáneas y use el token obtenido en el paso anterior como parámetro

 

 

5. El encabezado o los parámetros no pasan el token, o el valor del token está vacío, o el valor del token se completa al azar y la verificación falla, como el valor del token "abcd".

 

8. Puntos a tener en cuenta (muy importante)

 

En la figura anterior, no puede simplemente eliminar el token directamente sin verificar si la eliminación es exitosa, habrá un problema de seguridad de simultaneidad, porque es posible que varios subprocesos vayan a la línea 46 al mismo tiempo, y el token no ha sido eliminado en este momento, así que continúe si no verifica el resultado de eliminación de jedisUtil.del (token) y lo deja ir directamente, entonces todavía habrá un problema de envío repetido, incluso si en realidad solo hay una operación de eliminación real, vamos a reproducirlo a continuación

Modifica ligeramente el código:

 

Solicitar de nuevo

Mira la consola de nuevo

Aunque solo hay un token que realmente se elimina, dado que el resultado de la eliminación no se verifica, todavía hay un problema de concurrencia, por lo que debe verificarse

Nueve, resumen

De hecho, la idea es muy simple, que es asegurar la unicidad para cada solicitud, asegurando así la idempotencia. A través de interceptor + anotación, no es necesario escribir código repetido para cada solicitud. De hecho, también puede usar spring aop para lograr eso, no importa.

 

                                     ------------------------------------------- <FIN> ---- -----------------------------------

El camino del código de  wangzaiplus  código de agricultor 10 de mayo

Fuente: jianshu.com/p/6189275403ed

 

 

 

Supongo que te gusta

Origin blog.csdn.net/qq_31653405/article/details/107204508
Recomendado
Clasificación