之前接的一个特殊需求,无法使用nginx来限流,也没办法接公司现有的限流服务,只能在业务里来做限流,对比多款限流器,最终选择了redisson
下面是代码
一、@ReqRateLimiter
自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReqRateLimiter {
/**
* 限流的接口
* @return
*/
String inter();
/**
* 限流速率,默认每秒100
* @return
*/
long rate() default 100;
/**
* 限流周期
* @return
*/
long rateInterval() default 1;
/**
* 限流速率单位
* @return
*/
RateIntervalUnit timeUnit() default RateIntervalUnit.SECONDS;
}
二、ReqRateLimiterAspect
切面
@Slf4j
@Aspect
@Component
public class ReqRateLimitAspect {
@Autowired
private Redisson redisson;
/**
* 根据自定义注解获取切点
*/
@Pointcut("@annotation(reqRateLimiter)")
public void accessLimit(ReqRateLimiter reqRateLimiter) {
}
@Around(value = "accessLimit(reqRateLimiter)", argNames = "pjp,reqRateLimiter")
public Object around(ProceedingJoinPoint pjp, ReqRateLimiter reqRateLimiter) throws Throwable {
boolean result = true;
try {
String imei = "";
Object[] args = pjp.getArgs();
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
String[] parameterNames = pnd.getParameterNames(method);
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
if ("imei".equals(parameterNames[i])) {
imei = String.valueOf(args[i]);
}
}
}
// 限流拦截器
result = getRateLimiter(reqRateLimiter, imei);
} catch (Exception e) {
log.error("限流拦截器异常", e);
}
if (result) {
return pjp.proceed();
} else {
throw new BaseException(RespCode.SYSTEM_BUSY);
}
}
/**
* 获取限流拦截器
*
* @param reqRateLimiter
* @return
*/
private Boolean getRateLimiter(ReqRateLimiter reqRateLimiter, String imei) {
String inter = reqRateLimiter.inter();
RRateLimiter imeiRateLimiter = redisson.getRateLimiter(inter + SysConstant.COLON + appId + SysConstant.COLON + imei);
RRateLimiter interRateLimiter = redisson.getRateLimiter(inter);
if (!interRateLimiter.isExists()) {
interRateLimiter.trySetRate(RateType.PER_CLIENT, reqRateLimiter.rate(), reqRateLimiter.rateInterval(), reqRateLimiter.timeUnit());
interRateLimiter = redisson.getRateLimiter(inter);
}
return interRateLimiter.tryAcquire(1);
}
}
三、初始化限流策略
@Slf4j
@Service
public class ReqLimitServiceImpl implements ReqLimitService {
@Autowired
private Redisson redisson;
@Autowired
private OpenReqLimitService openReqLimitService;
@Override
public void initRule(String appId, String imei) {
log.info("初始化限流规则,入参 appId={},imei={}", appId, imei);
List<OpenReqLimit> limits = openReqLimitService.getLimits(appId, imei);
for (OpenReqLimit limit : limits) {
log.info("限流配置信息 appId={},imei={},rate={},interval={},del={}", limit.getAppId(),
limit.getImei(), limit.getRate(), limit.getRateInterval(), DelFlag.getByType(limit.getDelFlag()).getDesc());
RRateLimiter rRateLimiter = redisson.getRateLimiter(limit.getInter() + SysConstant.COLON + limit.getAppId() + SysConstant.COLON + limit.getImei());
if (rRateLimiter.isExists()) {
//删除的配置,删除对应的限流器
if (1 == limit.getDelFlag()) {
rRateLimiter.delete();
log.info("删除限流规则完成 appId={},imei={}", limit.getAppId(), limit.getImei());
} else {
RateLimiterConfig rateLimiterConfig = rRateLimiter.getConfig();
// 判断配置是否更新,如果更新,重新加载限流器配置
if (!Objects.equals(limit.getRate(), rateLimiterConfig.getRate())
|| !Objects.equals(RateIntervalUnit.SECONDS.toMillis(limit.getRateInterval()), rateLimiterConfig.getRateInterval())) {
rRateLimiter.delete();
rRateLimiter.trySetRate(RateType.PER_CLIENT, limit.getRate(), limit.getRateInterval(), RateIntervalUnit.SECONDS);
log.info("更新限流规则完成 appId={},imei={},rate={},interval={}", limit.getAppId(),
limit.getImei(), limit.getRate(), limit.getRateInterval());
}
}
} else if (0 == limit.getDelFlag()) {
rRateLimiter.trySetRate(RateType.PER_CLIENT, limit.getRate(), limit.getRateInterval(),RateIntervalUnit.SECONDS);
log.info("创建限流规则完成 appId={},imei={},rate={},interval={}", limit.getAppId(),
limit.getImei(), limit.getRate(), limit.getRateInterval());
}
}
}
}
四、使用
@PostMapping("/location/network")
@ReqRateLimiter(inter = "/openapi/location/network")
public void network(@RequestBody @Valid LocationNetworkReq req, HttpServletResponse response) throws Exception {
super.responseMsg(response, locationService.network(req));
}
@GetMapping("/dangerousPlace/list")
@ReqRateLimiter(inter = "/openapi/dangerousPlace/list")
public void getDangerousArea(@RequestParam("imei") String imei,
@RequestParam("location") String location,
@RequestParam(value = "radius", defaultValue = "100") Integer radius,
HttpServletResponse response) throws Exception {
super.responseMsg(response, locationService.getDangerousArea(imei, location, radius));
}
五、源码分析
上面基本能实现基本的限流需求,那么redisson是怎么实现的呢?
1、trySetRate 初始化限流器
上述lua脚本,及其含义如下
redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);
redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);
return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);
脚本调用的方法参数对照表:
| 脚本参数名 | Java参数名 | 参数值 |
|---|---|---|
| KEYS[1] | getRawName() | 限流器 key |
| ARGV[1] | rate | 限流器令牌数 |
| ARGV[2] | unit.toMillis(rateInterval) | 时间间隔 |
| ARGV[3] | type.ordinal() | 限流器类型(全局) |
2、tryAcquireAsync 尝试获取令牌
脚本调用的方法参数对照表:
| 脚本参数名 | Java参数名 | 参数值 |
|---|---|---|
| KEYS[1] | getRawName() | 限流器 key |
| KEYS[2] | getValueName() | 限流器令牌数量 key |
| KEYS[3] | getClientValueName() | 限流器客户端令牌数量 key |
| KEYS[4] | getPermitsName() | 限流器令牌 key |
| KEYS[5] | getClientPermitsName() | 限流器客户端令牌 key |
| ARGV[1] | value | 请求限流器令牌数 |
| ARGV[2] | System.currentTimeMillis() | 当前时间戳 |
| ARGV[3] | random | 随机数 |
获取令牌逻辑
3、availablePermits 返回可用令牌的数量
Lua 脚本调用的方法参数对照表:
| 脚本参数名 | Java参数名 | 参数值 |
|---|---|---|
| KEYS[1] | getRawName() | 限流器 key |
| KEYS[2] | getValueName() | 限流器令牌数量 key |
| KEYS[3] | getClientValueName() | 限流器客户端令牌数量 key |
| KEYS[4] | getPermitsName() | 限流器令牌 key |
| KEYS[5] | getClientPermitsName() | 限流器客户端令牌 key |
| ARGV[1] | System.currentTimeMillis() | 当前时间戳 |
4、monitor
可以进入控制台连接redis 通过monitor命令打印redis执行指令日志