在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。
服务降级:在高并发的情况, 防止用户一直等待,直接返回一个友好的错误提示给客户端。
服务熔断:在高并发的情况,一旦达到服务最大的承受极限,直接拒绝访问,使用服务降级。
服务隔离:
服务限流:在高并发的情况,一旦服务承受不了,使用服务限流机制
常见的限流算法有:令牌桶、漏桶。计数器也可以简单粗暴地实现限流。
它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,3秒钟内的请求不能超过3次。
我们可以在开始时设置一个计数器,每次请求,该计数器+1;
如果该计数器的值大于3并且与第一次请求的时间间隔在3秒钟内,那么说明请求过多,应该限流;
如果该请求与第一次请求的时间间隔大于3秒钟,并且该计数器的值还在限流范围内,那么重置该计数器。
/**
* 计数器限流
*/
public class LimitCountDemo {
private int limtCount = 3;// 限制最大访问的容量
AtomicInteger atomicInteger = new AtomicInteger(0); // 每秒钟 实际请求的数量
private boolean isReset = false;
private long start = System.currentTimeMillis();// 获取当前系统时间
private int interval = 3000;// 间隔时间
private final ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); // 线程池
private static final Logger logger = LoggerFactory.getLogger(LimitCountDemo.class);
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
private synchronized boolean acquire() {
long newTime = System.currentTimeMillis();
if (newTime > (start + interval) && !isReset) {
isReset = true;
// 判断是否是一个周期
start = newTime;
logger.info("start=" + sdf.format(new Date(start)));
atomicInteger.set(0); // 清理为0
}
if (isReset) {
isReset = false;
}
int i = atomicInteger.incrementAndGet();// i++;
logger.info("atomicInteger=" + i);
return i <= limtCount;
}
private void access() {
for (int i = 1; i <= 6; i++) {
final int tempI = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
if (acquire()) {
logger.info("你没有被限流,可以正常访问逻辑 i:" + tempI);
} else {
logger.info("你已经被限流呢 i:" + tempI);
}
}
});
}
}
public static void main(String[] args) throws InterruptedException {
LimitCountDemo limitService = new LimitCountDemo();
logger.info("start=" + sdf.format(new Date(limitService.start)));
limitService.access();
Thread.sleep(3100);
System.out.println("==================");
limitService.access();
Thread.sleep(3100);
System.out.println("==================");
limitService.access();
limitService.newCachedThreadPool.shutdown();
}
}
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。描述如下:
RateLimiter 是 guava 提供的基于令牌桶算法的实现类,可以非常简单地实现服务限流,并且根据系统的实际情况来调整生成 token 的速率。
通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。
下面来看个简单的 RateLimiter 使用 Demo。
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>25.1-jreversion>
dependency>
/**
* 使用RateLimiter 实现令牌桶算法
*/
@RestController
public class RateLimiterDemo {
private static final Logger logger = LoggerFactory.getLogger(RateLimiterDemo.class);
// 1.0 表示 每秒中生成1个令牌存放在桶中
private RateLimiter rateLimiter = RateLimiter.create(1); // 独立线程
// 下单请求
@RequestMapping("/addOrder")
public String addOrder() {
// 1.限流判断
// 如果在500毫秒内如果没有获取到令牌的话,则直接走服务降级处理
boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
logger.info("别抢了, 在抢也是一直等待的, 还是放弃吧!!!");
return "别抢了, 在抢也是一直等待的, 还是放弃吧!!!";
} else {
// 2. 业务逻辑处理
// 3. 返回结果
logger.info("恭喜您,抢购成功! ");
return "恭喜您,抢购成功!";
}
}
}
漏桶算法的描述如下:
主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。
Guava 的 RateLimiter 有两种模式:
SmoothBursty
,令牌生成的速度恒定。SmoothWarmingUp
,令牌生成速度缓慢提升直到维持在一个稳定值。比如在系统刚启动的时候,第一次访问的接口需要加载缓存等等,这时系统的处理速度较慢,就需要这种模式。这里主要讲 SmoothBursty
的源码。需要注意的是,RateLimiter 的实现是可以预支消费令牌,比如当桶里有一个令牌的时候,第一次请求可以获取1个令牌或者100个令牌都不需要等待,但是下一次请求就需要等待到至少剩余一个令牌才可以消费。
首先来看下它的几个属性
/** The currently stored permits. */
/** 当前存储的令牌数 */
double storedPermits;
/** The maximum number of stored permits. */
/** 最大可存储令牌数 */
double maxPermits;
/**
* The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
* per second has a stable interval of 200ms.
* 产生一个令牌的时间间隔
*/
double stableIntervalMicros;
/**
* The time when the next request (no matter its size) will be granted. After granting a request,
* this is pushed further in the future. Large requests push this further than small requests.
* 下次可以拿到令牌的时间
*/
private long nextFreeTicketMicros = 0L; // could be either in the past or future
/** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
/** 限流器空闲时,保存多少秒产生的令牌器 */
final double maxBurstSeconds;
调用 create
方法时,会执行下面几个步骤:
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
@VisibleForTesting
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = maxPermits;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
根据令牌桶算法,桶中的令牌是持续生成存放的,如果开启一个定时任务来生成令牌,这将会消耗大量的资源。而这里是通过延迟计算,resync
方法调用时,当前时间如果晚于下次可以拿到令牌的时间,则更新存储的令牌数和下次可以拿到令牌的时间。
/** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
tryAcquire
方法的功能是在指定等待的时间内获取指定数量的令牌,如果不能获取到,则直接返回 false
,如果能获取,则阻塞等待,然后再返回 true
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
long microsToWait;
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
canAcquire
返回指定的时间内,能否获取令牌
/**
* 下次可以拿到令牌的时间 <= 当前时间 + 等待的时间
*/
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}
获取需要等待的时间
/**
* Reserves next ticket and returns the wait time that the caller must wait for.
* @return the required wait time, never negative
*/
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
// 需要等待的时间 = 下次可以拿到令牌的时间 - 当前时间
return max(momentAvailable - nowMicros, 0);
}
返回下次可以拿到令牌的时间点:
/**
* Reserves the requested number of permits and returns the time that those permits can be used
* (with one caveat).
*
* @return the time that the permits may be used, or, if the permits may be used immediately, an
* arbitrary past or present time
*/
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}