目录
“ 什么是限流?为什么要做限流?各个场景下限流功能是如何实现的?”
单机限流
限流:
在高并发环境下,往往一个服务会有多个业务方调用,不同的业务方请求频次不一样,业务重要程度也不一样,所以在设计分布式服务架构中,需要增加限流模块来保证核心主要服务的稳定性,本文主要介绍三种常用的限流方案,适合不同的应用场景。
场景描述:
对于大部分单实例或者几个实例的应用,服务可以采用单机限流的方式,在后端代码中设置固定的值,针对后端指定接口限制每秒或者每小时进行请求总量的限制。
解决方案:
为了更优雅的使用单机限流:采用【自定义注解+AOP+RateLimiter】组合的方式
自定义注解可便捷配置指定接口为限流接口
AOP可以方便实现接口限流统一业务逻辑处理
引入Guava提供的RateLimiter工具类,可以快速实现单机限流。该类主要基于“令牌桶”算法,控制指定时间内可以有多少令牌,获取到令牌的请求则可执行后续逻辑,否则就请求异常。
优点:
(1)开发便捷,使用服务机器内存,并发效率好。
缺点:
(1)由于初始化值在程序配置文件中定义,每次修改需要重启服务。
(2)不支持分布式环境下的接口总量控制。
实现过程:
(1)创建自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiterAnnotation {
/**
*操作具体接口名称
*/
String function() default "other";
}
(2)创建AOP切面类:定义切面类实现具体的限流逻辑
@Aspect
@Component
public class RateLimiterAop {
/**
* 限制请求速度公共类
*/
private Map<String,RateLimiter> appIdRateLimiterMap = new HashMap<String,RateLimiter>(20);
RateLimiterAop(){
//初始化限制请求权限
appIdRateLimiterMap.put("test",RateLimiter.create(1));
appIdRateLimiterMap.put("zxx",RateLimiter.create(10));
}
@Pointcut("@annotation(com.chow.kayadmin.core.ratelimiter.aop.single.RateLimiterAnnotation)")
public void pointCutMethod() {
}
@Around(value = "pointCutMethod() && @annotation(annotation)")
public Object collector(ProceedingJoinPoint joinPoint, RateLimiterAnnotation annotation) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
RateLimiterAnnotation rateLimiterAnnotation = methodSignature.getMethod().
getAnnotation(RateLimiterAnnotation.class);
if (rateLimiterAnnotation != null) {
//获取注册上方传入的接口名称
String src = "other";
JSONObject requestJson = getParams(joinPoint);
if (!ObjectUtils.isEmpty(requestJson)) {
if (!StringUtils.isEmpty(requestJson.getString("src"))) {
src = requestJson.getString("src");
}
}
RateLimiter rateLimiter = appIdRateLimiterMap.get(src);
if(!ObjectUtils.isEmpty(rateLimiter)&&!rateLimiter.tryAcquire()){
//获取request
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String msg = "请求来源:"+src+",请求方IP:"+request.getRemoteAddr()
+",请求接口:"+request.getRequestURI()+",速率超过规定值(QPS/S):"+rateLimiter.getRate();
throw new RateLimiterException(msg);
}
//如果传入的src不在定义的值里面,可以在这里处理,demo默认为通过
}
return joinPoint.proceed();
}
/**
* 获取方法参数值并组装为JSONObject
* @param joinPoint
* @return
*/
public static JSONObject getParams(JoinPoint joinPoint) {
//获取参数值
Object[] args = joinPoint.getArgs();
if (ObjectUtils.isEmpty(args)) {
return null;
}
JSONObject params = new JSONObject();
//对象接收参数
try {
String data = JSON.toJSONString(joinPoint.getArgs()[0]);
params = JSON.parseObject(data);
}
//普通参数传入
catch (JSONException e){
//获取参数名
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
for(int i=0;i<methodSignature.getParameterNames().length;i++){
params.put(methodSignature.getParameterNames()[i],args[i]);
}
}
return params;
}
}
(3)在需要限流的接口上增加限流注解
@RateLimiterAnnotation
@PostMapping("/get/ids")
public Result genrateId(@RequestBody GenerateIdVO generateIdVO){
if(ObjectUtils.isEmpty(generateIdVO.getNums())){
throw new ParamsException("params nums is empty");
}
if(ObjectUtils.isEmpty(generateIdVO.getServiceId())){
throw new ParamsException("params serviceId is empty");
}
if(generateIdVO.getNums() > MAX_NUMS || generateIdVO.getNums() <= 0){
throw new ParamsException("nums cannot be greater than "+ MAX_NUMS+" or less than or equal 0");
}
if(generateIdVO.getServiceId() > GenerateIdUtils.MAX_SERVICE_ID
||generateIdVO.getServiceId() <0){
throw new ParamsException("serviceId cannot be greater than "+GenerateIdUtils.MAX_SERVICE_ID+" or less than 0");
}
Set<Long> uidList = new HashSet<>();
GenerateIdUtils generateId = new GenerateIdUtils(WORK_ID, generateIdVO.getServiceId());
for (int i = 0; i < generateIdVO.getNums(); i++) {
uidList.add(generateId.nextId());
}
return ResultUtils.success(uidList);
}
动态单机限流
场景描述:
动态单机限流,作为单机限流的升级版,可以通过服务端配置文件(启动预加载,服务端主动推送两种同步配置方式),动态更新服务限流的配置信息,适用于限流阈值需要动态调整的场景。
解决方案:
【Redis+自定义注解+AOP+Ratelimiter】
与单机限流的区别就是每个接口的限流参数,通过服务端配置写到redis环境中,限流逻辑首先读取redis缓存中的接口限流参数,如果没有取到值,则取程序默认值。
优点:
1、开发便捷,使用服务机器内存,可动态配置限流参数,并发效率好。
缺点:
1、相对于单机限流,增加了一层redis服务会增加少量网络开销。
2、不支持分布式环境下的接口总量控制。
实现过程:
具体操作流程同单机模式,唯一的区别就是读取每个src来源的限流值,通过读取redis获取,如果没有获取到,则使用自定义的默认值进行处理。
1、创建限流参数-redis操作类
2、创建自定义限流注解
3、创建AOP切面类:定义切面类实现具体的限流逻辑
4、在需要限流的接口上增加限流注解
分布式限流
场景描述:
对于分布式服务环境下往往一个服务可能有几十个或者上百个实例,上述两个方式就无法对每个服务的接口进行精准的控制,所以采用分布式环境下的限流方案。
解决方案:
【自定义注解+AOP+Redis+Lua】
根据每个业务请求方的接口生成唯一身份key,并通过redis+lua设置对应key对应的值,如果该值超过redis中配置的限流阈值,则返回异常。
优点:
1、可以控制分布式环境下的接口请求总量。
2、可动态配置限流参数。
缺点:
1、增加redis的资源使用费用以及增加少量网络开销
实现过程:
该demo为非动态读取参数版本,如果需要动态读取参数,直接增加一个redis操作类,帮对应src来源的值种到redis,然后在aop类,读取redis中指定key的值进行控制即可。感兴趣的小伙伴可以自行研究一下。
1、创建限流参数-redis操作类
2、创建redis+lua脚本操作工具类
@Configuration
@EnableCaching
public class RateLimiterCommon {
@Bean
public DefaultRedisScript<Long> redisLuaScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}
3、创建自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiterRedisAnnotation {
/**
*操作具体接口名称
*/
String function() default "other";
}
4、创建AOP切面类:使用redis+lua工具类进行限流控制
@Aspect
@Component
public class RateLimiterRedis {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DefaultRedisScript<Long> redisLuaScript;
/**
* 限制请求速度公共类
*/
private Map<String,Long> appIdRateLimiterMap = new HashMap<String,Long>(20);
/**
* 默认请求次数
*/
private static final long DEFAULT_CONUTS =100L;
/**
* 默认请求时间 1秒
*/
private static final long DEFAULT_LIMITER_TIME =1L;
/**
* 默认redis key前缀
*/
private static final String DEFAULT_LIMITER_KEYS ="service:rateLimiter:";
RateLimiterRedis(){
//初始化限制请求权限
appIdRateLimiterMap.put("test",1L);
appIdRateLimiterMap.put("zxx",10L);
}
@Pointcut("@annotation(com.chow.kayadmin.core.ratelimiter.aop.redis.RateLimiterRedisAnnotation)")
public void pointCutMethod() {
}
@Around(value = "pointCutMethod() && @annotation(annotation)")
public Object collector(ProceedingJoinPoint joinPoint, RateLimiterRedisAnnotation annotation) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
RateLimiterRedisAnnotation rateLimiterRedisAnnotation = methodSignature.getMethod().
getAnnotation(RateLimiterRedisAnnotation.class);
if (rateLimiterRedisAnnotation != null) {
//获取请求参数中的src
String src = "other";
JSONObject requestJson = getParams(joinPoint);
if (!ObjectUtils.isEmpty(requestJson)) {
if (!StringUtils.isEmpty(requestJson.getString("src"))) {
src = requestJson.getString("src");
}
}
List<String> keys = Collections.singletonList(DEFAULT_LIMITER_KEYS+src);
//如果使用动态参数,这里首先从redis取值,取不到则取系统的默认值
Long counts = appIdRateLimiterMap.get(src);
if(ObjectUtils.isEmpty(counts)||0==counts){
counts =DEFAULT_CONUTS;
}
Long number = (Long) redisTemplate.execute(redisLuaScript, keys, counts, DEFAULT_LIMITER_TIME);
if(number != null && 0==number.intValue()){
//获取request
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String msg = "请求来源:"+src+",请求方IP:"+request.getRemoteAddr()
+",请求接口:"+request.getRequestURI()+",速率超过规定值(QPS/S):"+counts;
throw new RateLimiterException(msg);
}
//如果传入的src不在定义的值里面,可以在这里处理,demo默认为通过
}
return joinPoint.proceed();
}
/**
* 获取方法参数值并组装为JSONObject
* @param joinPoint
* @return
*/
public static JSONObject getParams(JoinPoint joinPoint) {
//获取参数值
Object[] args = joinPoint.getArgs();
if (ObjectUtils.isEmpty(args)) {
return null;
}
JSONObject params = new JSONObject();
//对象接收参数
try {
String data = JSON.toJSONString(joinPoint.getArgs()[0]);
params = JSON.parseObject(data);
}
//普通参数传入
catch (JSONException e){
//获取参数名
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
for(int i=0;i<methodSignature.getParameterNames().length;i++){
params.put(methodSignature.getParameterNames()[i],args[i]);
}
}
return params;
}
}
5、在需要限流的接口上增加限流注解
@RateLimiterRedisAnnotation
@PostMapping("/check/ids")
public Result checkIds(@RequestBody GenerateIdVO generateIdVO){
if(ObjectUtils.isEmpty(generateIdVO.getIds())){
throw new ParamsException("params nums is empty");
}
if(ObjectUtils.isEmpty(generateIdVO.getIds().size())){
throw new ParamsException("nums cannot be greater than "+ MAX_NUMS);
}
List<GenerateId> generateIdList = new ArrayList<>(16);
int idSize = generateIdVO.getIds().size();
for (int i = 0; i < idSize; i++) {
GenerateId generateId = new GenerateId();
generateId.setId(generateIdVO.getIds().get(i));
generateId.setServiceId(GenerateIdUtils.getServiceId(generateIdVO.getIds().get(i)));
generateId.setWordId(GenerateIdUtils.getWorkerId(generateIdVO.getIds().get(i)));
generateId.setSequence(GenerateIdUtils.getSequence(generateIdVO.getIds().get(i)));
generateId.setTime(GenerateIdUtils.getTime(generateIdVO.getIds().get(i)));
generateIdList.add(generateId);
}
return ResultUtils.success(generateIdList);
}