如果某个接口可能出现突发情况,比如“秒杀”活动,那么很有可能因为突然爆发的访问量造成系统奔溃,我们需要最这样的接口进行限流。
在上一篇“限流算法”中,我们简单提到了两种限流方式:
1)(令牌桶、漏桶算法)限速率,例如:每 5r/1s = 1r/200ms 即一个请求以200毫秒的速率来执行;
2)(计数器方式)限制总数、或者单位时间内的总数,例如:设定总并发数的阀值,单位时间总并发数的阀值。
一、限制总并发数
我们可以采用java提供的atomicLong类来实现
atomicLong在java.util.concurrent.atomic包下,它直接继承于number类,它是线程安全的。
我们将使用它来计数
public class AtomicDemo {
// 计数
public static AtomicLong atomicLong = new AtomicLong(0L);
// 最大请求数量
static int limit = 10;
// 请求数量
static int reqAmonut = 15;
public static void main(String[] args) throws InterruptedException {
// 多线程并发模拟
final CountDownLatch latch = new CountDownLatch(1);
for (int i = 1; i <= reqAmonut; i++) {
final int t = i;
new Thread(new Runnable() {
public void run() {
try {
latch.await();
// 计数器加1,并判断最大请求数量
if (atomicLong.getAndIncrement() > limit) {
System.out.println(t + "线程:限流了");
return;
}
System.out.println(t + "线程:业务处理");
// 休眠1秒钟,模拟业务处理
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 计数器减1
atomicLong.decrementAndGet();
}
}
}).start();
}
latch.countDown();
}
}
二、限制单位时间的总并发数
下面用谷歌的Guava依赖中的Cache(线程安全)来完成单位时间的并发数限制,
Guava需要引入依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
具体逻辑如下:
1)根据当前的的时间戳(秒)做key,请求计数的值做value;
2)每个请求都通过时间戳来获取计数值,并判断是否超过限制。(即,1秒内的请求数量是否超过阀值)
代码如下:
public class AtomicDemo2 {
// 计数
public static AtomicLong atomicLong = new AtomicLong(0L);
// 最大请求数量
static int limit = 10;
// 请求数量
static int reqAmonut = 15;
public static void main(String[] args) throws InterruptedException {
// Guava的Cache来存储计数器
final LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>(){
@Override
public AtomicLong load(Long key) throws Exception {
return new AtomicLong(0L);
}
});
// 多线程并发模拟
final CountDownLatch latch = new CountDownLatch(1);
for (int i = 1; i <= reqAmonut; i++) {
final int t = i;
new Thread(new Runnable() {
public void run() {
try {
latch.await();
long currentSeconds = System.currentTimeMillis()/1000;
// 从缓存中取值,并计数器+1
if (counter.get(currentSeconds).getAndIncrement() > limit) {
System.out.println(t + "线程:限流了");
return;
}
System.out.println(t + "线程:业务处理");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}).start();
}
latch.countDown();
}
}
三、限制接口的速率
以上两种以较为简单的计数器方式实现了限流,但是他们都只是限制了总数。也就是说,它们允许瞬间爆发的请求达到最大值,这有可能导致一些问题。
下面我们将使用Guava的 RateLimiter提供的令牌桶算法来实现限制速率,例如:1r/200ms
同样需要引入依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
示例代码:
public class GuavaDemo {
// 每秒钟5个令牌
static RateLimiter limiter = RateLimiter.create(5);
public static void main(String[] args) throws InterruptedException {
final RateLimiter limiter2 = RateLimiter.create(5);
for (int i = 0; i < 20; i++) {
System.out.println(i + "-" + limiter2.acquire());
}
}
}
说明:
1)RateLimiter.create(5)表示创建一个容量为5的令牌桶,并且每秒钟新增5个令牌,也就是每200毫秒新增1个令牌;
2)limiter2.acquire() 表示消费一个令牌,如果桶里面没有足够的令牌那么就进入等待。
输出:
0.0
0.197729
0.192975
...
平均 1r/200ms的速率处理请求
RateLimiter允许突发超额,例如:
public class GuavaDemo {
// 每秒钟5个令牌
static RateLimiter limiter = RateLimiter.create(5);
public static void main(String[] args) throws InterruptedException {
final RateLimiter limiter2 = RateLimiter.create(5);
System.out.println(limiter2.acquire(10));
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
}
}
输出:
0.0
1.997777
0.194835
0.198466
0.195192
0.197448
0.196706
我们看到:
limiter2.acquire(10)
超额消费了10个令牌,而下一个消费需要等待超额消费的时间,所以等待了近2秒钟的时间,而后又开始匀速处理请求
由于上面的方式允许突发,很多人可能担心这种突发对于系统来说如果扛不住可能就造成崩溃。那针对这种情况,大家希望能够从慢速到匀速地平滑过渡。Guava当然也提供了这样的实现:
public class GuavaDemo {
// 每秒钟5个令牌
static RateLimiter limiter = RateLimiter.create(5);
public static void main(String[] args) throws InterruptedException {
final RateLimiter limiter2 = RateLimiter.create(5, 1, TimeUnit.SECONDS);
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
System.out.println(limiter2.acquire());
}
}
输出:
0.0
0.51798
0.353722
0.216954
0.195776
0.194903
0.194547
我们看到,速率从0.5慢慢地趋于0.2,平滑地过渡到了匀速状态。
RateLimter 还提供了tryAcquire()方法来判断是否有够的令牌,并即时返回结果,如:
public class GuavaDemo {
public static void main(String[] args) throws InterruptedException {
final RateLimiter limiter = RateLimiter.create(5, 1, TimeUnit.SECONDS);
for (int i = 0; i < 10; i++) {
if (limiter.tryAcquire()) {
System.out.println("处理业务");
}else{
System.out.println("限流了");
}
}
}
}
输出:
处理业务
限流了
限流了
限流了
限流了
限流了
限流了
限流了
限流了
限流了
以上,就是单实例系统的应用级接口限流方式。
参考:
http://jinnianshilongnian.iteye.com/blog/2305117