diff --git a/spring-boot-demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java b/spring-boot-demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java new file mode 100644 index 000000000..e372b3596 --- /dev/null +++ b/spring-boot-demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java @@ -0,0 +1,98 @@ +package com.xkcoding.ratelimit.redis.aspect; + +import cn.hutool.core.util.StrUtil; +import com.xkcoding.ratelimit.redis.annotation.RateLimiter; +import com.xkcoding.ratelimit.redis.util.IpUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.time.Instant; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + *
+ * 限流切面 + *
+ * + * @author yangkai.shen + * @date Created in 2019/9/30 10:30 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class RateLimiterAspect { + private final static String SEPARATOR = ":"; + private final static String REDIS_LIMIT_KEY_PREFIX = "limit:"; + private final StringRedisTemplate stringRedisTemplate; + private final RedisScript+ * Redis 配置 + *
+ * + * @author yangkai.shen + * @date Created in 2019/9/30 11:37 + */ +@Configuration +public class RedisConfig { + @Bean + @SuppressWarnings("unchecked") + public RedisScript+ * IP 工具类 + *
+ * + * @author yangkai.shen + * @date Created in 2019/9/30 10:38 + */ +@Slf4j +public class IpUtil { + private final static String UNKNOWN = "unknown"; + private final static int MAX_LENGTH = 15; + + /** + * 获取IP地址 + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + */ + public static String getIpAddr() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + String ip = null; + try { + ip = request.getHeader("x-forwarded-for"); + if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + } catch (Exception e) { + log.error("IPUtils ERROR ", e); + } + // 使用代理,则获取第一个IP地址 + if (!StrUtil.isEmpty(ip) && ip.length() > MAX_LENGTH) { + if (ip.indexOf(StrUtil.COMMA) > 0) { + ip = ip.substring(0, ip.indexOf(StrUtil.COMMA)); + } + } + return ip; + } +} diff --git a/spring-boot-demo-ratelimit-redis/src/main/resources/application.yml b/spring-boot-demo-ratelimit-redis/src/main/resources/application.yml index af5002ce9..43382fcd2 100644 --- a/spring-boot-demo-ratelimit-redis/src/main/resources/application.yml +++ b/spring-boot-demo-ratelimit-redis/src/main/resources/application.yml @@ -2,3 +2,20 @@ server: port: 8080 servlet: context-path: /demo +spring: + redis: + host: localhost + # 连接超时时间(记得添加单位,Duration) + timeout: 10000ms + # Redis默认情况下有16个分片,这里配置具体使用的分片 + # database: 0 + lettuce: + pool: + # 连接池最大连接数(使用负值表示没有限制) 默认 8 + max-active: 8 + # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 + max-wait: -1ms + # 连接池中的最大空闲连接 默认 8 + max-idle: 8 + # 连接池中的最小空闲连接 默认 0 + min-idle: 0 diff --git a/spring-boot-demo-ratelimit-redis/src/main/resources/scripts/redis/limit.lua b/spring-boot-demo-ratelimit-redis/src/main/resources/scripts/redis/limit.lua new file mode 100644 index 000000000..4658052c1 --- /dev/null +++ b/spring-boot-demo-ratelimit-redis/src/main/resources/scripts/redis/limit.lua @@ -0,0 +1,27 @@ +-- 下标从 1 开始 +local key = KEYS[1] +local now = tonumber(ARGV[1]) +local ttl = tonumber(ARGV[2]) +local expired = tonumber(ARGV[3]) +-- 最大访问量 +local max = tonumber(ARGV[4]) + +-- 清除过期的数据 +-- 移除指定分数区间内的所有元素,expired 即已经过期的 score +-- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired +redis.call('zremrangebyscore', key, 0, expired) +-- 获取 zset 中的元素个数 +local current = tonumber(redis.call('zcard', key)) + +-- +local next = current + 1 +if next > max then + -- 达到限流大小 返回 0 + return 0; +else + -- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score] + redis.call("zadd", key, now, now) + -- 每次访问均重新设置 zset 的过期时间,单位毫秒 + redis.call("pexpire", key, ttl) + return next +end