14.4 限流算法实现
面试重要程度:⭐⭐⭐⭐⭐
常见提问方式: "设计API限流系统" "令牌桶vs漏桶算法" "分布式限流实现"
预计阅读时间:40分钟
🎯 限流算法基础
常见限流算法
限流是保护系统稳定性的重要手段,常见算法包括:
- 固定窗口计数器:简单但有突刺问题
- 滑动窗口计数器:平滑限流,内存开销大
- 令牌桶算法:允许突发流量,平滑限流
- 漏桶算法:强制限制输出速率
🪙 令牌桶算法
令牌桶实现
/**
* 令牌桶限流算法
*/
public class TokenBucketRateLimiter {
private final int capacity; // 桶容量
private final double refillRate; // 令牌补充速率(个/秒)
private final Map<String, TokenBucket> buckets;
public TokenBucketRateLimiter(int capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.buckets = new ConcurrentHashMap<>();
}
/**
* 令牌桶
*/
static class TokenBucket {
private volatile double tokens;
private volatile long lastRefillTime;
private final int capacity;
private final double refillRate;
public TokenBucket(int capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire(int tokensRequested) {
refill();
if (tokens >= tokensRequested) {
tokens -= tokensRequested;
return true;
}
return false;
}
private void refill() {
long currentTime = System.currentTimeMillis();
double timePassed = (currentTime - lastRefillTime) / 1000.0;
if (timePassed > 0) {
double tokensToAdd = timePassed * refillRate;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = currentTime;
}
}
public double getAvailableTokens() {
refill();
return tokens;
}
}
/**
* 尝试获取许可
*/
public boolean tryAcquire(String key) {
return tryAcquire(key, 1);
}
/**
* 尝试获取指定数量的许可
*/
public boolean tryAcquire(String key, int permits) {
TokenBucket bucket = buckets.computeIfAbsent(key,
k -> new TokenBucket(capacity, refillRate));
return bucket.tryAcquire(permits);
}
}
🌐 分布式限流
基于Redis的分布式限流
/**
* 基于Redis的分布式限流器
*/
@Component
public class DistributedRateLimiter {
private final RedisTemplate<String, Object> redisTemplate;
private final String keyPrefix = "rate_limiter:";
public DistributedRateLimiter(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 固定窗口限流
*/
public boolean tryAcquireFixedWindow(String key, int maxRequests, long windowSizeInSeconds) {
String redisKey = keyPrefix + "fixed:" + key;
long currentWindow = System.currentTimeMillis() / (windowSizeInSeconds * 1000);
String windowKey = redisKey + ":" + currentWindow;
// 使用Lua脚本保证原子性
String luaScript =
"local current = redis.call('GET', KEYS[1]) " +
"if current == false then " +
" redis.call('SET', KEYS[1], 1) " +
" redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" if tonumber(current) < tonumber(ARGV[1]) then " +
" return redis.call('INCR', KEYS[1]) " +
" else " +
" return -1 " +
" end " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(windowKey),
maxRequests, windowSizeInSeconds);
return result != null && result != -1;
}
/**
* 令牌桶限流
*/
public boolean tryAcquireTokenBucket(String key, int capacity, double refillRate) {
String redisKey = keyPrefix + "token:" + key;
long currentTime = System.currentTimeMillis();
String luaScript =
"local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill') " +
"local tokens = tonumber(bucket[1]) or tonumber(ARGV[1]) " +
"local lastRefill = tonumber(bucket[2]) or tonumber(ARGV[4]) " +
"local timePassed = (tonumber(ARGV[4]) - lastRefill) / 1000.0 " +
"if timePassed > 0 then " +
" tokens = math.min(tonumber(ARGV[1]), tokens + timePassed * tonumber(ARGV[2])) " +
"end " +
"if tokens >= tonumber(ARGV[3]) then " +
" tokens = tokens - tonumber(ARGV[3]) " +
" redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', ARGV[4]) " +
" redis.call('EXPIRE', KEYS[1], 3600) " +
" return 1 " +
"else " +
" redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', ARGV[4]) " +
" redis.call('EXPIRE', KEYS[1], 3600) " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisS
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经
查看1道真题和解析