"分布式系统就像餐厅管理,如何分配顾客、控制人流、生成订单号,都是学问!" 🍽️
🚦 限流算法
为什么需要限流?
场景:双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用令牌桶而不是漏桶?
(答案:令牌桶允许短时突发流量,更灵活!)