1. 分布式锁
1.1 难点
1.1.1 锁延期
同一时间内不允许多个客户端同时获得锁;
1.1.2 防止死锁
需要确保在任何故障场景下,都不会出现死锁;
1.2.3 可重入
特殊的锁机制,它允许同一个线程多次获取同一个锁而不会被阻塞。
1.2.4 高可用
同时,加锁、释放锁的过程性能开销应尽量低,并保证高可用,避免单点故障。
1.2 技术方案
1.2.1 基于Redis
- 技术要点
原理 | Redis的SETNX指令:SETNX(set if not exist)指令用于插入一个键值对。如果键已经存在,则返回False,否则插入成功并返回True。 |
锁延期 | Redis 实现分布式锁时,为了确保锁的安全性,引入了 watchdog(看门狗)机制。watchdog 的主要作用是定期重置锁的过期时间,从而避免锁意外过期被其他客户端获取。(在客户端启动) |
释放锁 | 同时,可以利用EXPIRE指令为键值对设置过期时间,防止锁忘记释放。 |
可重入 | Redis分布式锁实现可重入意味着同一个线程或进程可以多次获取同一个锁而不会被阻塞。这通常是通过在客户端维护一个锁计数器来实现的,每次获取锁时计数器加1,每次释放锁时计数器减1,只有当计数器为0时才真正释放锁。 |
高可用 | Redis的RedLock算法:尝试从多个相互独立的Redis实例获取锁,只有当从大多数实例上获取了锁,并且获取锁的时间小于锁的过期时间时,才认为锁获取成功。 |
- 技术实践
1.2.2 基于Zookeeper
- 技术要点
原理 | Zookeeper分布式锁:利用Zookeeper的顺序临时节点实现分布式锁和等待队列。 客户端尝试在ZooKeeper的一个特定路径下创建一个临时顺序节点。这个节点是临时的,意味着当客户端会话结束时,ZooKeeper会自动删除它。顺序节点则是ZooKeeper会为其分配一个唯一的、单调递增的序列号。 客户端检查它创建的节点是否是在特定路径下序号最小的节点。如果是,那么它获取到了锁。如果不是,它就需要等待,通常通过监视序号在它之前的节点的删除事件。 |
锁延期 | 为了保持锁,客户端必须保持与ZooKeeper的会话活动。ZooKeeper的会话有一个超时时间,如果在这个时间内客户端没有与ZooKeeper进行任何交互(如心跳),会话将过期,导致所有与该会话关联的临时节点被删除。因此,为了延期锁,客户端需要定期与ZooKeeper交互以重置会话的超时计时器。 |
释放锁 | 当客户端完成其任务并准备释放锁时,它只需删除它之前创建的临时节点。由于该节点是临时的,它的删除不会触发其他客户端的监视事件,除非有其他客户端正在等待成为下一个锁持有者。 如果客户端在持有锁期间崩溃或遇到其他问题,ZooKeeper将自动删除其临时节点(因为会话已过期),从而释放锁。其他等待的客户端将能够获取锁。 |
可重入 | 本地锁计数器: 重入逻辑: |
高可用 | Zookeeper设计的初衷就是为了实现分布式锁服务。 |
-
技术实践
Apache Curator是一个流行的ZooKeeper客户端库,它提供了高级的分布式锁实现,包括可重入锁。以下是一个使用Curator实现可重入锁的示例:
Curator is a keeper or custodian of a museum or other collection - A ZooKeeper Keeper.
Apache Curator is a Java/JVM client library for Apache ZooKeeper, a distributed coordination service. It includes a high level API framework and utilities to make using Apache ZooKeeper much easier and more reliable. It also includes recipes for common use cases and extensions such as service discovery and a Java 8 asynchronous DSL.
Welcome to Apache Curator | Apache Curator
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessReentrantLock;
public class ZooKeeperReentrantLockExample {
private final CuratorFramework client;
private final InterProcessReentrantLock lock;
public ZooKeeperReentrantLockExample(CuratorFramework client, String lockPath) {
this.client = client;
this.lock = new InterProcessReentrantLock(client, lockPath);
}
public void acquireLock() throws Exception {
lock.acquire(); // 如果当前线程已经持有锁,则重入
}
public boolean tryAcquireLock(long time, TimeUnit unit) throws Exception {
return lock.tryLock(time, unit); // 尝试获取锁,支持重入
}
public void releaseLock() throws Exception {
lock.release(); // 释放锁,如果本地锁计数器减至0,则删除ZooKeeper中的节点
}
}
2. 限流
2.1 难点
2.1.1 突发流量
在实际应用中,流量往往是不稳定的,存在高峰期和低谷期。如何根据流量的变化动态地调整限流策略,是限流算法需要解决的问题。此外,还需要考虑突发流量的情况,如突然出现的大量请求可能导致系统过载。
2.1.2 平滑
计数器限流算法是一种直观的限流方法,它通过累加在特定时间窗口内的请求数量,并在达到设定的阈值时执行限流操作。然而,这种方法存在一个显著的问题,那就是在时间窗口的末期,如果计数器接近或达到限流阈值,那么新到来的请求很可能会触发限流,即使这些请求在实际上并不会对系统造成过载。
观察请求量曲线图,计数器这种限流器会容易呈现出锯齿状而不是平滑的曲线。
2.2 方法
2.2.1 计数器
原理 | 计数器算法通过记录时间窗口内的请求数量来进行限流。当请求数量超过设定的阈值时,新的请求将被拒绝。 |
优点 | 简单直观,易于实现。 |
缺点 | 无法应对突发流量,因为一旦达到阈值,后续请求无论多少都会被拒绝。此外,随着流量增长,精度可能下降。 |
2.2.2 基于滑动窗口的计数器
原理 | 将时间窗口划分为多个小的时间段(格子),每个格子内有一个计数器。随着时间推移,窗口会滑动,抛弃最老的时间段并纳入新的时间段。限流基于整个窗口内的请求总数。 |
优点 | 相比于固定窗口计数器算法,滑动窗口能更好地应对突发流量,因为每个时间段都有独立的计数器。 |
缺点 | 实现相对复杂,需要维护多个计数器和时间窗口的状态。 |
2.2.3 令牌桶
原理 | 令牌桶中存放着一定数量的令牌,每个令牌代表一个请求权限。请求到达时,如果桶中有令牌则取走令牌并允许请求通过;否则请求被拒绝或等待。 |
优点 | 能够应对突发流量,因为桶中的令牌可以累积并在需要时快速发放。适用于需要灵活调整限流速率的场景。 |
缺点 | 如果令牌发放过快,可能导致系统过载;如果发放过慢,则可能浪费系统资源。 |
2.2.4 漏斗算法
原理 | 想象一个固定容量的桶,请求作为水流进入桶中。如果桶已满,新的请求(水流)会被丢弃或等待直到桶中有空间。 |
优点 | 能够平滑突发流量,确保系统的稳定性。对于下游系统来说,流量是恒定的,有助于处理请求。 |
缺点 | 可能导致延迟,因为即使系统有能力处理更多请求,漏桶也会限制流量的速率。 |
比较两者的特点,漏桶算法更侧重于强制限制数据的传输速率,确保系统接收的请求速率稳定。而令牌桶算法则在限制平均传输速率的同时,允许一定程度的突发传输,以适应流量变化的实际情况。
在适用场景上,漏斗算法更多地用于保护系统,防止因流量过大而崩溃。而令牌桶算法则更适用于那些需要应对突发流量的场景,比如在秒杀活动中,用户的请求速率不固定,令牌桶算法可以确保系统既能处理稳定的请求流,又能应对突发的请求高峰
2.3 方案
2.3.1 单机限流
2.3.1.1 Guava限流
- Guava RateLimiter———-限制时间窗口内的凭据速率
- 平滑突发限流(SmoothBurst)
- 平滑预热限流(SmoothWarningUp)
- 主要方法:RateLimiter.create()和limiter.acquire()
2.3.2 分布式限流
2.3.2.1 Ngnix+Lua
2.3.2.2 Redis+Lua
思路上就很粗暴!比如当前限流为10000QPS/s,直接将当前秒作为key,每一个请求到达的时候都将这个key自增,当一个请求将其自增到10000后,就拒绝访问!具体的实现见张开涛的《亿级流量网络架构核心技术》p75—-“分布式限流”。