`
hbxflihua
  • 浏览: 660091 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

基于Redis的分布式服务限流方案

阅读更多

由于API接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。 

 

限流指对应用服务接口的请求调用次数进行限制,对超过限制次数的请求则进行快速失败或丢弃。

 

限流可以应对:

1、热点业务带来的高并发请求;

2、客户端异常重试导致的并发请求;

3、恶意攻击请求;

 

限流算法多种多样,比如常见的:固定窗口计数器、滑动窗口计数器、漏桶、令牌桶等。本章通过Redis 的Lua来实现滑动窗口的计数器算法。

 

1、Redis lua脚本如下:

local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token') 
local last_time = ratelimit_info[1] 
local current_token = tonumber(ratelimit_info[2]) 
local max_token = tonumber(ARGV[1]) 
local token_rate = tonumber(ARGV[2]) 
local current_time = tonumber(ARGV[3]) 
local reverse_time = token_rate*1000/max_token 
if current_token == nil then 
  current_token = max_token 
  last_time = current_time 
else 
  local past_time = current_time-last_time 
  local reverse_token = math.floor(past_time/reverse_time)
  current_token = current_token+reverse_token 
  last_time = reverse_time*reverse_token+last_time 
  if current_token>max_token then 
    current_token = max_token 
  end 
end 

local result = '0' 
if(current_token>0) then 
  result = '1' 
  current_token = current_token-1 
end 

redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token) 
redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time))) 

return result

 

2、项目中引入spring-data-redis和commons-codec,相关配置请自行google。

 

3、RedisRateLimitScript类

package com.huatech.support.limit;

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.data.redis.core.script.RedisScript;

public class RedisRateLimitScript implements RedisScript<String> {

   private static final String SCRIPT = 
      "local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token') local last_time = ratelimit_info[1] local current_token = tonumber(ratelimit_info[2]) local max_token = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) local reverse_time = token_rate*1000/max_token if current_token == nil then current_token = max_token last_time = current_time else local past_time = current_time-last_time local reverse_token = math.floor(past_time/reverse_time) current_token = current_token+reverse_token last_time = reverse_time*reverse_token+last_time if current_token>max_token then current_token = max_token end end local result = '0' if(current_token>0) then result = '1' current_token = current_token-1 end redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token) redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time))) return result"; 

  @Override   
  public String getSha1() { 
    return DigestUtils.sha1Hex(SCRIPT); 
  } 

  @Override   
  public Class<String> getResultType() {     
	  return String.class; 
  } 

  @Override   
  public String getScriptAsString() {     
	  return SCRIPT; 
  } 
} 

 

4、添加RateLimit注解

package com.huatech.support.limit;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
	
	/**
	 * 接口标识
	 * @return
	 */
	String value() default "";
	
	/**
	 * 周期:多久为一个周期,单位s
	 * @return
	 */
	int period() default 1;
	
	/**
	 * 周期速率
	 * @return
	 */
	int rate() default 100;
	
	/**
	 * 限制类型,默认按接口限制
	 * @return
	 */
	LimitType limitType() default LimitType.GLOBAL;
	
	/**
	 * 超限后处理方式,默认拒绝访问
	 * @return
	 */
	LimitedMethod method() default LimitedMethod.ACCESS_DENIED;

}

 

基于Redis的分布式服务限流有两种落地方案:

一种是基于aop的切面实现,另一种是基于interceptor的拦截器实现,下面分别做介绍。

 

方案一:基于aspject的aop实现方案

1、添加LimitAspect类

package com.huatech.common.aop;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.WebUtils;

import com.alibaba.fastjson.JSONObject;
import com.huatech.common.constant.Constants;
import com.huatech.common.util.IpUtil;
import com.huatech.support.limit.RateLimit;
import com.huatech.support.limit.RedisRateLimitScript;


@Aspect
@Component
public class LimitAspect {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(LimitAspect.class);
	@Autowired
	private StringRedisTemplate redisTemplate;
	
	@Around("execution(* com.huatech.core.controller..*(..) ) && @annotation(com.huatech.support.limit.RateLimit)")
	public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable{
		
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		RateLimit rateLimit = method.getAnnotation(RateLimit.class);
		if(rateLimit !=	null) {
			ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
			HttpServletRequest request = requestAttributes.getRequest();
			HttpServletResponse response = requestAttributes.getResponse();
			
			Class<?> targetClass = method.getDeclaringClass();
			List<String> keyList = new ArrayList<>(1);
		    String key = rateLimit.value();
		    if(StringUtils.isBlank(key)){
		    	key = targetClass.getName() + "-" + method.getName();
		    }
		    switch (rateLimit.limitType()) {
			case IP:
				String ip = IpUtil.getRemortIP(request);
				key = ip + "-" + key;
				break;
			case USER:
				String userId = WebUtils.getSessionAttribute(request, Constants.SESSION_USER_ID).toString();
				key = userId + "-" + key;
			default:
				break;
			}
		    keyList.add(key);
		    
		    long timer = System.currentTimeMillis();
		    boolean pass = "1".equals(redisTemplate.execute(new RedisRateLimitScript(), keyList, 
		    		Integer.toString(rateLimit.rate()), Integer.toString(rateLimit.period()), 
		    		Long.toString(timer)));
		    if(pass){
		    	return joinPoint.proceed();
		    }else{				
		    	LOGGER.warn("接口key:{}, 周期:{}, 频率:{}", key, rateLimit.period(), rateLimit.rate());
		    	Map<String, Object> result = new HashMap<>();
				result.put("code", "400");
				result.put("msg", "访问超过次数限制!");
				response.setContentType("application/json");
				response.setCharacterEncoding("utf-8");
				response.getWriter().print(JSONObject.toJSON(result));
		    	return null;
		    }
		    
		}else{
			return joinPoint.proceed();
		}
	}
	
}

 

2、在spring-mvc配置文件中开启自定义注解

<aop:aspectj-autoproxy/>

 

3、开启LimitAspect类的自动扫描操作,或者在spring配置文件中配置bean

<context:component-scan base-package="com.huatech.common.aop,com.huatech.core.controller"/>  

 

 

方式二:基于interceptor的拦截器实现方案

1、添加RateLimitInterceptor类

public class RateLimitInterceptor extends HandlerInterceptorAdapter {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitInterceptor.class);
	@Autowired StringRedisTemplate redisTemplate;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

		if (handler instanceof HandlerMethod) {
			HandlerMethod method = (HandlerMethod) handler;
			final RateLimit rateLimit = method.getMethodAnnotation(RateLimit.class);
			if (rateLimit != null) {
				// 令牌名称
				List<String> keyList = new ArrayList<>(1);
			    String key = rateLimit.value();
			    if(StringUtils.isBlank(key)){
			    	key = method.getClass().getName() + "-" + method.getMethod().getName();
			    }
			    switch (rateLimit.limitType()) {
					case IP:
						String ip = IpUtil.getRemortIP(request);
						key = ip + "-" + key;
						break;
					case USER:
						String userId = WebUtils.getSessionAttribute(request, Constants.SESSION_USER_ID).toString();
						key = "uid:" + userId + "-" + key;
					default:
						break;
				}
			    keyList.add(key);
			    
			    long timer = System.currentTimeMillis();
			    boolean pass = "1".equals(redisTemplate.execute(new RedisRateLimitScript(), keyList, 
			    		Integer.toString(rateLimit.rate()), Integer.toString(rateLimit.period()), 
			    		Long.toString(timer)));
			    if(pass){
			    	return true;
			    }else{				
			    	LOGGER.warn("接口key:{}, 周期:{}, 频率:{}", key, rateLimit.period(), rateLimit.rate());
			    	Map<String, Object> result = new HashMap<>();
					result.put("code", "400");
					result.put("msg", "访问超过次数限制!");
					response.setContentType("application/json");
					response.setCharacterEncoding("utf-8");
					response.getWriter().print(JSONObject.toJSON(result));
			    	return false;
			    }
				
			}
		}

		return true;
	}
}

 

2、在spring-mvc配置文件中配置拦截器

<!-- 拦截器配置 -->
 	<mvc:interceptors>
 		<!-- 其他拦截器配置 -->
		****
		<!-- 限速拦截器配置 -->
		<mvc:interceptor>
			<mvc:mapping path="/**"/>
			<bean class="com.huatech.common.interceptor.RateLimitInterceptor"/>
		</mvc:interceptor>
	</mvc:interceptors> 

 

使用@RateLimit

  在controller类的方法头上添加RateLimit注解

 /**
     * 服务端ping地址
     * @param request
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = "/api/app/open/ping.htm")
    @RateLimit(value="ping", period=5, rate=5)
    public void ping(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	Map<String, Object> data = new HashMap<String, Object>();
    	data.put("time", System.currentTimeMillis());
    	ServletUtils.successData(response,data);
    }

 

 

 另外两个枚举类

package com.huatech.support.limit;
/**
 * 超限处理方式
 * @author lh@erongdu.com
 * @since 2019年8月28日
 * @version 1.0
 *
 */
public enum LimitedMethod {
	
	/**
	 * 拒绝访问(直接拒绝访问,不预警)
	 */
	ACCESS_DENIED,
	/**
	 * 预警短信(发送预警短信,但不拒绝访问)
	 */
	WARN_SMS,
	/**
	 * 拒绝访问并预警
	 */
	DENIED_AND_SMS
	;

}

 

package com.huatech.support.limit;
/**
 * 接口限制类型
 * @author lh@erongdu.com
 * @since 2019年8月29日
 * @version 1.0
 *
 */
public enum LimitType {
	
	/**
	 * 整个接口限制
	 */
	GLOBAL("接口"), 
	/**
	 * ip层面限制
	 */
	IP("ip"), 
	/**
	 * 用户层面限制
	 */
	USER("用户");
	
	public String value;
	private LimitType(String value) {
		this.value = value;
	}
	
	
}

 

 IpUtil工具类

package com.huatech.common.util;

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 
 * @author lh@erongdu.com
 * @since 2019年8月29日
 * @version 1.0
 *
 */
public class IpUtil {
	
	public static final Logger logger = LoggerFactory.getLogger(IpUtil.class);
    
	/**
	 * 获取请求IP
	 * @param request
	 * @return
	 */
	public static String getRemortIP(HttpServletRequest request) {
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("X-Real-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		
		 //这里主要是获取本机的ip,可有可无  
	    if ("127.0.0.1".equals(ip) || ip.endsWith("0:0:0:0:0:0:1")) {  
	        // 根据网卡取本机配置的IP  
	        InetAddress inet = null;
	        try {  
	            inet = InetAddress.getLocalHost();  
	        } catch (UnknownHostException e) {  
	            logger.error(e.getMessage(), e);
	        }
	        if(inet != null){
	        	ip = inet.getHostAddress();
	        }
	        return ip;
	    } 
		if(ip.length() > 0){
			String[] ipArray = ip.split(",");
			if (ipArray != null && ipArray.length > 1) {
				return ipArray[0];
			}
			return ip;
		}
		
		return "";
	}
}

 

分享到:
评论

相关推荐

    基于springboot , zookeeper , redis 分布式事务强一致性方案+源代码+文档说明

    FAT ,基于springboot , 使用zookeeper,redis , spring async , spring transactionManager的强一致性分布式事务解决方案 ## 框架介绍 纯编码方式,强一致性。 使用redis/zookeeper作为注册中心 ,代理事务的执行...

    Java思维导图xmind文件+导出图片

    高并发下的服务降级、限流实战 基于分布式架构下分布式锁的解决方案实战 分布式架构实现分布式定时调度 分布式架构-中间件 分布式消息通信 消息中间件在分布式架构中的应用 ActiveMQ ActiveMQ高可用集群企业...

    【spring-boot-seckill分布式秒杀系统 v1.0】从0到1构建的java秒杀系统源码+安装说明

    4、后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 优化思路 1、分流、分流、分流,重要的事情说三遍,再牛逼的机器也抵挡不住高级别的...

    基于springcloud+Netty+MQ+mysql的分布式即时聊天系统源码+数据库+项目说明.zip

    限流组件使用 Sentinel;基于 Netty 进行通信、维护长连接;RocketMQ 作为消息队列,处理聊天消息的异步入库以及解决分布式 Netty 节点问题; Zookeeper 用于分布式 id 的生成;Redis 用于记录用户在线状态以及记录 ...

    spring-boot-seckill分布式秒杀系统-其他

    4、后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 优化思路 1、分流、分流、分流,重要的事情说三遍,再牛逼的机器也抵挡不住高级别的...

    开涛高可用高并发-亿级流量核心技术

    4.3 分布式限流 75 4.3.1 Redis+Lua实现 76 4.3.2 Nginx+Lua实现 77 4.4 接入层限流 78 4.4.1 ngx_http_limit_conn_module 78 4.4.2 ngx_http_limit_req_module 80 4.4.3 lua-resty-limit-traffic 88 4.5 节流 90 ...

    2021互联网大厂Java架构师面试题突击视频教程

    上百节课详细讲解,需要的小伙伴自行百度网盘下载,链接见附件,永久有效。 课程介绍: 01_先来看一个互联网java工程师的招聘JD 02_互联网Java工程师面试突击训练课程第一季的内容说明 ...限流?熔断?降级?什么鬼!

    spring-boot-seckill分布式秒杀系统 v1.0

    4、后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 spring-boot-seckill分布式秒杀系统优化思路 1、分流、分流、分流,重要的事情说三...

    mallcloud商城-其他

    模块包括:企业级的认证系统、开发平台、应用监控、慢sql监控、统一日志、单点登录、Redis分布式高速缓存、配置中心、分布式任务调度、接口文档、代码生成等等。 mallcloud商城特点: 1、前后端分离的企业级微服务...

    mallcloud商城 v1.0

    模块包括:企业级的认证系统、开发平台、应用监控、慢sql监控、统一日志、单点登录、Redis分布式高速缓存、配置中心、分布式任务调度、接口文档、代码生成等等。mallcloud商城特点1、前后端分离的企业级微服务架构 ...

    lamp-cloud微服务脚手架

    其中扩展和借鉴国外项目的扩展基于JWT的Zuul限流插件,方面进行限流。 4、熔断机制: 因为采取了服务的分布,为了避免服务之间的调用“雪崩”,采用了Hystrix的作为熔断器,避免了服务之间的“雪崩”。 5、监控: ...

    springcloud-alibaba:springcloud阿里巴巴演示

    Spring Cloud Alibaba致力于提供微服务... springcloudsimple:基于springcloud alibaba的基础学习模块,里面主要致力基本的服务构建,服务注册,调用,限流,熔断等基础组建的演示。是一个非常好的入门学习项目。 spr

    goodsKill::ox:基于springcloud + dubbo构建的模拟秒杀项目,重置设计,集成了分库分表,elasticsearch:magnifying_glass_tilted_left:,gateway,mybatis-plus,spring-session等常用开源组件

    集成的sentinel限流组件,可以针对http请求以及dubbo rpc调用限流 集成新版支付宝easySDK,通过当面扫完成扫码付款 集成服务网关,采用Spring Cloud Gateway网关组件,并提供JWT用户鉴权功能 :gem_stone:分支介绍 ...

    spring-boot示例项目

    cloud-alibaba|[nacos服务中心、配置中心、限流等使用(系列示例整理中...)](https://github.com/smltq/spring-boot-demo/blob/master/cloud-alibaba) #### Spring Cloud Alibaba 模块 模块名称|主要内容 ---|...

    单点登录源码

    服务网关,对外暴露统一规范的接口和包装响应结果,包括各个子系统的交互接口、对外开放接口、开发加密接口、接口文档等服务,可在该模块支持验签、鉴权、路由、限流、监控、容错、日志等功能。示例图: ![API网关]...

    JAVA上百实例源码以及开源项目

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    JAVA上百实例源码以及开源项目源代码

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    zheng企业级开发框架-其他

    服务网关,对外暴露统一规范的接口和包装响应结果,包括各个子系统的交互接口、对外开放接口、开发加密接口、接口文档等服务,可在该模块支持验签、鉴权、路由、限流、监控、容错、日志等功能。 zheng-cms 内容管理...

    lamp-cloud微服务脚手架-其他

    其中扩展和借鉴国外项目的扩展基于JWT的Zuul限流插件,方面进行限流。 4、熔断机制: 因为采取了服务的分布,为了避免服务之间的调用“雪崩”,采用了Hystrix的作为熔断器,避免了服务之间的“雪崩”。 5、监控: ...

Global site tag (gtag.js) - Google Analytics