当系统处理能力有限时,为了阻止请求继续对系统施压,需要限流。
控制用户行为,避免垃圾请求,也需要限流策略。

实现限流的方式有很多种,比如:

  • Tomcat 的 maxThreads (默认200,Linux 单个进程最多1000线程),acceptCount(排队容量)
  • Nginx 的 limit_req_zone(单位时间请求数)
  • Redis-cell 漏斗限流
  • gava 的令牌桶限流(单体)

个人感觉最实用的还是 Redis 的 zset 滑动窗口限流,实现简单,可用于分布式

zset 滑动窗口实现

为了测试方便(一毫秒内添加多次) 使用了
value: [uuid] score : [秒级时间戳]

,实际项目中可以使用,
value: [毫秒级时间戳] score : [毫秒级时间戳]
毕竟来自单个用户的请求一毫秒内不会有第二次

python

  1. import time
  2. import redis
  3. import uuid
  4. client = redis.StrictRedis()
  5. def is_action_allowed(user_id, action_key, period, max_count):
  6. """
  7. 是否未超限度允许执行
  8. :param user_id: 用户标识
  9. :param action_key: 动作标识
  10. :param period: 时间段,单位:秒
  11. :param max_count: 限制次数
  12. :return: 是否允许执行
  13. """
  14. key = 'hist:{}:{}'.format(user_id, action_key)
  15. now_ts = int(time.time()) # 秒级时间戳
  16. with client.pipeline() as pip:
  17. # 记录行为
  18. # value: uuid score : 秒级时间戳
  19. pip.zadd(key, {str(uuid.uuid4()): now_ts})
  20. # 移除时间窗口之前的行为记录,剩下的都是时间窗口的
  21. pip.zremrangebyscore(key, 0, now_ts - period)
  22. # 获取窗口内的行为数量
  23. pip.zcard(key)
  24. # 设置 zset 过期时间,避免冷用户持续占用内存
  25. # 过期时间应该等于时间窗口的长度,再多宽限 1s
  26. pip.expire(key, period + 1)
  27. _, _, current_count, _ = pip.execute()
  28. # 比较数量是否还没超限制
  29. return current_count <= max_count
  30. if __name__ == '__main__':
  31. for i in range(20):
  32. print(is_action_allowed("qj1", "reply", 60, 5))

java

RedisTemplate版本

@Component
public class RateLimiter {

    @Resource
    private RedisTemplate redisTemplate;

    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);
        int nowTs = (int) System.currentTimeMillis();
        String uuidStr = UUID.randomUUID().toString();
        List<Object> result = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations ops) throws DataAccessException {
                ops.multi();
                ops.opsForZSet().add(key, uuidStr, nowTs);
                ops.opsForZSet().removeRangeByScore(key, 0, nowTs - period);
                ops.opsForZSet().zCard(key);
                ops.expire(key, period + 1, TimeUnit.SECONDS);
                return ops.exec();
            }
        });
        Long count = (Long) result.get(2);
        return count.intValue() <= maxCount;
    }
}

Jedis版本

public class RateLimiter {

    public boolean isActionAllowed(String userId, String actionKey, long period, int maxCount) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();
        String uuidStr = UUID.randomUUID().toString();
        Pipeline pipeline = jedis.pipelined();
        pipeline.multi();
        pipeline.zadd(key, nowTs, uuidStr);
        pipeline.zrangeByScore(key, 0, nowTs - period);
        Response<Long> count = pipeline.zcard(key);
        pipeline.expire(key, period + 1);
        pipeline.exec();
        pipeline.close();
        return count.get() <= maxCount;
    }
}