由于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 ""; } }
相关推荐
FAT ,基于springboot , 使用zookeeper,redis , spring async , spring transactionManager的强一致性分布式事务解决方案 ## 框架介绍 纯编码方式,强一致性。 使用redis/zookeeper作为注册中心 ,代理事务的执行...
高并发下的服务降级、限流实战 基于分布式架构下分布式锁的解决方案实战 分布式架构实现分布式定时调度 分布式架构-中间件 分布式消息通信 消息中间件在分布式架构中的应用 ActiveMQ ActiveMQ高可用集群企业...
4、后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 优化思路 1、分流、分流、分流,重要的事情说三遍,再牛逼的机器也抵挡不住高级别的...
限流组件使用 Sentinel;基于 Netty 进行通信、维护长连接;RocketMQ 作为消息队列,处理聊天消息的异步入库以及解决分布式 Netty 节点问题; Zookeeper 用于分布式 id 的生成;Redis 用于记录用户在线状态以及记录 ...
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 ...
上百节课详细讲解,需要的小伙伴自行百度网盘下载,链接见附件,永久有效。 课程介绍: 01_先来看一个互联网java工程师的招聘JD 02_互联网Java工程师面试突击训练课程第一季的内容说明 ...限流?熔断?降级?什么鬼!
4、后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 spring-boot-seckill分布式秒杀系统优化思路 1、分流、分流、分流,重要的事情说三...
模块包括:企业级的认证系统、开发平台、应用监控、慢sql监控、统一日志、单点登录、Redis分布式高速缓存、配置中心、分布式任务调度、接口文档、代码生成等等。 mallcloud商城特点: 1、前后端分离的企业级微服务...
模块包括:企业级的认证系统、开发平台、应用监控、慢sql监控、统一日志、单点登录、Redis分布式高速缓存、配置中心、分布式任务调度、接口文档、代码生成等等。mallcloud商城特点1、前后端分离的企业级微服务架构 ...
其中扩展和借鉴国外项目的扩展基于JWT的Zuul限流插件,方面进行限流。 4、熔断机制: 因为采取了服务的分布,为了避免服务之间的调用“雪崩”,采用了Hystrix的作为熔断器,避免了服务之间的“雪崩”。 5、监控: ...
Spring Cloud Alibaba致力于提供微服务... springcloudsimple:基于springcloud alibaba的基础学习模块,里面主要致力基本的服务构建,服务注册,调用,限流,熔断等基础组建的演示。是一个非常好的入门学习项目。 spr
集成的sentinel限流组件,可以针对http请求以及dubbo rpc调用限流 集成新版支付宝easySDK,通过当面扫完成扫码付款 集成服务网关,采用Spring Cloud Gateway网关组件,并提供JWT用户鉴权功能 :gem_stone:分支介绍 ...
cloud-alibaba|[nacos服务中心、配置中心、限流等使用(系列示例整理中...)](https://github.com/smltq/spring-boot-demo/blob/master/cloud-alibaba) #### Spring Cloud Alibaba 模块 模块名称|主要内容 ---|...
服务网关,对外暴露统一规范的接口和包装响应结果,包括各个子系统的交互接口、对外开放接口、开发加密接口、接口文档等服务,可在该模块支持验签、鉴权、路由、限流、监控、容错、日志等功能。示例图: ![API网关]...
Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...
Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...
服务网关,对外暴露统一规范的接口和包装响应结果,包括各个子系统的交互接口、对外开放接口、开发加密接口、接口文档等服务,可在该模块支持验签、鉴权、路由、限流、监控、容错、日志等功能。 zheng-cms 内容管理...
其中扩展和借鉴国外项目的扩展基于JWT的Zuul限流插件,方面进行限流。 4、熔断机制: 因为采取了服务的分布,为了避免服务之间的调用“雪崩”,采用了Hystrix的作为熔断器,避免了服务之间的“雪崩”。 5、监控: ...