01.背景
最近项目中碰到一些因为网络延迟及系统响应速度变慢造成的请求重复提交问题,印象中之前有看到过类似的注解,但是忘了是哪个注解,也没搜到,所以自己写一个。
02.编写代码
首先自定义一个注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
int value() default 1000;
}
表明该注解只添加在方法上,该注解会在程序运行时保留。
默认值为1000毫秒
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Component
@Aspect
public class RedisRateLimiterAspect {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
@Pointcut("@annotation(com.xxx.annotation.RateLimiter)")
public void rateLimiterPointCut() {
}
@Around("rateLimiterPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
int millis = rateLimiter.value();
String key = method.getDeclaringClass().getName() + "#" + method.getName();
Long expire = redisTemplate.opsForValue().getOperations().getExpire(key);
if (expire != null && expire > 0) {
throw new RuntimeException("请求过于频繁,请稍后再试");
}
redisTemplate.opsForValue().set(key, System.currentTimeMillis(), millis, TimeUnit.MILLISECONDS);
return joinPoint.proceed();
}
}
使用 redisTemplate.opsForValue().getOperations().getExpire(key)
获取 Key 的过期时间。如果这个值大于 0,则说明 Key 还在有效期内,我们就会抛出一个运行时异常提示用户稍后再试。
使用 redisTemplate.opsForValue().set(key, System.currentTimeMillis(), millis, TimeUnit.MILLISECONDS)
来存储 Key 并设置其过期时间为 millis
毫秒。这意味着这个 Key 将在 millis
毫秒之后自动过期并被删除。这样,我们就可以通过判断这个 Key 是否过期来判断这个方法是否可以被调用。
在这个方法里,需要用到redis来缓存Key,所以需要提前配置好redis的配置。
如果不使用redis的话可以改为以下代码
@Component
@Aspect
public class RateLimiterAspect {
private final ConcurrentHashMap<String, Long> rateLimiterMap = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.myp.annotation.RateLimiter)")
public void rateLimiterPointCut() {
}
@Around("rateLimiterPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
int millis = rateLimiter.value();
String key = method.getDeclaringClass().getName() + "#" + method.getName();
long currentTimeMillis = System.currentTimeMillis();
Long lastTimeMillis = rateLimiterMap.putIfAbsent(key, currentTimeMillis);
if (lastTimeMillis != null && (currentTimeMillis - lastTimeMillis) < millis) {
throw new RuntimeException("请求过于频繁,请稍后再试");
}
rateLimiterMap.put(key, currentTimeMillis);
return joinPoint.proceed();
}
}
不过,请注意,这种实现方式只适用于单个实例的情况。如果你的应用有多个实例,你需要使用分布式锁(如 Redis)来确保在所有实例之间正确同步限流规则。
另外,这种实现方式对于高并发场景可能会有性能问题,所以如果条件允许,建议使用上面的redis方法。
03.测试效果
写一个测试接口,来测试一下这个注解
@RestController
public class TestController {
@RequestMapping("/test")
@RateLimiter(value = 5000)
public String test(String name) {
return "访问成功"+name;
}
}
第一次访问没问题
五秒内第二次访问则会抛出请求频繁异常
通过redis可视化工具可以看到有存入调用的方法名
使用注意事项:
RedisRateLimiterAspect 类用于限制 API 的访问频率。
该类使用 AOP 切面来实现限流,并使用 Redis 来存储访问记录和计算访问频率。
使用示例:
在以下示例中,我们使用 RedisRateLimiterAspect 类来限制 someMethod 方法的访问频率,
默认1000毫秒内最多只允许调用 1 次该方法,您可以填写value值,单位为毫秒:
@Service
public class SomeService {
@RateLimiter(value = 5000) //代表5000毫秒内只能调用一次该方法
public void someMethod() {
// ...
}
}
在上述示例中,我们在 someMethod 方法上使用@RateLimiter注解并填写值为5000毫秒来实现限流。
注意事项:
在使用 RedisRateLimiterAspect 类时,请注意以下事项:
- 请确保您的应用中已经正确配置了 Redis 连接信息。
- 请注意不要在同一时间点对同一方法进行并发调用,否则可能会出现数据不一致的情况。
- 如果您需要限制不同方法的调用频率,请使用不同的 key 值进行区分。