服务限频限次的场景方案
概述
在设计服务的时候,我们会遇到不同的限速限频需求。根据不同的需求场景,我们会有对应的解决方案。
服务限速
需求场景
- 单机需要限制某些代码块的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复杂,不过可以减少空间占用;
小结
以上是常见的限速限频需求,本文简单介绍一些基本的思路。在实际应用场景中或许有更巧妙的解决方案,可以说说自己的见解。