上一篇《限流--单机限流》讲述了单机限流的原理和技术实现,那么在现在分布式架构盛行的互联网时代,对于资源紧俏或者出于安全防范的目的,对一些核心的接口会做限流,或者对于一些黑灰产业在应用入口处做拦截或者限流。先举两个例子让大家更有体感:
案例一:数据资源紧缺
对于互联网应用,数据库很多时候会变成稀缺资源,很多小公司架构模式是在应用层实现分布式架构,但是数据库层可能是单点或者简单的主从结构:
在流量高峰期QPS可能会超过DB能够支撑的阈值,如果不做管控或限制,可能会因为DB挂掉导致整个应用挂掉,正常访问那部分流量也会受到影响,这个时候我们会考虑在一些热点接口做限流管控,对于上述的架构模式,一般是在应用或者访问层限流,对应就是http接口和rpc接口,具体在哪一层限流,需要根据具体的业务场景做区分,应用层更关注具体业务场景,服务层更多关注的是业务场景的代码实现,更通用。两种方式各有优劣,应用层也就是我们所说的控制器层,按照分层设计思想来说这一层不做逻辑的,如果加了限流其实就违背了这个思想,但是从业务角度思考,加在这一层其实是以更细的业务粒度做流量管控;加在服务层,更加通用也符合分层设计思想,但是如果服务层暴露给上层应用的是一个通用的接口,那么此处限流粒度就有点粗,可能原本想对某一个业务场景限流,但是实际上影响了n个业务场景的访问。
案例二:防黑灰产和爬虫
就目前来说,国内的电商可以说已经发展到了鼎盛时期,现在也产生了不计其数的个人或者注册公司的团队依附于大的电商平台来生存,他们的手段就是以用户的身份去访问电商平台,然后对拿到的数据做比较专业的分析,然后把一些优惠信息卖给其他人来牟利。
那么对于电商平台来说,其实这部分流量对我来说就是非法流量,那如果我能够通过大数据或者其他方式甄别出来这部分流量,并且加入黑名单,对其访问做限制,对于电商平台来说首先是一种自我保护措施,再者其实就是把优惠分配给平台上真正的优质用户。
基于这个场景,我此处暂时列出一种解决方案:
在反向代理层(请求尚未进入真实服务器)对在黑名单中的用户做限流,限制其访问频次,如果是黑灰产或者爬虫,其肯定会频繁的访问,在nginx层限制在黑名单中的用户每秒只能访问一次,从一定程度上就能起到系统保护作用。
上边两个案例描述了分布式应用中需要限流的一些点,还有不同场景下限流的时机。对于案例一,目前可是基于redis实现接口限流,对于案例二,可以使用lua redis实现在代理层限流。此篇接下来将重点讲述案例一中的基于redis实现接口访问限流的原理及技术实现,案例二中的nginx lua redis实现限流后续篇幅中会讲述。
基于redis实现接口限流
redis2.6版本增加了对lua脚本的原生支持,为了保证限流操作中redis每个步骤的原子性,我们此处借助lua脚本操作redis,核心代码如下:
private static JedisPool jedisPool = SpringContextUtil.getBean("jedisPool",JedisPool.class);
/**
* 针对资源key,每seconds秒最多访问maxCount次,超过maxCount次返回false
*
* @param key
* @param seconds
* @param limitCount
* @return
*/
public static boolean tryAccess(String key, int seconds, int limitCount) {
LimitRule limitRule = new LimitRule();
limitRule.setLimitCount(limitCount);
limitRule.setSeconds(seconds);
return tryAccess(key, limitRule);
}
/**
* 针对资源key,每limitRule.seconds秒最多访问limitRule.limitCount,超过limitCount次返回false 超过lockCount
* 锁定lockTime
*
* @param key
* @param limitRule
* @return
*/
public static boolean tryAccess(String key, LimitRule limitRule) {
Jedis jedis = null;
long count = -1;
try {
jedis = jedisPool.getResource();
List<String> keys = new ArrayList<String>();
String newKey = "limit:" key ":" limitRule.getSeconds();
keys.add(newKey);
List<String> args = new ArrayList<String>();
args.add(Math.max(limitRule.getLimitCount(), limitRule.getLockCount()) "");
args.add(limitRule.getSeconds() "");
args.add(limitRule.getLockCount() "");
args.add(limitRule.getLockTime() "");
count = Long.parseLong(jedis.eval(buildLuaScript(limitRule), keys, args) "");
return count <= limitRule.getLimitCount();
} finally {
if (jedis != null)
jedis.close();
}
}
/**
* 创建lua脚本交给redis执行
* <p>新版本redis支持lua脚本</p>
* <p>lua脚本被执行的时候能够保证原子性</p>
* @param limitRule
* @return
*/
private static String buildLuaScript(LimitRule limitRule) {
StringBuilder lua = new StringBuilder();
lua.append("nlocal c");
lua.append("nc = redis.call('get',KEYS[1])");//KEYS[1]表示要限制访问的key
lua.append("nif c and tonumber(c) > tonumber(ARGV[1]) then");//ARGV[1]=限制访问的次数
lua.append("nreturn c;");
lua.append("nend");
lua.append("nc = redis.call('incr',KEYS[1])");
lua.append("nif tonumber(c) == 1 then");
lua.append("nredis.call('expire',KEYS[1],ARGV[2])");//如果是限流期间key第一次访问,重新设置ARGV[2]失效时间
lua.append("nend");
if (limitRule.enableLimitLock()) {//如果设置了超过限流次数锁定
lua.append("nif tonumber(c) > tonumber(ARGV[3]) then");//ARGV[3]访问锁定次数
lua.append("nredis.call('expire',KEYS[1],ARGV[4])");//ARGV[4]访问锁定时间
lua.append("nend");
}
lua.append("nreturn c;");
return lua.toString();
}
上述代码中和新逻辑是tryAccess方法:
首先获取redis连接,然后用冒号隔开构建redis存储的key,接着构建了两个列表,一个是key,一个是参数,再然后调用jedis客户端的eval方法使用原生的lua脚本执行命令,如果返回值小于限流大小就允许访问,否则拒绝。
编写测试代码:
public static void main(String[] args) {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("spring-root.xml");
context.start();
String ipHost = "192.168.1.1";
for(int i = 0 ;i < 10; i ) {
boolean result = RateLimiter.tryAccess(ipHost,1,1);
if(result) {
System.out.println("访问成功");
} else {
System.out.println("访问受限");
}
}
context.stop();
}
运行测试代码,结果如下:
我们设置的访问限制是每秒允许一次,然后循环调用10次,看后只有一次成功了,也就是成功限流了超过阈值的部分,虽然这里是单机运行,对于多台服务器发起的请求同样能够限制,因为所有服务器走到限流逻辑的时候都会去redis获取访问资格。至于多服务器发起的请求限流,这里就不做赘述,感兴趣可以自己模拟实现。
总结
通过上述一系列描述,想必对分布式限流有了比较深刻的认识,使用Redis lua脚本编码实现限流,首先实现了限流逻辑中对redis查询和更新操作的原子性,然后从效果层面看,也实现了对访问频率的限制。其实接口粒度的限流有很多时候并不能解决所有问题,首先既然能够走到接口限流,那么请求必然已经进入了服务器,就算在接口层面被拦截,但也势必占用一定的系统资源,对于限流有句话讲的特别好“限流越早越好”,也就是说能够在服务器外层拦截或者限制掉最好,很多应用架构都是在nginx层做请求拦截和限流。希望此篇赘述对大家对限流的理解有所启发,在日常开发中有所帮助。