概述
在设计服务的时候,我们会遇到不同的限速限频需求。根据不同的需求场景,我们会有对应的解决方案。
服务限速
需求场景
- 单机需要限制某些代码块的QPS,如数据库的插入操作、Redis写入操作等;
- 超过指定QPS时自动阻塞线程或者自动丢弃指定代码块逻辑;
- 代替自己通过sleep的方式实现的限速方案;
解决方案
使用Guava的RateLimiter工具类,基于令牌桶算法实现流量限制。
一般地RateLimiter提供的是单机的限流方案。若需要实现服务整体的QPS流量控制,可以基于Redis实现令牌桶算法。也可以将整体QPS拆分成单个机器的访问QPS,再进行相应的控制(这种方式QPS控制并不是很准确,尤其是在服务QPS限制比较低的情况下)。
使用方法
private final RateLimiter rateLimiter = RateLimiter.create(100); // 100QPS
// 堵塞限制QPS
void foo1() {
rateLimiter.acquire(); // 在这里有可能发生堵塞
// 实际执行的逻辑块
}
// 不堵塞限制QPS
void foo2() {
if (rateLimiter.tryAcquire()) { // 这里不会发生任何堵塞行为
// QPS之内允许执行的代码路径
} else {
// 超过指定QPS时执行的代码路径
}
}
注意事项
一般不推荐在同步业务请求中使用堵塞版本的RateLimter进行限速,更多的使用场景是在消费类场景中使用(如Kafka消费、定时任务之类)。
时间间隔的次数限制
需求场景
-
高频次场景
如1天内接口请求次数;
-
低频次场景
如1个小时内用户只能发5条feed动态;
解决方案
-
高频次场景(Redis Counter)
在高频次场景我们一般都不会太关注具体的限频次数,所以在计数上不会要求特别准确。
我们可以通过Redis的Counter对频次进行统计。如1天内的接口请求次数我们可以类似这样设计20190403_appkey,在20190403当天所有的接口访问,都会对这个key进行加1操作,这样就可以大概统计到当天的所有请求次数,并进行相应的限次逻辑。
private final FastDateFormat fastDateFormat = FastDateFormat.getInstance("MMddHH"); @Resource private RedisClient rc; public long incrCount(String appkey, long time) { String key = key(appkey, time); return rc.incr(key); } private String key(String appkey, long time) { return appkey + "_" + fastDateFormat.format(time); }
-
低频次场景
在低频次场景,因为次数限制比较少,所以要求计数准确。
Redis SortedSet
我们可以使用Redis SortedSet去维护feed集合,member为对应的feed动态ID,score为对应的feed动态发布时间。在用户发布feed动态的时候,先计算有效的元素个数,最后进行相应的限次逻辑。
这里是任意的时间间隔,而不是固定的时间间隔。假若用户在0点59分发布了5条feed动态,用户在1点0分的时候仍不可以发布,需要等到1点59分的时候才可以解除限制。
@Resource private RedisClient rc; public void add(User user, FeedItem feedItem) { rc.zadd(key(user), feedItem.getCreateTime(), feedItem.getFeedId()); double min = 0; double max = (double) System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1); rc.zremrangeByScore(key(user), min, max); } public List<String> get(User user, long curTime) { double max = curTime; double min = (double) curTime - TimeUnit.HOURS.toMillis(1); Set<byte[]> value = rc.zrevrangeByScore(key(user), max, min); return value.stream().map(RedisUtils::toString).collect(Collectors.toList()); } private String key(User user) { return String.valueOf(user.getUid()); }
Redis Hash
如果我们的需求变得更复杂,需要根据动态类型限定用户只能发布N条feed动态。
我们可以通过Redis的Hash结构来维护用户最近发布的N条feed动态,其中field为动态类型,value为feed动态列表。在用户发布feed动态的时候,需要获取对应类型的发布feed集合,过滤掉过期的feed集合后判断用户是否允许发布。若允许发布,再把新的feed加入到对应的集合中。(注:过期的feed集合已经过滤,因此数据不会越来越多)
注意事项
-
注意并发,可以通过分布式锁解决并发访问的问题。这里一般是低频操作,出现并发的可能性较低;
-
使用上比Sorted Set复杂,不过可以减少空间占用;
-
小结
以上是我们常见的限速限频需求,本文简单介绍一些基本的思路。在实际应用场景中或许有更巧妙的解决方案,可以一起沟通交流。