代码地址
依赖 依赖:springweb、nosql的redis、aop
1 2 3 4 5 6 7 8 9 10 11 12 <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 >
application.properties:
1 2 spring.redis.host =localhost spring.redis.port =6379
准备工作 step1:限流类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.lcdzzz.rate_limiter.enums;public enum LimitType { DEFAULT, IP }
step2:定义注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.lcdzzz.rate_limiter.annotation;import com.lcdzzz.rate_limiter.enums.LimitType;public @interface RateLimiter { String key () default "rate_limit" ; int time () default 60 ; int count () default 100 ; LimitType limitType () default LimitType.DEFAULT; }
step3:配置redis 原因 因为待会去操作限流时需要lua表达式,所以先要对redis进行简单的配置
【问】
为什么要对redis进行配置?
【答】
在springDataRedis里面提供了两个我们可以直接操作redis的类:RedisTemplate
、StringRedisTemplate
。
区别在于前者的key和value可以操作对象,而后者不可以是对象(只能是字符串)。
但是!虽然说RedisTemplate
更方便,但是也存在问题。它的序列化方案是org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
。有兴趣深入了解的可以看https://cloud.tencent.com/developer/article/1863347 这篇博客。同时,想要用其他的序列化方式,也可以看这篇。
以下简单来说
假如我加入一个 key:xxx value:good11
虽然在get时,如果依然用的是RedisTemplate
,那它自己会帮忙转回去,但是如果用redis命令行去get xxx
,那是行不通的。
办法 所以!!!要修改的它的序列化方案,用JSON的 ,java代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.lcdzzz.rate_limiter.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;@Configuration public class RedisConfig { @Bean RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate <>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer <>(Object.class); template.setKeySerializer(serializer); template.setHashKeySerializer(serializer); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); return template; } }
step4:lua脚本
简说lua脚本
到时候会传入两个参数进来
传key:这个key就是一个list集合,就是可能传多个key进来
传多个参数进来,多个参数也是数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 local key = KEYS[1 ]local time =tonumber (ARGV[1 ])local count=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)
step5:加载lua脚本 上一步写的lua脚本是需要加载的,springDataRedis中也为加载lua脚本提供了方法,只要定义一个bean就行了,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.lcdzzz.rate_limiter.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.scripting.support.ResourceScriptSource;@Configuration public class RedisConfig { @Bean RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate <>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer <>(Object.class); template.setKeySerializer(serializer); template.setHashKeySerializer(serializer); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); return template; } @Bean DefaultRedisScript<Long> limitScript () { DefaultRedisScript<Long> script = new DefaultRedisScript <>(); script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource (new ClassPathResource ("lua/limit.lua" ))); return script; } }
step6:自定义异常 @RestControllerAdvice
是Spring Framework中的注解,用于定义一个全局性的异常处理器(Global Exception Handler)。
在Spring应用中,@RestControllerAdvice
通常用于集中处理所有控制器(Controller)中抛出的异常。它的主要作用包括:
全局异常处理: 允许开发者定义一个全局的异常处理逻辑,捕获并处理整个应用中的异常,而不仅仅是在单个控制器中。这有助于提高代码的一致性和可维护性。
统一响应: 可以在全局异常处理器中定义统一的响应格式,使得应用的异常返回给客户端时具有一致的结构和格式。
异常日志记录: 可以在全局异常处理器中集中处理异常的日志记录,方便监控和排查问题。
一个简单的例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 javaCopy code@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal Server Error" ); } }
在上述例子中,@ExceptionHandler
注解用于指定处理的异常类型,这里是 Exception.class
,表示处理所有类型的异常。然后,定义了一个处理异常的方法 handleException
,在这个方法中可以实现自定义的异常处理逻辑。在实际应用中,可以根据需要添加更多的 @ExceptionHandler
方法来处理不同类型的异常。
此项目中,全局异常处理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.lcdzzz.rate_limiter.exception;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.HashMap;import java.util.Map;@RestControllerAdvice public class GlobalException { @ExceptionHandler(RateLimitException.class) public Map<String,Object> rateLimitException (RateLimitException e) { HashMap<String, Object> map = new HashMap <>(); map.put("status" ,500 ); map.put("message" ,e.getMessage()); return map; } }
自定义的异常如下:
1 2 3 4 5 6 7 8 package com.lcdzzz.rate_limiter.exception;public class RateLimitException extends Exception { public RateLimitException (String message) { super (message); } }
至此准备工作算是完成了,接下来就可以去写切面,去解析注解
定义切面 IpUtils 请看:https://lcdzzz.github.io/2022/04/26/java-chang-yong-util-gong-ju-lei/
RateLimiterAspect 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package com.lcdzzz.rate_limiter.aspectj;import com.lcdzzz.rate_limiter.annotation.RateLimiter;import com.lcdzzz.rate_limiter.enums.LimitType;import com.lcdzzz.rate_limiter.exception.RateLimitException;import com.lcdzzz.rate_limiter.utils.IpUtils;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.RedisScript;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import java.lang.reflect.Method;import java.util.Collections;@Aspect @Component public class RateLimiterAspect { private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired RedisTemplate<Object, Object> redisTemplate; @Autowired RedisScript<Long> redisScript; @Before("@annotation(rateLimiter)") public void before (JoinPoint jp, RateLimiter rateLimiter) throws RateLimitException{ int time = rateLimiter.time(); int count = rateLimiter.count(); String combineKey = getCombineKey(rateLimiter, jp); try { Long number = redisTemplate.execute(redisScript, Collections.singletonList(combineKey), time, count); if (number==null ||number.intValue()>count){ logger.info("当前接口以达到最大限流次数" ); throw new RateLimitException ("访问过于频繁,请稍后访问" ); } logger.info("一个时间窗内请求次数:{},当前请求次数:{},缓存的key为{}" ,count,number,combineKey); } catch (Exception e) { throw e; } } private String getCombineKey (RateLimiter rateLimiter, JoinPoint jp) { StringBuffer key = new StringBuffer (rateLimiter.key()); if (rateLimiter.limitType() == LimitType.IP) { key.append(IpUtils.getClientIp(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())) .append("-" ); } MethodSignature signature = (MethodSignature) jp.getSignature(); Method method = signature.getMethod(); key.append(method.getDeclaringClass().getName()) .append("-" ) .append(method.getName()); return key.toString(); } }
测试 不限制ip地址 controller层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.lcdzzz.rate_limiter.annotation.RateLimiter;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class HelloController { @GetMapping("/hello") @RateLimiter(time = 10,count=3) public String hello () { return "hello" ; } }
结果
限制ip地址 controller层 1 2 3 4 @RateLimiter(time = 10,count=3,limitType = LimitType.IP) public String hello () { return "hello" ; }
结果
对标tienchin/ruoyi
tienchin-common/src/main/java/com/lcdzzz/common/annotation/RateLimiter.java
自定义注解
tienchin-common/src/main/java/com/lcdzzz/common/annotation/RateLimiter.java
切面,解析注解
tienchin-framework/src/main/java/com/lcdzzz/framework/config/RedisConfig.java
序列化+限流脚本
不同点:ruoyi是拼接字符串,而上面的方法是用lua脚本,但殊途同归