🌐 分布式算法:限流、负载均衡、分布式ID,实战必备!

"分布式系统就像餐厅管理,如何分配顾客、控制人流、生成订单号,都是学问!" 🍽️


🚦 限流算法

为什么需要限流?

场景:双11抢购,瞬间10万人涌入,服务器扛不住!😱

解决方案:限流!控制流量,保护服务器!


1. 固定窗口算法

原理:每个时间窗口允许固定数量的请求

scss 复制代码
时间窗口:1秒
限制:100个请求

0-1秒:✅✅✅...✅ (100个)
1-2秒:✅✅✅...✅ (100个)
2-3秒:✅✅✅...✅ (100个)

代码实现

java 复制代码
public class FixedWindowRateLimiter {
    private int limit;           // 时间窗口内最大请求数
    private long windowSize;     // 时间窗口大小(毫秒)
    private AtomicInteger count; // 当前窗口的请求计数
    private long windowStart;    // 当前窗口的开始时间
    
    public FixedWindowRateLimiter(int limit, long windowSize) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.count = new AtomicInteger(0);
        this.windowStart = System.currentTimeMillis();
    }
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 判断是否进入新的时间窗口
        if (now - windowStart >= windowSize) {
            windowStart = now;
            count.set(0);
        }
        
        // 判断是否超过限制
        if (count.get() < limit) {
            count.incrementAndGet();
            return true;
        }
        
        return false;  // 拒绝请求
    }
}

问题:边界问题(临界突刺)

makefile 复制代码
09:59:59 → 100个请求 ✅
10:00:00 → 100个请求 ✅

1秒内200个请求!超过限制!😱

2. 滑动窗口算法 ⭐⭐⭐

原理:时间窗口滑动,更精确

复制代码
固定窗口:
├────┼────┼────┤
0s   1s   2s   3s

滑动窗口:
├──┼──┼──┼──┼──┤
0  0.5 1  1.5 2

窗口随时间滑动,更平滑!

代码实现

java 复制代码
public class SlidingWindowRateLimiter {
    private int limit;
    private long windowSize;
    private LinkedList<Long> timestamps;  // 记录请求时间戳
    
    public SlidingWindowRateLimiter(int limit, long windowSize) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.timestamps = new LinkedList<>();
    }
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 移除过期的时间戳
        while (!timestamps.isEmpty() && now - timestamps.peekFirst() >= windowSize) {
            timestamps.pollFirst();
        }
        
        // 判断是否超过限制
        if (timestamps.size() < limit) {
            timestamps.addLast(now);
            return true;
        }
        
        return false;
    }
}

3. 漏桶算法(Leaky Bucket)⭐⭐⭐

原理:水(请求)流入桶,桶以固定速率漏水(处理请求)

markdown 复制代码
         ↓↓↓ 请求涌入
      ┌─────────┐
      │  ████   │ ← 桶(队列)
      │  ████   │
      │  ██     │
      └────┬────┘
           ↓
        固定速率流出

特点

  • ✅ 平滑流量
  • ✅ 处理突发流量
  • ❌ 不能应对突发需求(速率固定)

代码实现

java 复制代码
public class LeakyBucketRateLimiter {
    private int capacity;        // 桶的容量
    private int rate;            // 漏出速率(每秒)
    private int water;           // 当前水量
    private long lastLeakTime;   // 上次漏水时间
    
    public LeakyBucketRateLimiter(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastLeakTime = System.currentTimeMillis();
    }
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 计算漏掉的水
        long leaked = (now - lastLeakTime) / 1000 * rate;
        water = Math.max(0, water - (int) leaked);
        lastLeakTime = now;
        
        // 判断桶是否已满
        if (water < capacity) {
            water++;
            return true;
        }
        
        return false;  // 桶满,拒绝请求
    }
}

4. 令牌桶算法(Token Bucket)⭐⭐⭐⭐⭐

原理:以固定速率生成令牌,请求需要获取令牌

markdown 复制代码
       固定速率生成令牌
            ↓
      ┌─────────┐
      │ 🪙🪙🪙  │ ← 令牌桶
      │ 🪙🪙    │
      └─────────┘
            ↑
      请求拿令牌(有令牌就放行)

特点

  • ✅ 允许突发流量(桶内有多个令牌)
  • ✅ 平滑限流
  • Guava RateLimiter使用令牌桶

代码实现

java 复制代码
public class TokenBucketRateLimiter {
    private int capacity;        // 桶的容量
    private int tokensPerSecond; // 每秒生成的令牌数
    private int tokens;          // 当前令牌数
    private long lastRefillTime; // 上次补充令牌时间
    
    public TokenBucketRateLimiter(int capacity, int tokensPerSecond) {
        this.capacity = capacity;
        this.tokensPerSecond = tokensPerSecond;
        this.tokens = capacity;
        this.lastRefillTime = System.currentTimeMillis();
    }
    
    public synchronized boolean tryAcquire() {
        refill();
        
        if (tokens > 0) {
            tokens--;
            return true;
        }
        
        return false;
    }
    
    private void refill() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastRefillTime;
        
        // 计算新生成的令牌
        int newTokens = (int) (elapsedTime / 1000 * tokensPerSecond);
        
        if (newTokens > 0) {
            tokens = Math.min(capacity, tokens + newTokens);
            lastRefillTime = now;
        }
    }
}

Guava RateLimiter

java 复制代码
import com.google.common.util.concurrent.RateLimiter;

// 创建限流器:每秒2个请求
RateLimiter limiter = RateLimiter.create(2.0);

// 获取令牌(阻塞等待)
limiter.acquire();  // 获取1个令牌
limiter.acquire(5); // 获取5个令牌

// 尝试获取令牌(不阻塞)
if (limiter.tryAcquire()) {
    // 处理请求
}

⚖️ 负载均衡算法

为什么需要负载均衡?

场景:3台服务器,如何分配请求?

css 复制代码
客户端请求 → [负载均衡器] → 服务器1
                    ↓       服务器2
                            服务器3

1. 轮询(Round Robin)

原理:依次分配请求

erlang 复制代码
请求1 → 服务器1
请求2 → 服务器2
请求3 → 服务器3
请求4 → 服务器1(循环)
...

代码实现

java 复制代码
public class RoundRobinLoadBalancer {
    private List<String> servers;
    private AtomicInteger index;
    
    public RoundRobinLoadBalancer(List<String> servers) {
        this.servers = servers;
        this.index = new AtomicInteger(0);
    }
    
    public String getServer() {
        int i = index.getAndIncrement() % servers.size();
        return servers.get(i);
    }
}

优点:简单、公平

缺点:不考虑服务器性能差异


2. 加权轮询(Weighted Round Robin)⭐⭐⭐

原理:性能好的服务器分配更多请求

复制代码
服务器1:权重5
服务器2:权重3
服务器3:权重2

分配序列:1,1,1,1,1,2,2,2,3,3 (循环)

代码实现

java 复制代码
public class WeightedRoundRobinLoadBalancer {
    private List<Server> servers;
    private int currentIndex;
    private int currentWeight;
    private int maxWeight;
    private int gcdWeight;
    
    static class Server {
        String address;
        int weight;
        
        Server(String address, int weight) {
            this.address = address;
            this.weight = weight;
        }
    }
    
    public String getServer() {
        while (true) {
            currentIndex = (currentIndex + 1) % servers.size();
            
            if (currentIndex == 0) {
                currentWeight -= gcdWeight;
                if (currentWeight <= 0) {
                    currentWeight = maxWeight;
                }
            }
            
            if (servers.get(currentIndex).weight >= currentWeight) {
                return servers.get(currentIndex).address;
            }
        }
    }
}

3. 随机(Random)

原理:随机选择服务器

java 复制代码
public class RandomLoadBalancer {
    private List<String> servers;
    private Random random;
    
    public RandomLoadBalancer(List<String> servers) {
        this.servers = servers;
        this.random = new Random();
    }
    
    public String getServer() {
        int index = random.nextInt(servers.size());
        return servers.get(index);
    }
}

4. 加权随机(Weighted Random)

java 复制代码
public class WeightedRandomLoadBalancer {
    private List<Server> servers;
    private int totalWeight;
    
    public String getServer() {
        int randomWeight = new Random().nextInt(totalWeight);
        int currentWeight = 0;
        
        for (Server server : servers) {
            currentWeight += server.weight;
            if (randomWeight < currentWeight) {
                return server.address;
            }
        }
        
        return servers.get(0).address;
    }
}

5. 最少连接(Least Connections)⭐⭐⭐

原理:选择当前连接数最少的服务器

复制代码
服务器1:3个连接
服务器2:1个连接 ← 选这个!
服务器3:5个连接

新请求 → 服务器2

代码实现

java 复制代码
public class LeastConnectionsLoadBalancer {
    private Map<String, AtomicInteger> connectionCounts;
    
    public String getServer() {
        return connectionCounts.entrySet().stream()
            .min(Comparator.comparingInt(e -> e.getValue().get()))
            .map(Map.Entry::getKey)
            .orElse(null);
    }
    
    public void addConnection(String server) {
        connectionCounts.get(server).incrementAndGet();
    }
    
    public void removeConnection(String server) {
        connectionCounts.get(server).decrementAndGet();
    }
}

6. 一致性哈希(Consistent Hashing)⭐⭐⭐⭐⭐

原理:把服务器和数据映射到同一个环上

详见第6章哈希表文档!

应用场景

  • 分布式缓存(Redis Cluster)
  • 数据分片
  • 负载均衡

🆔 分布式ID生成算法

为什么需要分布式ID?

单机:数据库自增ID

分布式:多台服务器,如何保证ID唯一且有序?


1. 雪花算法(Snowflake)⭐⭐⭐⭐⭐

原理:64位long类型ID

markdown 复制代码
64位ID结构:

| 1位符号 | 41位时间戳 | 10位机器ID | 12位序列号 |
    ↓          ↓           ↓           ↓
   固定0   毫秒级时间   数据中心+机器  同一毫秒的序列

例子:
0 | 0001111011011... | 0000000001 | 000000000001

优点:
✅ 趋势递增
✅ 不依赖数据库
✅ 高性能(单机每秒409.6万个ID)

代码实现

java 复制代码
public class SnowflakeIdGenerator {
    // 起始时间戳(2020-01-01)
    private final long START_TIMESTAMP = 1577836800000L;
    
    // 各部分位数
    private final long SEQUENCE_BITS = 12;      // 序列号位数
    private final long MACHINE_BITS = 5;        // 机器ID位数
    private final long DATACENTER_BITS = 5;     // 数据中心位数
    
    // 最大值
    private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
    private final long MAX_MACHINE = ~(-1L << MACHINE_BITS);
    private final long MAX_DATACENTER = ~(-1L << DATACENTER_BITS);
    
    // 左移位数
    private final long MACHINE_SHIFT = SEQUENCE_BITS;
    private final long DATACENTER_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
    private final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS + DATACENTER_BITS;
    
    private long datacenterId;  // 数据中心ID
    private long machineId;     // 机器ID
    private long sequence = 0L; // 序列号
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId越界");
        }
        if (machineId > MAX_MACHINE || machineId < 0) {
            throw new IllegalArgumentException("machineId越界");
        }
        
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        // 时钟回拨检测
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,拒绝生成ID");
        }
        
        // 同一毫秒内,序列号自增
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            
            // 序列号溢出,等待下一毫秒
            if (sequence == 0) {
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒,序列号归0
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        // 组装ID
        return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
            | (datacenterId << DATACENTER_SHIFT)
            | (machineId << MACHINE_SHIFT)
            | sequence;
    }
    
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

// 使用
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
long id = generator.nextId();  // 生成ID

优缺点

优点 缺点
✅ 高性能 ❌ 依赖机器时钟
✅ 趋势递增 ❌ 时钟回拨问题
✅ 不依赖DB ❌ 机器ID需要配置

2. UUID

优点:简单、全局唯一

缺点

  • ❌ 无序(不适合做主键)
  • ❌ 字符串,占用空间大
java 复制代码
String uuid = UUID.randomUUID().toString();
// 例:550e8400-e29b-41d4-a716-446655440000

3. 数据库自增

优点:简单

缺点

  • ❌ 依赖数据库
  • ❌ 单点故障
  • ❌ 性能瓶颈

4. Redis自增

java 复制代码
public long generateId() {
    return redisTemplate.opsForValue().increment("id_generator");
}

优点:简单、性能好

缺点:依赖Redis


📝 总结

限流算法对比

算法 优点 缺点 适用场景
固定窗口 简单 边界突刺 简单场景
滑动窗口 精确 内存占用 精确限流
漏桶 流量平滑 不能突发 稳定流量
令牌桶 允许突发 实现复杂 🏆 推荐

负载均衡算法对比

算法 优点 缺点 适用场景
轮询 简单公平 不考虑性能 服务器性能一致
加权轮询 考虑性能 配置复杂 🏆 推荐
随机 简单 不够均衡 简单场景
最少连接 动态平衡 需要统计 长连接
一致性哈希 扩展性好 实现复杂 分布式缓存

恭喜你!🎉 你已经掌握了分布式系统中的核心算法!

这些都是实际工作中最常用的!💪


📌 重点记忆:令牌桶、加权轮询、雪花算法

🤔 思考题:为什么Guava RateLimiter用令牌桶而不是漏桶?

(答案:令牌桶允许短时突发流量,更灵活!)

相关推荐
golang学习记3 小时前
Spring Boot 4.0官宣: 弃用 Undertow:Tomcat笑麻了
后端
林太白3 小时前
rust-Serialize序列和反序列Deserialize
后端·rust
后端小张3 小时前
【JAVA 进阶】穿越之我在修仙世界学习 @Async 注解(深度解析)
java·开发语言·spring boot·后端·spring·注解·原理
Yeats_Liao3 小时前
Go Web 编程快速入门 18 - 附录B:查询与扫描
开发语言·前端·后端·golang
国服第二切图仔3 小时前
Rust实战开发之图形界面开发入门(egui crate)
开发语言·后端·rust
程序员爱钓鱼3 小时前
Python编程实战:文件读写(文本/二进制)详解与实战
后端·python·ipython
Zhangzy@3 小时前
Rust 依赖管理与版本控制
开发语言·后端·rust
程序员爱钓鱼3 小时前
Python编程实战:try...except...finally —— 让程序更稳健的异常处理机制
后端·python
间彧4 小时前
CAP定理:Partition tolerance(分区容错性)详解
后端