forked from xkcoding/spring-boot-demo
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
229 additions
and
0 deletions.
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
...-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/** | ||
* <p> | ||
* 限流切面 | ||
* </p> | ||
* | ||
* @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<Long> limitRedisScript; | ||
|
||
@Pointcut("@annotation(com.xkcoding.ratelimit.redis.annotation.RateLimiter)") | ||
public void rateLimit() { | ||
|
||
} | ||
|
||
@Around("rateLimit()") | ||
public Object pointcut(ProceedingJoinPoint point) throws Throwable { | ||
MethodSignature signature = (MethodSignature) point.getSignature(); | ||
Method method = signature.getMethod(); | ||
// 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解 | ||
RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class); | ||
if (rateLimiter != null) { | ||
String key = rateLimiter.key(); | ||
// 默认用方法名做限流的 key 前缀 | ||
if (StrUtil.isBlank(key)) { | ||
key = method.getName(); | ||
} | ||
// 最终限流的 key 为 前缀 + IP地址 | ||
// TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理 | ||
key = key + SEPARATOR + IpUtil.getIpAddr(); | ||
|
||
long max = rateLimiter.max(); | ||
long timeout = rateLimiter.timeout(); | ||
TimeUnit timeUnit = rateLimiter.timeUnit(); | ||
boolean limited = shouldLimited(key, max, timeout, timeUnit); | ||
if (limited) { | ||
throw new RuntimeException("手速太快了,慢点儿吧~"); | ||
} | ||
} | ||
|
||
return point.proceed(); | ||
} | ||
|
||
private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) { | ||
// 最终的 key 格式为: | ||
// limit:自定义key:IP | ||
// limit:方法名:IP | ||
key = REDIS_LIMIT_KEY_PREFIX + key; | ||
// 统一使用单位毫秒 | ||
long ttl = timeUnit.toMillis(timeout); | ||
// 当前时间毫秒数 | ||
long now = Instant.now().toEpochMilli(); | ||
long expired = now - ttl; | ||
// 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String | ||
Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + ""); | ||
if (executeTimes != null) { | ||
if (executeTimes == 0) { | ||
log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max); | ||
return true; | ||
} else { | ||
log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes); | ||
return false; | ||
} | ||
} | ||
return false; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...t-demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/config/RedisConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.xkcoding.ratelimit.redis.config; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.core.io.ClassPathResource; | ||
import org.springframework.data.redis.core.script.DefaultRedisScript; | ||
import org.springframework.data.redis.core.script.RedisScript; | ||
import org.springframework.scripting.support.ResourceScriptSource; | ||
|
||
/** | ||
* <p> | ||
* Redis 配置 | ||
* </p> | ||
* | ||
* @author yangkai.shen | ||
* @date Created in 2019/9/30 11:37 | ||
*/ | ||
@Configuration | ||
public class RedisConfig { | ||
@Bean | ||
@SuppressWarnings("unchecked") | ||
public RedisScript<Long> limitRedisScript() { | ||
DefaultRedisScript redisScript = new DefaultRedisScript<>(); | ||
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/redis/limit.lua"))); | ||
redisScript.setResultType(Long.class); | ||
return redisScript; | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
spring-boot-demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/util/IpUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package com.xkcoding.ratelimit.redis.util; | ||
|
||
import cn.hutool.core.util.StrUtil; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.web.context.request.RequestContextHolder; | ||
import org.springframework.web.context.request.ServletRequestAttributes; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
|
||
/** | ||
* <p> | ||
* IP 工具类 | ||
* </p> | ||
* | ||
* @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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
spring-boot-demo-ratelimit-redis/src/main/resources/scripts/redis/limit.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |