实现方案 1.基于guava 限流实现(单机版) guava 为谷歌开源的一个比较实用的组件,利用这个组件可以帮助开发人员完成常规的限流操作,接下来看具体的实现步骤
1.1 依赖引入 1 2 3 4 5 <dependency > <groupId > com.google.guava</groupId > <artifactId > guava</artifactId > <version > 30.1-jre</version > </dependency >
1.2 自定义注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.zhen.studytotal.limitRequest.annotation;import java.lang.annotation.*;@Documented @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) public @interface GuavaLimitRateAnnotation { String limitType () ; double limitCount () default 5d ; }
1.3 guava工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.zhen.studytotal.limitRequest.aspect; import com.google.common.util.concurrent.RateLimiter;import java.util.HashMap;import java.util.Map;public class RateLimiterHelper { private RateLimiterHelper () {} private static Map<String, RateLimiter> rateMap = new HashMap <>(); public static RateLimiter getRateLimiter (String limitType, double limitCount ) { RateLimiter rateLimiter = rateMap.get(limitType); if (rateLimiter == null ){ rateLimiter = RateLimiter.create(limitCount); rateMap.put(limitType,rateLimiter); } return rateLimiter; } }
1.4 AOP切面类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 package com.zhen.studytotal.limitRequest.aspect;import com.alibaba.fastjson2.JSONObject;import com.fasterxml.jackson.databind.util.JSONPObject;import com.google.common.util.concurrent.RateLimiter;import com.zhen.studytotal.limitRequest.annotation.GuavaLimitRateAnnotation;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.ServletOutputStream;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Method;import java.util.Objects;@Aspect @Component public class GuavaLimitRateAspect { private static Logger logger = LoggerFactory.getLogger(GuavaLimitRateAspect.class); @Before("execution(@com.zhen.studytotal.limitRequest.annotation.GuavaLimitRateAnnotation * *(..))") public void limit (JoinPoint joinPoint) { Method currentMethod = getCurrentMethod(joinPoint); if (Objects.isNull(currentMethod)){ return ; } String limitType = currentMethod.getAnnotation(GuavaLimitRateAnnotation.class).limitType(); double limitCount = currentMethod.getAnnotation(GuavaLimitRateAnnotation.class).limitCount(); RateLimiter rateLimiter = RateLimiterHelper.getRateLimiter(limitType, limitCount); boolean b = rateLimiter.tryAcquire(); if (b){ logger.info("获取令牌成功" ); } else { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); JSONObject jsonObject = new JSONObject (); jsonObject.put("success" , false ); jsonObject.put("message" , "请求频繁,限流中···" ); try { output(response, jsonObject.toJSONString()); }catch (Exception e){ logger.error("限流异常:{}" , e); } } } private void output (HttpServletResponse response, String jsonString) throws IOException { response.setContentType("application/json;charset=UTF-8" ); ServletOutputStream outputStream = null ; try { outputStream = response.getOutputStream(); outputStream.write(jsonString.getBytes("UTF-8" )); }catch (IOException e){ e.printStackTrace(); }finally { assert outputStream != null ; outputStream.flush(); outputStream.close(); } } private Method getCurrentMethod (JoinPoint joinPoint) { Method[] methods = joinPoint.getTarget().getClass().getMethods(); Method target = null ; for (Method method : methods) { if (method.getName().equals(joinPoint.getSignature().getName())) { target = method; break ; } } return target; } }
1.5 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.zhen.studytotal.limitRequest.controller;import com.zhen.studytotal.limitRequest.annotation.GuavaLimitRateAnnotation;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/test/guava") public class TestGuavaController { @GetMapping("/limit") @GuavaLimitRateAnnotation(limitType = "测试guava限流", limitCount = 1) public String test () { return "limit" ; } }
1.6 效果 这里为了清楚看到效果,限流QPS设置为1,多刷新几次请求即可
2. 基于 redis+lua 限流实现(分布式版) redis是线程安全的,天然具有线程安全的特性,支持原子性操作,限流服务不仅需要承接超高QPS,还要保证限流逻辑的执行层面具备线程安全的特性,利用Redis这些特性做限流,既能保证线程安全,也能保证性能
结合流程图可以得出以下实现思路:
1.编写 lua 脚本,指定入参的限流规则,比如对特定的接口限流时,可以根据某个或几个参数进行判定,调用该接口的请求,在一定的时间窗口内监控请求次数; 2.既然是限流,最好能够通用,可将限流规则应用到任何接口上,那么最合适的方式就是通过自定义注解形式切入; 3.提供一个配置类,被 spring 的容器管理,redisTemplate 中提供了 DefaultRedisScript这个 bean; 4.提供一个能动态解析接口参数的类,根据接口参数进行规则匹配后触发限流;
2.1 依赖 YML文件记得添加redis相关配置
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
2.2 自定义注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.zhen.studytotal.limitRequest.annotation;import com.zhen.studytotal.limitRequest.enums.LimitTypeEnum;import org.apache.commons.lang3.StringUtils;import java.lang.annotation.*;@Documented @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) public @interface RedisLimitAnnotation { String key () default "" ; String prefix () default "" ; int count () ; int time () ; LimitTypeEnum limitType () default LimitTypeEnum.INTERFACE; }
2.3 限流类型枚举 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.zhen.studytotal.limitRequest.enums;import lombok.Getter;@Getter public enum LimitTypeEnum { INTERFACE , IP , CUSTOMER ; }
2.4 IP工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.zhen.studytotal.limitRequest.utils;import javax.servlet.http.HttpServletRequest;import java.net.HttpCookie;import java.net.InetAddress;import java.net.UnknownHostException;public class IPUtils { public static String getIpAddr (HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for" ); if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-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.getHeader("HTTP_CLIENT_IP" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } if ("localhost" .equalsIgnoreCase(ip) || "127.0.0.1" .equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1" .equalsIgnoreCase(ip)){ InetAddress inet; try { inet = InetAddress.getLocalHost(); ip = inet.getHostAddress(); } catch (UnknownHostException e) { e.printStackTrace(); } } if (null != ip && ip.length() > 15 ) { if (ip.indexOf("," ) > 15 ) { ip = ip.substring(0 , ip.indexOf("," )); } } return ip; } }
2.5 自定义 lua 脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 local key = KEYS[1 ]local limit = tonumber (ARGV[1 ])local count = tonumber (ARGV[2 ])local current = tonumber (redis.call('get' , key) or "0" )if current + 1 > limit then return 0 end current = redis.call("INCRBY" , key, "1" ) if tonumber (current) == 1 then redis.call("expire" , key, count) end return current
2.6 Redis配置类 设置执行lua脚本,这里注意lua脚本返回类型,我这里原来采用number类型接收返回,报错
io.lettuce.core.output.ValueOutput does not support set(long),改用Long类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.zhen.studytotal.limitRequest.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.scripting.support.ResourceScriptSource;@Configuration public class RedisConfiguration { @Bean public DefaultRedisScript<Long> redisLuaScript () { DefaultRedisScript<Long> numberDefaultRedisScript = new DefaultRedisScript <>(); numberDefaultRedisScript.setScriptSource(new ResourceScriptSource (new ClassPathResource ("lua\\limit.lua" ))); numberDefaultRedisScript.setResultType(Long.class); return numberDefaultRedisScript; } @Bean("redisTemplate") public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate= new RedisTemplate <>(); redisTemplate.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer (Object.class); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
2.7 Redis切面类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 package com.zhen.studytotal.limitRequest.aspect;import com.zhen.studytotal.limitRequest.annotation.RedisLimitAnnotation;import com.zhen.studytotal.limitRequest.enums.LimitTypeEnum;import com.zhen.studytotal.limitRequest.utils.IPUtils;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;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.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;import java.util.Collections;import java.util.Objects;@Aspect @Component public class RedisLimitAspect { public static final Logger logger = LoggerFactory.getLogger(RedisLimitAspect.class); @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private DefaultRedisScript<Long> redisLuaScript; @Pointcut("@annotation(com.zhen.studytotal.limitRequest.annotation.RedisLimitAnnotation)") public void redisLimit () {} @Around("redisLimit()") public Object interceptor (ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); RedisLimitAnnotation annotation = method.getAnnotation(RedisLimitAnnotation.class); if (Objects.isNull(annotation)){ return joinPoint.proceed(); } String key = getKeyByLimitType(annotation,signature); Long number = redisTemplate.execute(redisLuaScript, Collections.singletonList(key), annotation.count(), annotation.time()); logger.info("限流时时间内访问次数:{}" ,number); if (number != null && number.intValue() != 0 && number.intValue() <= annotation.count()){ logger.info("限流时时间内访问次数:{}" ,number); return joinPoint.proceed(); } throw new RuntimeException ("访问次数超过限制,限流中···" ); } private String getKeyByLimitType (RedisLimitAnnotation annotation, MethodSignature signature) { String key = "" ; LimitTypeEnum limitTypeEnum = annotation.limitType(); String prefix = annotation.prefix(); if (StringUtils.isNotBlank(prefix)){ key += prefix + ":" ; } if (LimitTypeEnum.CUSTOMER == limitTypeEnum) { String tempKey = annotation.key(); if (StringUtils.isBlank(tempKey)) { throw new RuntimeException ("redis限流->自定义类型,key不能为空" ); } return key + tempKey; } Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); String classFullName = targetClass.getName() + "-" + method.getName(); if (LimitTypeEnum.INTERFACE == limitTypeEnum) { return key + classFullName; } HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ipAddr = IPUtils.getIpAddr(request); return key + ipAddr + "-" + classFullName; } }
2.8 测试 1 2 3 4 5 @GetMapping("/redis") @RedisLimitAnnotation(key = "limitByRedis",time = 5,count = 5,limitType = LimitTypeEnum.IP) public String testRedis () { return "REDIS" ; }
3.基于 sentinel 限流实现(分布式版) sentinel 通常是需要结合 springcloud-alibaba 框架一起实用的,而且与框架集成之后,可以配合控制台一起使用达到更好的效果,实际上,sentinel 官方也提供了相对原生的 SDK 可供使用,接下来就以这种方式进行整合
3.1 依赖 1 2 3 4 5 <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-core</artifactId > <version > 1.8.0</version > </dependency >
3.2 自定义注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.zhen.studytotal.limitRequest.annotation;import java.lang.annotation.*;@Documented @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) public @interface SentinelLimitRateAnnotation { String resourceName () ; int limitCount () default 5 ; }
3.3 AOP切面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 package com.zhen.studytotal.limitRequest.aspect;import com.alibaba.csp.sentinel.Entry;import com.alibaba.csp.sentinel.SphU;import com.alibaba.csp.sentinel.Tracer;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.alibaba.csp.sentinel.slots.block.RuleConstant;import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;import com.zhen.studytotal.limitRequest.annotation.SentinelLimitRateAnnotation;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.checkerframework.checker.units.qual.A;import org.springframework.stereotype.Component;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.List;import java.util.Objects;@Aspect @Component public class SentinelLimitRateAspect { @Pointcut(value = "@annotation(com.zhen.studytotal.limitRequest.annotation.SentinelLimitRateAnnotation)") public void rateLimit () { } @Around(value = "rateLimit()") public Object around (ProceedingJoinPoint joinPoint) { Method currentMethod = getCurrentMethod(joinPoint); if (Objects.isNull(currentMethod)) { return null ; } String resourceName = currentMethod.getAnnotation(SentinelLimitRateAnnotation.class).resourceName(); if (StringUtils.isEmpty(resourceName)){ throw new RuntimeException ("资源名称为空" ); } int limitCount = currentMethod.getAnnotation(SentinelLimitRateAnnotation.class).limitCount(); initFlowRule(resourceName,limitCount); Entry entry = null ; Object result = null ; try { entry = SphU.entry(resourceName); try { result = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } } catch (BlockException ex) { System.out.println("blocked" ); return "被限流了" ; } catch (Exception e) { Tracer.traceEntry(e, entry); } finally { if (entry != null ) { entry.exit(); } } return result; } private static void initFlowRule (String resourceName,int limitCount) { List<FlowRule> rules = new ArrayList <>(); FlowRule rule = new FlowRule (); rule.setResource(resourceName); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(limitCount); rules.add(rule); FlowRuleManager.loadRules(rules); } private Method getCurrentMethod (JoinPoint joinPoint) { Method[] methods = joinPoint.getTarget().getClass().getMethods(); Method target = null ; for (Method method : methods) { if (method.getName().equals(joinPoint.getSignature().getName())) { target = method; break ; } } return target; } }
3.4 测试 1 2 3 4 5 @GetMapping("/sentinel") @SentinelLimitRateAnnotation(resourceName = "测试sentinel限流", limitCount = 1) public String testSentinel () { return "Sentinel" ; }
4.参考