下面通过Aop自定义注解、Redis + Lua 脚本的方式实现限流,步骤会比较详细
:::info
引入依赖包
:::
pom文件中添加如下依赖包,比较关键的就是spring-boot-starter-data-redis和 spring-boot-starter-aop
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>21.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies>
:::info
配置信息application.properties
:::
在application.properties文件中配置提前搭建好的redis服务地址和端口。
spring.redis.host=127.0.0.1spring.redis.port=6379
:::info 自定义RedisTemplate :::
@Configurationpublic class RedisLimiterHelper {@Beanpublic RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {RedisTemplate<String, Serializable> template = new RedisTemplate<>();template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new GenericJackson2JsonRedisSerializer());template.setConnectionFactory(redisConnectionFactory);return template;}}
:::info 限流类型枚举 ::: 枚举类字段能够避免在业务处不小心打错字符串,代码更加的规范整洁
/*** @author liuyanntes* @description 限流类型* @date 2020/4/8 13:47*/public enum LimitType {/*** 自定义key*/CUSTOMER,/*** 请求者IP*/IP;}
:::info
自定义AOP注解
:::
我们自定义个@Limit注解
注解类型为ElementType.METHOD即作用于方法上
/*** @author liuyanntes* @description 自定义限流注解* @date 2020/4/8 13:15*/@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface Limit {/*** 名字*/String name() default "";/*** key*/String key() default "";/*** Key的前缀*/String prefix() default "";/*** 请求限制时间段 单位(秒)*/int period();/*** 在period()内最多访问次数*/int count();/*** 限流的类型,可以根据请求的IP、自定义key*/LimitType limitType()}
:::info AOP切面逻辑 :::
/*** @author liuyanntes* @description 限流切面实现* @date 2020/4/8 13:04*/@Aspect@Configurationpublic class LimitInterceptor {private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);private static final String UNKNOWN = "unknown";private final RedisTemplate<String, Serializable> limitRedisTemplate;@Autowiredpublic LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {this.limitRedisTemplate = limitRedisTemplate;}/*** @param pjp* @author liuyanntes* @description 切面* @date 2020/4/8 13:04*/@Around("execution(public * *(..)) && @annotation(com.xiaofu.limit.api.Limit)")public Object interceptor(ProceedingJoinPoint pjp) {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();Limit limitAnnotation = method.getAnnotation(Limit.class);LimitType limitType = limitAnnotation.limitType();String name = limitAnnotation.name();String key;int limitPeriod = limitAnnotation.period();int limitCount = limitAnnotation.count();/*** 根据限流类型获取不同的key ,如果不传我们会以方法名作为key*/switch (limitType) {case LimitType.IP:key = getIpAddress();break;case LimitType.CUSTOMER:key = limitAnnotation.key();break;default:key = StringUtils.upperCase(method.getName());}ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));try {String luaScript = buildLuaScript();RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);logger.info("Access try count is {} for name={} and key = {}", count, name, key);if (count != null && count.intValue() <= limitCount) {return pjp.proceed();} else {throw new RuntimeException("You have been dragged into the blacklist");}} catch (Throwable e) {if (e instanceof RuntimeException) {throw new RuntimeException(e.getLocalizedMessage());}throw new RuntimeException("server exception");}}/*** @author liuyanntes* @description 编写 redis Lua 限流脚本* @date 2020/4/8 13:24*/public String buildLuaScript() {StringBuilder lua = new StringBuilder();lua.append("local c");lua.append("\nc = redis.call('get',KEYS[1])");// 调用不超过最大值,则直接返回lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");lua.append("\nreturn c;");lua.append("\nend");// 执行计算器自加lua.append("\nc = redis.call('incr',KEYS[1])");lua.append("\nif tonumber(c) == 1 then");// 从第一次调用开始限流,设置对应键值的过期lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");lua.append("\nend");lua.append("\nreturn c;");return lua.toString();}/*** @author liuyanntes* @description 获取id地址* @date 2020/4/8 13:24*/public String getIpAddress() {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}}
:::info
控制层实现
:::
我们将@Limit注解作用在需要进行限流的接口方法上,在10秒内只允许放行3个请求,这里为直观一点用AtomicInteger计数。
/*** @Author: liuyanntes* @Description:*/@RestControllerpublic class LimiterController {private static final AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();private static final AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();private static final AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();/*** @author liuyanntes* @description* @date 2020/4/8 13:42*/@Limit(key = "limitTest", period = 10, count = 3)@GetMapping("/limitTest1")public int testLimiter1() {return ATOMIC_INTEGER_1.incrementAndGet();}/*** @author liuyanntes* @description* @date 2020/4/8 13:42*/@Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)@GetMapping("/limitTest2")public int testLimiter2() {return ATOMIC_INTEGER_2.incrementAndGet();}/*** @author liuyanntes* @description* @date 2020/4/8 13:42*/@Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)@GetMapping("/limitTest3")public int testLimiter3() {return ATOMIC_INTEGER_3.incrementAndGet();}}
:::info
限流测试
:::
测试「预期」:10秒内的连续3次请求均可以成功,第4次请求被拒绝。接下来看一下是不是我们预期的效果
请求地址:http://127.0.0.1:8080/limitTest1
可以看到第四次请求时,应用直接拒绝了请求,说明我们基于Aop自定义注解、Redis + Lua的限流方案搭建成功。
