`
hbxflihua
  • 浏览: 680270 次
  • 性别: 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 "";
	}
}

 

分享到:
评论

相关推荐

    基于Redis实现分布式应用限流的方法

    总的来说,基于Redis的分布式限流方案具有以下优点: 1. 高性能:Redis作为内存数据库,读写速度快,能够快速响应限流决策。 2. 可扩展性:在分布式环境中,多个应用实例可以共享同一套限流规则,通过Redis进行协调...

    Go-基于redis的分布式限频模块

    总之,基于Redis的分布式限频模块在Go语言中是一个实用的工具,它提供了一种简单的方法来控制服务的访问速率,从而保护系统免受过高负载的影响。通过合理地设计和使用这样的限频策略,开发者可以有效地提升系统的...

    分布式环境下限流方案的实现redis RateLimiter Guava,Token Bucket, Leaky Bucket1

    【分布式环境下限流方案的实现】在现代高并发的分布式系统中,限流是一个至关重要的技术,用于防止系统被过量的请求淹没,确保服务的稳定性和可用性。本篇文章主要探讨了在分布式环境中,如何利用Redis、Guava的...

    基于redis限流系统

    此外,为了提高限流系统的可扩展性和灵活性,可以考虑引入分布式限流方案,例如使用Redis集群,通过分布式锁或分布式计数器来协调不同节点间的限流策略。还可以结合服务发现机制动态调整限流策略,根据服务实例的...

    python基于redis的限流器.zip

    在IT行业中,限流是一种常见的系统保护策略,用于限制在特定时间...通过学习这个项目,我们可以了解如何利用Redis的数据结构和原子操作来构建高效、可扩展的限流解决方案,这对于构建稳定、高可用的后端服务至关重要。

    基于分布式配置中心配置限流参数的Redis轻量级分布式限流组件-lightweight-rate-limiter.zip

    本文将深入探讨基于分布式配置中心配置限流参数的Redis轻量级分布式限流组件——lightweight-rate-limiter。该组件旨在帮助开发者实现高效、灵活的限流策略,确保服务的稳定性和性能。 一、限流概念与重要性 限流...

    基于RateLimiter和Lua脚本限量控制实现分布式限流.docx

    ### 基于RateLimiter和Lua脚本的分布式限流技术详解 #### 一、引言 在高并发场景下,为了保护后端服务不被突发流量冲击导致崩溃,通常会采用限流策略来控制进入系统的请求数量。限流机制能够有效地分配资源,避免...

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

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

    基于redis限流系统.zip

    在这个“基于redis限流系统.zip”压缩包中,我们可以预见到包含了一个使用Redis和Lua脚本实现的限流解决方案,以及可能的设计文档或源代码。 首先,我们来看看限流的基本概念。限流的主要目标是避免系统过载,保护...

    一个应用于springboot项目的,基于redis的分布式锁 可用于多节点项目防重复业务调用

    综上所述,这个项目提供了一套便捷的Spring Boot集成Redis分布式锁的解决方案,能够帮助开发者快速实现多节点环境下的并发控制,提高系统的稳定性和可靠性。通过学习和实践,你可以深入理解分布式锁的设计与实现,...

    redis 分布式缓存

    2. 限流和计数:利用Redis的原子操作,可以实现如访问频率限制、消息队列等功能。 3. 分布式锁:Redis的`SETNX`和`EXPIRE`命令可以实现简单的分布式锁,解决并发问题。 4. Session共享:在Web应用中,可以将用户...

    基于Redis+Lua脚本实现分布式限流组件封装的方法

    在分布式系统中,限流是一种重要的策略,用于保护服务免受过高的请求负载,避免系统崩溃。...通过这种方式,我们可以构建一个高效、灵活且易于扩展的分布式限流解决方案,有效保障服务的稳定性和性能。

    基于redis和lua脚本的分布式锁的实现

    * 集群限流:在分布式系统中,需要限制某个资源的访问频次,以避免过载和崩溃。 * 分布式事务:在分布式系统中,需要确保多个节点之间的数据一致性。 * 分布式配置:在分布式系统中,需要在多个节点之间同步配置信息...

    详解springboot+aop+Lua分布式限流的最佳实践

    本文主要介绍了springboot+aop+Lua分布式限流的最佳实践,涵盖了限流的定义、限流的重要性、限流方案等多个方面。 一、什么是限流?为什么要限流? 限流是指限制系统处理请求的速度,以避免系统崩溃和超载。限流是...

    redis分布式锁及会出现的问题解决

    Redis 分布式锁是分布式系统中用于解决并发问题的重要工具,尤其在高并发场景下,如秒杀活动、限流控制等。它基于单机的 Redis 数据存储来实现跨节点的锁服务。以下是对Redis分布式锁及其可能出现的问题的详细解释:...

    20年IT老民工苦心编撰成超大流量分布式系统架构解决方案文档.docx

    本章重点讨论了流量管制的基本原理和技术实现方式,包括但不限于常见的限流算法(如令牌桶算法、漏桶算法)、接入层和应用层的限流方案。 **2. 基于时间分片的削峰方案** 除了传统的限流技术之外,文章还提出了一...

    springboot基于redis防止接口恶意刷新和暴力请求

    7. **分布式限流**:在多实例的微服务环境中,限流策略需要考虑分布式场景。可以使用Redis的分布式锁或者Spring Cloud Gateway的RateLimiter组件实现跨服务的限流。 通过以上步骤,我们能够构建一个基于Spring Boot...

    (源码)基于Redis的秒杀与缓存管理系统.zip

    # 基于Redis的秒杀与缓存管理... 缓存雪崩解决方案通过设置不同的过期时间、热点数据永不过期、限流降级等手段,防止缓存雪崩。 缓存击穿解决方案通过互斥锁和逻辑过期时间两种方式,解决缓存击穿问题。 ### 分布式锁

    微服务架构下的分布式限流方案全解析

    微服务架构下的分布式限流方案全解析,是针对现代服务架构中确保系统稳定性和抗压能力的重要技术。在微服务环境中,服务间的依赖性增强,系统复杂性也随之增加,因此,缓存、降级和限流成为保障系统稳定运行的三大...

    分布式Redis+消息中间件实现高并发(秒杀方案)源码.zip

    分布式系统在处理高并发场景时,常常...以上就是基于分布式Redis和消息中间件实现高并发秒杀方案的基本原理和可能的源码结构。实际应用中,还需要结合具体业务需求和系统架构进行调整优化,确保系统的高可用性和性能。

Global site tag (gtag.js) - Google Analytics