Maison  >  Article  >  base de données  >  Comment utiliser SpringBoot + Redis pour implémenter la limitation de courant d'interface

Comment utiliser SpringBoot + Redis pour implémenter la limitation de courant d'interface

PHPz
PHPzavant
2023-05-27 15:01:191581parcourir

Configuration

Nous créons d'abord un projet Spring Boot, introduisons les dépendances Web et Redis et considérons que la limitation actuelle de l'interface est généralement marquée par des annotations et que les annotations sont analysées via AOP, nous devons donc également ajouter des dépendances AOP. Les dépendances finales sont comme suit :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Préparez ensuite une instance Redis à l'avance. Une fois notre projet configuré, nous pouvons directement configurer les informations de base de Redis, comme suit :

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123

Annotation de limitation de courant

Ensuite, nous créons une note de limitation de courant, nous divisez la limitation de courant en deux situations :

  • Limitation de courant globale pour l'interface actuelle, par exemple, l'interface est accessible 100 fois en 1 minute.

  • Limitation pour une certaine adresse IP, par exemple, une adresse IP est accessible 100 fois en 1 minute.

Pour ces deux situations, nous créons une classe d'énumération :

public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}

Ensuite, nous créons l'annotation de limitation de courant :

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key
     */
    String key() default "rate_limit:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
}

Le premier paramètre est la clé de limitation de courant. Ce n'est qu'un préfixe. Il sera complet dans. la clé est ce préfixe plus le chemin complet de la méthode d'interface, qui forment ensemble la clé de limitation actuelle. Cette clé sera stockée dans Redis.

Les trois autres paramètres sont faciles à comprendre, je n’entrerai donc pas dans les détails.

D'accord, si l'interface doit limiter le flux à l'avenir, ajoutez simplement l'annotation @RateLimiter sur cette interface, puis configurez les paramètres pertinents. @RateLimiter 注解,然后配置相关参数即可。

定制 RedisTemplate

在 Spring Boot 中,我们其实更习惯使用 Spring Data Redis 来操作 Redis,不过默认的 RedisTemplate 有一个小坑,就是序列化用的是 JdkSerializationRedisSerializer,不知道小伙伴们有没有注意过,直接用这个序列化工具将来存到 Redis 上的 key 和 value 都会莫名其妙多一些前缀,这就导致你用命令读取的时候可能会出错。

例如存储的时候,key 是 name,value 是 test,但是当你在命令行操作的时候,get name 却获取不到你想要的数据,原因就是存到 redis 之后 name 前面多了一些字符,此时只能继续使用 RedisTemplate 将之读取出来。

我们用 Redis 做限流会用到 Lua 脚本,使用 Lua 脚本的时候,就会出现上面说的这种情况,所以我们需要修改 RedisTemplate 的序列化方案。

可能有小伙伴会说为什么不用 StringRedisTemplate 呢?StringRedisTemplate 确实不存在上面所说的问题,但是它能够存储的数据类型不够丰富,所以这里不考虑。

修改 RedisTemplate 序列化方案,代码如下:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

这个其实也没啥好说的,key 和 value 我们都使用 Spring Boot 中默认的 jackson 序列化方式来解决。

Lua 脚本

这个其实我在之前 vhr 那一套视频中讲过,Redis 中的一些原子操作我们可以借助 Lua 脚本来实现,想要调用 Lua 脚本,我们有两种不同的思路:

  • 在 Redis 服务端定义好 Lua 脚本,然后计算出来一个散列值,在 Java 代码中,通过这个散列值锁定要执行哪个 Lua 脚本。

  • 直接在 Java 代码中将 Lua 脚本定义好,然后发送到 Redis 服务端去执行。

Spring Data Redis 中也提供了操作 Lua 脚本的接口,还是比较方便的,所以我们这里就采用第二种方案。

我们在 resources 目录下新建 lua 文件夹专门用来存放 lua 脚本,脚本内容如下:

local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call(&#39;get&#39;, key)
if current and tonumber(current) > count then
    return tonumber(current)
end
current = redis.call(&#39;incr&#39;, key)
if tonumber(current) == 1 then
    redis.call(&#39;expire&#39;, key, time)
end
return tonumber(current)

这个脚本其实不难,大概瞅一眼就知道干啥用的。KEYS 和 ARGV 都是一会调用时候传进来的参数,tonumber 就是把字符串转为数字,redis.call 就是执行具体的 redis 指令,具体流程是这样:

  • 首先获取到传进来的 key 以及 限流的 count 和时间 time。

  • 通过 get 获取到这个 key 对应的值,这个值就是当前时间窗内这个接口可以访问多少次。

  • 如果是第一次访问,此时拿到的结果为 nil,否则拿到的结果应该是一个数字,所以接下来就判断,如果拿到的结果是一个数字,并且这个数字还大于 count,那就说明已经超过流量限制了,那么直接返回查询的结果即可。

  • 如果拿到的结果为 nil,说明是第一次访问,此时就给当前 key 自增 1,然后设置一个过期时间。

  • 最后把自增 1 后的值返回就可以了。

其实这段 Lua 脚本很好理解。

接下来我们在一个 Bean 中来加载这段 Lua 脚本,如下:

@Bean
public DefaultRedisScript<Long> limitScript() {
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
    redisScript.setResultType(Long.class);
    return redisScript;
}

可以啦,我们的 Lua 脚本现在就准备好了。

注解解析

接下来我们就需要自定义切面,来解析这个注解了,我们来看看切面的定义:

@Aspect
@Component
public class RateLimiterAspect {
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    private RedisScript<Long> limitScript;
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (number==null || number.intValue() > count) {
                throw new ServiceException("访问过于频繁,请稍候再试");
            }
            log.info("限制请求&#39;{}&#39;,当前请求&#39;{}&#39;,缓存key&#39;{}&#39;", count, number.intValue(), key);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }
}

这个切面就是拦截所有加了 @RateLimiter

Customized RedisTemplate🎜🎜Dans Spring Boot, nous sommes en fait plus habitués à utiliser Spring Data Redis pour faire fonctionner Redis, mais le RedisTemplate par défaut a un petit écueil, c'est-à-dire qu'il utilise JdkSerializationRedisSerializer pour la sérialisation. Si vous utilisez directement cet outil de sérialisation, les clés et valeurs stockées dans Redis auront inexplicablement plus de préfixes, ce qui peut provoquer des erreurs lorsque vous les lisez avec des commandes. 🎜🎜Par exemple, lors du stockage, la clé est le nom et la valeur est le test, mais lorsque vous opérez sur la ligne de commande, get name ne peut pas obtenir les données souhaitées, car elles sont enregistrées. vers redis. Il y a quelques caractères supplémentaires devant le nom. Pour le moment, nous ne pouvons continuer à utiliser RedisTemplate que pour le lire. 🎜🎜Lorsque nous utiliserons Redis pour la limitation de courant, nous utiliserons des scripts Lua. Lors de l'utilisation de scripts Lua, la situation mentionnée ci-dessus se produira, nous devons donc modifier le schéma de sérialisation de RedisTemplate. 🎜
🎜Certains amis peuvent demander pourquoi ne pas utiliser StringRedisTemplate ? StringRedisTemplate ne présente pas les problèmes mentionnés ci-dessus, mais les types de données qu'il peut stocker ne sont pas assez riches, il n'est donc pas pris en compte ici. 🎜
🎜Modifiez le schéma de sérialisation RedisTemplate, le code est le suivant : 🎜
@RestController
public class HelloController {
    @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {
        return "hello>>>"+new Date();
    }
}
🎜En fait, il n'y a rien à dire à ce sujet. Nous utilisons la méthode de sérialisation Jackson par défaut dans Spring Boot pour résoudre la clé et la valeur. 🎜🎜Script Lua🎜🎜En fait, je l'ai mentionné dans la vidéo vhr précédente. Nous pouvons implémenter certaines opérations atomiques dans Redis à l'aide de scripts Lua. Si nous voulons appeler des scripts Lua, nous avons deux idées différentes :🎜🎜 🎜. 🎜 Définissez le script Lua sur le serveur Redis, puis calculez une valeur de hachage dans le code Java, utilisez cette valeur de hachage pour verrouiller le script Lua à exécuter. 🎜🎜🎜🎜 Définissez le script Lua directement dans le code Java puis envoyez-le au serveur Redis pour exécution. 🎜🎜🎜🎜Spring Data Redis fournit également une interface pour faire fonctionner les scripts Lua, ce qui est assez pratique, nous adopterons donc ici la deuxième option. 🎜🎜Nous créons un nouveau dossier lua dans le répertoire des ressources spécifiquement pour stocker les scripts lua. Le contenu du script est le suivant : 🎜
@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(ServiceException.class)
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);
        map.put("message", e.getMessage());
        return map;
    }
}
🎜Ce script n'est en fait pas difficile. Vous pouvez probablement savoir à quoi il sert en un coup d'œil. KEYS et ARGV sont tous deux des paramètres transmis lors de l'appel. Tonumber consiste à convertir une chaîne en un nombre redis.call consiste à exécuter des instructions redis spécifiques. Le processus spécifique est le suivant : 🎜🎜🎜🎜Obtenez d'abord la clé transmise et Current. limiter le nombre et le temps. 🎜🎜🎜🎜Obtenez la valeur correspondant à cette clé via get. Cette valeur est le nombre de fois que cette interface est accessible dans la fenêtre horaire actuelle. 🎜🎜🎜🎜Si c'est la première visite, le résultat obtenu à ce moment est nul, sinon le résultat obtenu devrait être un nombre, donc l'étape suivante est de juger, si le résultat obtenu est un nombre, et ce nombre est supérieur que le nombre, cela signifie que la limite de trafic a été dépassée, les résultats de la requête peuvent alors être renvoyés directement. 🎜🎜🎜🎜Si le résultat obtenu est nul, cela signifie qu'il s'agit du premier accès. A ce moment, augmentez la clé actuelle de 1, puis définissez un délai d'expiration. 🎜🎜🎜🎜Enfin, renvoyez simplement la valeur augmentée de 1. 🎜🎜🎜🎜En fait, ce script Lua est facile à comprendre. 🎜🎜Ensuite, nous chargeons ce script Lua dans un Bean, comme suit : 🎜rrreee🎜OK, notre script Lua est maintenant prêt. 🎜🎜Analyse des annotations🎜🎜Ensuite, nous devons personnaliser l'aspect pour analyser cette annotation. Jetons un coup d'œil à la définition de l'aspect : 🎜rrreee🎜Cet aspect intercepte toutes les annotations avec la méthode @RateLimiter à traiter. annotations en pré-notification. 🎜
  • 首先获取到注解中的 key、time 以及 count 三个参数。

  • 获取一个组合的 key,所谓的组合的 key,就是在注解的 key 属性基础上,再加上方法的完整路径,如果是 IP 模式的话,就再加上 IP 地址。以 IP 模式为例,最终生成的 key 类似这样:rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello(如果不是 IP 模式,那么生成的 key 中就不包含 IP 地址)。

  • 将生成的 key 放到集合中。

  • 通过 redisTemplate.execute 方法取执行一个 Lua 脚本,第一个参数是脚本所封装的对象,第二个参数是 key,对应了脚本中的 KEYS,后面是可变长度的参数,对应了脚本中的 ARGV。

  • 判断 Lua 脚本执行后的结果是否超过 count,若超过则视为过载,抛出异常处理即可。

接口测试

接下来我们就进行接口的一个简单测试,如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {
        return "hello>>>"+new Date();
    }
}

每一个 IP 地址,在 5 秒内只能访问 3 次。

这个自己手动刷新浏览器都能测试出来。

全局异常处理

由于过载的时候是抛异常出来,所以我们还需要一个全局异常处理器,如下:

@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(ServiceException.class)
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);
        map.put("message", e.getMessage());
        return map;
    }
}

我将这句话重写成如下: 这个 demo 很小,所以我没有定义实体类,而是直接使用 Map 来返回 JSON。 最后我们看看过载时的测试效果:

Comment utiliser SpringBoot + Redis pour implémenter la limitation de courant dinterface

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer