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圣经

全部评论
你这个令牌桶好像没体现出预支付令牌的功能
点赞 回复 分享
发布于 2025-09-15 00:24 广东
耐面王
点赞 回复 分享
发布于 2025-09-06 08:18 浙江
很有收获,谢谢分享!
点赞 回复 分享
发布于 2025-09-02 16:55 陕西

相关推荐

不可否认的是,26的秋招和春招有一部分公司开的都很早。秋招就先不谈,春招来说,字节目前有的岗位已经开放了。目前的趋势来看就是校招的时间线统一拉长了,基本上每个时间段都有公司在招聘,只不过最集中的还是常说的金九银十和金三银四。与其纠结&nbsp;“金一银二”&nbsp;是否取代了传统旺季,不如认清&nbsp;“校招无淡季”&nbsp;的新现实&nbsp;——&nbsp;对求职者来说,早启动、稳节奏、强准备,才是应对拉长时间线的核心逻辑。首先,放弃&nbsp;“等旺季再发力”&nbsp;的拖延心态,现在就是最佳准备期。既然头部公司已陆续开放岗位,就意味着&nbsp;“先到先得”&nbsp;的隐形规则开始生效:部分岗位招满即止,早投递能抢占&nbsp;HR&nbsp;的注意力,也能避开后期简历扎堆的竞争高峰,哪怕你目前简历还未打磨好、项目经验尚需梳理。其次,建立&nbsp;“动态岗位库”,精准跟踪而非盲目海投。春招岗位分散在不同时间段,盲目投递不仅耗费精力,还可能因岗位与自身适配度低导致反馈率低下。建议你按&nbsp;“梦想公司、匹配公司、保底公司”&nbsp;分类,每类筛选&nbsp;5-8&nbsp;家核心目标:梦想公司重点关注其提前批、内推通道(内推通常能跳过简历初筛,直达笔试或面试);匹配公司聚焦与专业、实习经历高度契合的岗位,这类岗位是拿&nbsp;offer&nbsp;的核心抓手;保底公司则选择行业稳定、需求旺盛的企业,避免后期陷入&nbsp;“无&nbsp;offer&nbsp;可拿”&nbsp;的被动。同时用表格记录每家公司的岗位开放时间、投递状态、笔面试节点,确保不遗漏关键信息。再者,笔试面试提前练,拒绝&nbsp;“临时抱佛脚”。校招时间线拉长,不代表笔面试难度降低&nbsp;——&nbsp;恰恰相反,早开放的岗位往往对候选人的硬实力要求更高。建议你现在就启动刷题计划:技术岗重点刷&nbsp;********&nbsp;算法题、计算机基础知识点(操作系统、数据库、网络),每周至少完成&nbsp;10-15&nbsp;道题,同时模拟笔试场景训练答题速度;最后,保持心态稳定,接受&nbsp;“阶段性反馈”。拉长的校招周期容易让人陷入焦虑&nbsp;——&nbsp;可能你投递了几家公司却迟迟没回应,或是笔试通过后卡在面试环节。但请记住,校招是一个&nbsp;“逐步匹配”&nbsp;的过程,一次失利不代表能力不足,可能只是岗位适配度或时机问题。建议你每周花&nbsp;1-2&nbsp;小时复盘:简历是否需要优化?笔试错题是否掌握?面试中哪些问题回答得不好?及时调整策略,比盲目投递更有意义。同时不用过度关注他人进度,每个人的求职节奏不同,专注于自己的目标,稳步推进,自然会等到属于自己的&nbsp;offer。
今年春招是金一银二嘛?
点赞 评论 收藏
分享
评论
6
13
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务