服务限频限次的场景方案

3,311 阅读4分钟

概述

在设计服务的时候,我们会遇到不同的限速限频需求。根据不同的需求场景,我们会有对应的解决方案。

服务限速

需求场景

  • 单机需要限制某些代码块的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复杂,不过可以减少空间占用;

小结

以上是我们常见的限速限频需求,本文简单介绍一些基本的思路。在实际应用场景中或许有更巧妙的解决方案,可以一起沟通交流。