Comment utiliser SpringBoot + Redis pour implémenter la limitation de courant d'interface
PHPz
Libérer: 2023-05-27 15:01:19
avant
1567 Les gens l'ont consulté
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 :
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 :
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@RateLimitersur cette interface, puis configurez les paramètres pertinents.@RateLimiter注解,然后配置相关参数即可。
定制 RedisTemplate
在 Spring Boot 中,我们其实更习惯使用 Spring Data Redis 来操作 Redis,不过默认的 RedisTemplate 有一个小坑,就是序列化用的是 JdkSerializationRedisSerializer,不知道小伙伴们有没有注意过,直接用这个序列化工具将来存到 Redis 上的 key 和 value 都会莫名其妙多一些前缀,这就导致你用命令读取的时候可能会出错。
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('get', key) if current and tonumber(current) > count then return tonumber(current) end current = redis.call('incr', key) if tonumber(current) == 1 then redis.call('expire', key, time) end return tonumber(current)
@Bean public DefaultRedisScript limitScript() { DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua"))); redisScript.setResultType(Long.class); return redisScript; }
Copier après la connexion
可以啦,我们的 Lua 脚本现在就准备好了。
注解解析
接下来我们就需要自定义切面,来解析这个注解了,我们来看看切面的定义:
@Aspect @Component public class RateLimiterAspect { private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired private RedisTemplate redisTemplate; @Autowired private RedisScript 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 keys = Collections.singletonList(combineKey); try { Long number = redisTemplate.execute(limitScript, keys, count, time); if (number==null || number.intValue() > count) { throw new ServiceException("访问过于频繁,请稍候再试"); } log.info("限制请求'{}',当前请求'{}',缓存key'{}'", 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(); } }
Copier après la connexion
这个切面就是拦截所有加了@RateLimiter
Customized RedisTemplateDans 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 namene 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(); } }
Copier après la connexion
Copier après la connexion
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 LuaEn 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 serviceException(ServiceException e) { HashMap map = new HashMap<>(); map.put("status", 500); map.put("message", e.getMessage()); return map; } }
Copier après la connexion
Copier après la connexion
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 : rrreeeOK, notre script Lua est maintenant prêt. Analyse des annotationsEnsuite, nous devons personnaliser l'aspect pour analyser cette annotation. Jetons un coup d'œil à la définition de l'aspect : rrreeeCet 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 地址)。
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn