当系统处理能力有限时,为了阻止请求继续对系统施压,需要限流。
控制用户行为,避免垃圾请求,也需要限流策略。
实现限流的方式有很多种,比如:
- Tomcat 的 maxThreads (默认200,Linux 单个进程最多1000线程),acceptCount(排队容量)
- Nginx 的 limit_req_zone(单位时间请求数)
- Redis-cell 漏斗限流
- gava 的令牌桶限流(单体)
个人感觉最实用的还是 Redis 的 zset 滑动窗口限流,实现简单,可用于分布式
zset 滑动窗口实现
为了测试方便(一毫秒内添加多次) 使用了
value: [uuid] score : [秒级时间戳]
,实际项目中可以使用,
value: [毫秒级时间戳] score : [毫秒级时间戳]
毕竟来自单个用户的请求一毫秒内不会有第二次
python
import time
import redis
import uuid
client = redis.StrictRedis()
def is_action_allowed(user_id, action_key, period, max_count):
"""
是否未超限度允许执行
:param user_id: 用户标识
:param action_key: 动作标识
:param period: 时间段,单位:秒
:param max_count: 限制次数
:return: 是否允许执行
"""
key = 'hist:{}:{}'.format(user_id, action_key)
now_ts = int(time.time()) # 秒级时间戳
with client.pipeline() as pip:
# 记录行为
# value: uuid score : 秒级时间戳
pip.zadd(key, {str(uuid.uuid4()): now_ts})
# 移除时间窗口之前的行为记录,剩下的都是时间窗口的
pip.zremrangebyscore(key, 0, now_ts - period)
# 获取窗口内的行为数量
pip.zcard(key)
# 设置 zset 过期时间,避免冷用户持续占用内存
# 过期时间应该等于时间窗口的长度,再多宽限 1s
pip.expire(key, period + 1)
_, _, current_count, _ = pip.execute()
# 比较数量是否还没超限制
return current_count <= max_count
if __name__ == '__main__':
for i in range(20):
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;
}
}