Schéma d'implémentation de l'idempotence de l'interface dans SpringBoot - annotation personnalisée + implémentation de Redis + intercepteur pour empêcher la soumission de commandes répétées

Scènes

SpringBoot+Redis+custom annotations implémentent l'anti-brush d'interface (limiter le nombre maximum de requêtes par unité de temps pour différentes interfaces) :

SpringBoot+Redis+custom annotations implémentent l'anti-brush d'interface (limiter le nombre maximum de requêtes par unité de temps pour différentes interfaces)

La mise en œuvre de l'idempotence des interfaces suivantes est similaire au blog ci-dessus, auquel on peut se référer.

Idempotence de l'interface

Qu’est-ce que l’idempotence ?

L'idempotence est un concept en mathématiques et en informatique. Lorsqu'une certaine opération élémentaire en mathématiques est idempotente, elle agit deux fois sur n'importe quel élément.

aura le même résultat que s’il était appliqué une seule fois. En programmation informatique, une opération idempotente se caractérise par l'exécution de tous

Les deux ont le même impact qu’une seule exécution. Une fonction ou une méthode idempotente est une fonction qui peut être exécutée de manière répétée avec les mêmes paramètres,

et une fonction qui obtient le même résultat. Ces fonctions n'affectent pas l'état du système et il n'y a pas lieu de s'inquiéter des modifications du système provoquées par des exécutions répétées.

Qu’est-ce que l’idempotence de l’interface ?

Dans HTTP/1.1, l'idempotence est définie. Il décrit comment une ou plusieurs requêtes pour une ressource devraient avoir

Le même résultat (sauf pour des problèmes tels que les délais d'attente du réseau), c'est-à-dire que la première requête a des effets secondaires sur les ressources, mais les requêtes suivantes

Ni l’un ni l’autre n’aura plus d’effets secondaires sur les ressources. L’effet secondaire ici n’est pas de détruire le résultat ou de produire des résultats inattendus.

Autrement dit, n'importe quel nombre d'exécutions aura le même impact sur la ressource elle-même qu'une seule exécution.

Pourquoi faut-il atteindre l’idempotence ?

En général, lorsque l'interface est appelée, les informations peuvent être renvoyées normalement et ne seront pas soumises à plusieurs reprises, mais des problèmes peuvent survenir lorsque les situations suivantes sont rencontrées :

comme:

Le front-end soumet le formulaire à plusieurs reprises :

Lorsqu'il remplit certains formulaires, l'utilisateur remplit et soumet, mais dans de nombreux cas, en raison des fluctuations du réseau, l'utilisateur ne répond pas à temps à sa soumission réussie.

Cela amène l'utilisateur à penser que la soumission n'a pas réussi, puis continue de cliquer sur le bouton Soumettre. À ce moment, des demandes répétées de soumission de formulaire se produiront.

L'utilisateur glisse malicieusement les commandes :

Par exemple, lors de la mise en œuvre de la fonction de vote des utilisateurs, si l'utilisateur soumet à plusieurs reprises des votes pour un utilisateur, l'interface recevra

Les utilisateurs soumettent à plusieurs reprises des informations de vote, ce qui rendra les résultats du vote sérieusement incompatibles avec les faits.

L'interface a expiré et a été soumise à plusieurs reprises :

Dans de nombreux cas, les outils clients HTTP activent par défaut le mécanisme de nouvelle tentative d'expiration du délai, notamment lorsqu'un tiers appelle l'interface, afin d'empêcher le réseau

Pour les échecs de requête causés par des délais d'attente de fluctuation, etc., un mécanisme de nouvelle tentative sera ajouté, entraînant plusieurs soumissions d'une même requête.

Les messages sont consommés à plusieurs reprises :

Lors de l'utilisation du middleware de messages MQ, si une erreur se produit dans le middleware de messages et que les informations de consommation ne sont pas soumises à temps, ce qui entraîne une consommation répétée.

Le plus grand avantage de l'utilisation de l'idempotence est de faire en sorte que l'interface garantisse tout fonctionnement idempotent, évitant ainsi les problèmes inconnus causés par le système en raison des tentatives.

Il existe de nombreuses solutions pour atteindre l'idempotence de l'interface.Ce qui suit enregistre une annotation personnalisée ainsi qu'une implémentation d'intercepteur et de redis pour simuler la prévention des soumissions de commandes répétées.

Note:

Blog :
 Domineering Rogue Temperament_C#, Architecture Road, SpringBoot-CSDN Blog

accomplir

1. Créez un nouveau projet SpringBoot et ajoutez les dépendances nécessaires

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--MySQL驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--MyBatis整合SpringBoot框架的起步依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.1</version>
        </dependency>
        <!-- redis 缓存操作 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
    </dependencies>

2. Modifiez le fichier de configuration yml pour ajouter la configuration pertinente

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 996
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # tomcat最大线程数,默认为200
    max-threads: 800
    # Tomcat启动初始化的线程数,默认值25
    min-spare-threads: 30

# 数据源
spring:
  application:
    name: badao-tcp-demo
  datasource:
   url:jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    dbcp2:
      min-idle: 5                                # 数据库连接池的最小维持连接数
      initial-size: 5                            # 初始化连接数
      max-total: 5                               # 最大连接数
      max-wait-millis: 150                       # 等待连接获取的最大超时时间

  # redis 配置
  redis:
    # 地址
    #本地测试用
    host: 127.0.0.1
    port: 6379
    password: 123456
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

# mybatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml    # mapper映射文件位置
  type-aliases-package: com.badao.demo.entity    # 实体类所在的位置
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   #用于控制台打印sql语句


3. Créez deux classes de configuration pour Redis

RedisConfig :

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;

/**
 * redis配置
 *
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    @Primary
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

FastJson2JsonRedisSerializer :

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 *
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    @SuppressWarnings("unused")
    private ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }

    public void setObjectMapper(ObjectMapper objectMapper)
    {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

4. Créez une nouvelle classe d'outils Redis

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * spring redis 工具类
 *
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    @Autowired
    public StringRedisTemplate stringRedisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        return operation;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value, timeout, timeUnit);
        return operation;
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public void deleteObject(String key)
    {
        redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection
     */
    public void deleteObject(Collection collection)
    {
        redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
    {
        ListOperations listOperation = redisTemplate.opsForList();
        if (null != dataList)
        {
            int size = dataList.size();
            for (int i = 0; i < size; i++)
            {
                listOperation.leftPush(key, dataList.get(i));
            }
        }
        return listOperation;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(String key)
    {
        List<T> dataList = new ArrayList<T>();
        ListOperations<String, T> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);

        for (int i = 0; i < size; i++)
        {
            dataList.add(listOperation.index(key, i));
        }
        return dataList;
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(String key)
    {
        Set<T> dataSet = new HashSet<T>();
        BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     * @return
     */
    public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (null != dataMap)
        {
            for (Map.Entry<String, T> entry : dataMap.entrySet())
            {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(String key)
    {
        Map<String, T> map = redisTemplate.opsForHash().entries(key);
        return map;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(String pattern)
    {
        return redisTemplate.keys(pattern);
    }

    /**
     * 如果redis中不存在则存储进redis
     * @return
     */
    public Boolean setIfAbsent(Object key,Object value,long timeout){
        return redisTemplate.opsForValue().setIfAbsent(key, value,timeout, TimeUnit.SECONDS);
    }
}

5. Créez une nouvelle classe d'annotation personnalisée Idempotent

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

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

    /**
     * 用于拼接幂等性判断的key的入参字段
     * @return
     */
    String[] fields();
    /**
     * 用于接口幂等性校验的Redis中Key的过期时间,单位秒
     * @return
     */
    long timeout() default 10l;
}

6. Créez un intercepteur personnalisé IdempotentInterceptor

import com.badao.demo.annotation.Idempotent;
import com.badao.demo.utils.RedisCache;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;

@Component
public class IdempotentInterceptor implements HandlerInterceptor {

    @Resource
    private RedisCache redisCache;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Idempotent idempotent = ((HandlerMethod)handler).getMethodAnnotation(Idempotent.class);
        if(idempotent == null){
            return true;
        }
        String idempotentKey = this.idempotentKey(idempotent,request);
        Boolean success = redisCache.setIfAbsent(idempotentKey,1, idempotent.timeout());
        if(Boolean.FALSE.equals(success)){
            render(response,"请勿重复请求");
            return false;
        }
        return true;
    }

    private String idempotentKey(Idempotent idempotent,HttpServletRequest request){
        String[] fields = idempotent.fields();
        StringBuilder idempotentKey = new StringBuilder();
        for (String field : fields) {
            String parameter = request.getParameter(field);
            idempotentKey.append(parameter);
        }
        return idempotentKey.toString();
    }

    /**
     * 接口渲染
     * @param response
     * @throws Exception
     */
    private void render(HttpServletResponse response,String message)throws Exception {
        response.setContentType("application/json;charset=UTF-8");
        OutputStream out = response.getOutputStream();
        out.write(message.getBytes("UTF-8"));
        out.flush();
        out.close();
    }
}

7. Configurez l'intercepteur IdempotentInterceptor pour qu'il s'inscrive dans la chaîne d'intercepteurs SpringMVC

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;

/**
 * 配置拦截器IdempotentInterceptor注册到SpringMVC的拦截器链中
 */
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    @Resource
    private IdempotentInterceptor idempotentInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempotentInterceptor);
    }
}

8. Créez une interface de test et ajoutez des annotations personnalisées

import com.badao.demo.annotation.Idempotent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Idempotent(fields = {"orderId"},timeout = 10)
    @GetMapping("/test")
    public String test(@RequestParam("orderId") String orderId, String remark){
        return "success";
    }
}

Ici, orderId est spécifié comme paramètre ID unique de la commande, et si la demande avec le même champ est définie dans les 10 secondes, elle sera interceptée

9. Créer une méthode de test

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@SpringBootTest
class IdempotenceTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Test
    void test1() throws Exception {
        //初始化MockMvc
        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        //循环调用5次进行测试
        for (int i = 1; i <= 5; i++) {
            System.out.println("第"+i+"次调用接口");
            //调用接口
            String result = mockMvc.perform(MockMvcRequestBuilders.get("/test")
                    .accept(MediaType.TEXT_HTML)
                    .param("orderId","001")
                    .param("remark","badao"))
                    .andReturn()
                    .getResponse()
                    .getContentAsString();
            System.out.println(result);
        }
    }
}

Résultats de test

Utilisez Postman et d'autres outils de test d'interface pour retester et vérifier

 

Je suppose que tu aimes

Origine blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/131910440
conseillé
Classement