前言 高并发三把利器:缓存、降级、限流
缓存可以用来解决访问速度的问题,提高系统的吞吐量,减少数据库压力,分布式系统中,常用的缓存有 Redis、MongoDB 等。
本篇主要总结下降级和限流相关知识点笔记,用以提醒自己看到这些点后知道去哪里翻代码。
降级
人工降级 针对核心服务,配置相应开关
自动降级
通过熔断实现,如SpringCloud中的hystrix,dubbo 的 mock 等。
通过限流实现降级,比如当某个业务访问量过大时,可以通过限流的手段加以控制,给被限流的请求返 回友好提示或者固定页面。
限流 限流的目的是为了保护系统能够稳定地运行
限流算法 滑动窗口 TCP 流量整形手段,通过配置滑动窗口的大小来实现限流的目的。
图形演示地址:
http://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/
计数器 计数器限流算法是比较常用一种的限流方案也是最为粗暴直接的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。
漏桶算法 leaky bucket
当桶满后,常见的两种处理方式:
暂时拦截住上方注水,等待漏桶中的一部分水漏走后,再放行注水。
溢出的水直接抛弃。
漏桶算法特点 漏水的速率恒定,即使出现注水速率突然增大的情况,漏水的速率依然不变,所以演变出了另一种算法—令牌桶
令牌桶 tokenbucket 能够解决流量突发情况,是限流中最常用的办法。
令牌桶由三部分组成,令牌流、数据流、令牌桶,原理如图所示:
令牌桶算法的好处是,在请求来之前桶里预备好了一些令牌(桶满后不再生成令牌),当流量小时,获取令牌的速度小于或等于令牌生成的速度;当流量突发时,请求速率大于令牌生成速率,桶里预备的令牌也可以应付一阵子,当流量再次变小时,令牌桶继续存储预备令牌。
限流实际应用
谷歌开源的 Guava 的RateLimiter是单机版的限流实现,有两种方式:WarmUp 和 Bursty
WarmUp:使用leaky bucket 算法
Bursty: 使用令牌桶算法
Redis+Lua脚本实现分布式限流
Lua脚本如下:
1 2 3 4 5 6 7 8 9 local c = redis.call('incr' , KEYS[1 ])if tonumber (c) == 1 then redis.call('expire' , KEYS[1 ], ARGV[1 ]) return 1 elseif tonumber (c) > tonumber (ARGV[2 ]) then return 0 else return 1 end
解释:
KEYS[1]:被限制的对象key,可以是某个方法,或者ip等
ARGV[1]:第一个Value,传 超时时间 timeout,单位 s;
ARGV[2]第二个 Value,传限制次数 limitCount。
第一次使用incr对KEY加一,如果是第一次访问,使用expire设置一个超时时间,返回 1-成功。如果现在递增到值大于限制次数,返回 0-失败,否则返回 1-成功。
Lua脚本在 Redis 中执行是原子性的。
我写了个分布式限流的简单 Demo:
application.yml 配置文件 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 spring: redis: host: localhost port: 7000 password: Yakai2018@ database: 0 jedis: pool: max-active: 100 max-idle: 10 min-idle: 5 max-wait: -1ms redis-limit-lua: | local c = redis.call('INCR', KEYS[1]) if tonumber(c) == 1 then redis.call('expire', KEYS[1], ARGV[1]) return 1 elseif tonumber(c) > tonumber(ARGV[2]) then return 0 else return 1 end
LimitType 限制类型 1 2 3 4 5 6 7 8 9 10 11 public enum LimitType { IP, METHOD }
RateLimiter 限流注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface RateLimiter { int period () ; int limitCount () ; LimitType limitType () default LimitType.IP; }
RedisConfig 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class RedisConfig { @Autowired private RedisConnectionFactory factory; @Bean public RedisTemplate<String, Serializable> redisTemplate () { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate <>(); redisTemplate.setKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashValueSerializer(new StringRedisSerializer ()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer ()); redisTemplate.setConnectionFactory(factory); return redisTemplate; } }
LimitAspect 限流注解切面类 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 @Slf4j @Aspect @Component public class LimitAspect { private static String redisLimitLua; @Value("${redis-limit-lua}") public void setRedisLimitLua (String limitLua) { redisLimitLua = limitLua; } @Autowired private RedisTemplate<String,Serializable> redisTemplate; @Around("within(cn.zhenyk.limit.controller.*) && @annotation(limit)") public Object interceptor (ProceedingJoinPoint pjp, RateLimiter limit) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); LimitType limitType = limit.limitType(); String key; int limitCount = limit.limitCount(); int period = limit.period(); switch (limitType){ case IP: key = getIpAddress(); break ; case METHOD: key = method.getName(); break ; default : key = getIpAddress(); } List<String> keys = Collections.singletonList(key); try { RedisScript<Number> redisScript = new DefaultRedisScript <>(redisLimitLua, Number.class); Number c = redisTemplate.execute(redisScript, keys, period, limitCount); if (c.intValue() == 1 ){ Object proceed = pjp.proceed(); log.info("未被限流,正常执行:{}" , proceed); return proceed; }else { log.info("被限流" ); return "被限流" ; } }catch (Exception e){ log.error("执行发生异常:" ,e); return "系统错误" ; } } private String getIpAddress () { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); 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.getRemoteAddr(); } return ip; } }
TestController测试: 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class TestController { @RequestMapping("limit") @RateLimiter(period = 10,limitCount = 3) public String limit () { return "OK" ; } }
测试结果 1 2 3 4 5 6 7 8 9 未被限流,正常执行:OK 未被限流,正常执行:OK 未被限流,正常执行:OK 被限流 被限流 未被限流,正常执行:OK 未被限流,正常执行:OK
项目地址:https://github.com/zhengyakai/limit