作者:石某俊 | 来源:互联网 | 2023-08-14 09:35
上周公司里发生了一件怪事,就是我们自己系统的注册接口被人恶意频繁访问最后导致该服务不可用,该注册接口是输入电话号码然后获取验证码注册,有人用遍历的方法无限重试验证码,最终服务没抗住挂掉了。更怪的是查到这个人的ip竟然是自己内部的公网ip,大概是有人闲的无聊了在搞怪,没办法,又不能封了ip,那样大家都访问不了了。
So,今天有空研究了一下关于如何解决api接口高并发的问题,在此记录一下。
1、通过控制并发数量来实现
信号量:这应该是大学操作系统课本里的概念,它是用在多进程和多任务之间的同步的,它就像十字路口的红绿灯,保证这个路口四条路的畅通行驶。
java中也有这个概念,Java并发库的Semaphore可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
写一个测试接口如下:
private final Semaphore permit = new Semaphore(10, true);
@PostMapping("/test")public String test(){try {permit.acquire();log.info("处理请求===============>");Thread.sleep(2000);}catch (Exception e){log.error("error");}finally {permit.release();}return "success";}
以上是我写了一个测试接口,这个接口最多允许同时10个并发量,超过10个则等待。
接下来我们用压力测试工具测一下这个接口,这个是我用的测试工具:
http://coolaf.com//tool/post 这是在线的,不能修改并发数,里面有本地版本,下载解压就能用。
下载解压后,在工具上输入url和并发量
![](https://img8.php1.cn/3cdc5/188e2/61b/4b3e93985e89ad04.jpeg)
这里设置并发量为50,也就是同时50个客户端访问该接口,点击开始测试,查看日志
![](https://img8.php1.cn/3cdc5/188e2/61b/83c61fa8a81b5f0f.jpeg)
我们可以看到,每次只有10个请求进入接口,然后过2秒后再来10个请求,效果很明显,我们想增大访问量只需要修改初始的信号量的数量即可。
2、通过控制访问速率来实现
这种方式采用令牌桶算法来实现,我们以一个恒定的速率向一个桶内放令牌,每次请求来的时候去桶里拿令牌,如果拿到了就继续后面的操作,如果没有拿到则等待。
在我们的工程实践中,通常使用Google开源工具包Guava提供的限流工具类RateLimiter来实现控制速率,该类基于令牌桶算法来完成限流,非常易于使用,而且非常高效。如我们不希望每秒的任务提交超过1个。
public static void main(String[] args) {String start &#61; new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());RateLimiter limiter &#61; RateLimiter.create(1.0); // 这里的1表示每秒允许处理的量为1个for (int i &#61; 1; i <&#61; 10; i&#43;&#43;) {double waitTime &#61; limiter.acquire(i);// 请求RateLimiter, 超过permits会被阻塞System.out.println("cutTime&#61;" &#43; System.currentTimeMillis() &#43; " call execute:" &#43; i &#43; " waitTime:" &#43; waitTime);}String end &#61; new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());System.out.println("start time:" &#43; start);System.out.println("end time:" &#43; end);}
RateLimiter limiter &#61; RateLimiter.create(1.0) 创建一个限流器&#xff0c;每秒生成1个令牌&#xff1b;
limiter.acquire(i) 以阻塞的方式获取令牌&#xff0c;随着i的增加&#xff0c;需要的令牌数增多&#xff0c;则需要等待的时间也增加。
查看输出结果&#xff1a;
![](https://img8.php1.cn/3cdc5/188e2/61b/36a45f71c35fd9b6.jpeg)
可以看出&#xff0c;当 i&#61;6时&#xff0c;等待时间差不多为i-1&#61;5秒&#xff0c;在这里是因为RateLimiter支持预消费&#xff0c;来支持一定程度的突发情况。
开可以用tryAcquire(int permits, long timeout, TimeUnit unit)
来设置等待超时时间的方式获取令牌&#xff0c;当等待超过了超时时长则立马返回。
令牌的生成策略有两种&#xff0c;一种是稳定模式&#xff08;SmoothBursty 模式&#xff09;&#xff0c;一种为渐进模式&#xff08;SmoothWarmingUp模式&#xff09;。
SmoothBursty 模式&#xff1a;RateLimiter limiter &#61; RateLimiter.create(5);
RateLimiter.create(5)表示桶容量为5且每秒新增5个令牌&#xff0c;即每隔200毫秒新增一个令牌&#xff1b;limiter.acquire()表示消费一个令牌&#xff0c;如果当前桶中有足够令牌则成功&#xff08;返回值为0&#xff09;&#xff0c;如果桶中没有令牌则暂停一段时间&#xff0c;比如发令牌间隔是200毫秒&#xff0c;则等待200毫秒后再去消费令牌&#xff0c;这种实现将突发请求速率平均为了固定请求速率。
SmoothWarmingUp模式&#xff1a;RateLimiter limiter &#61; RateLimiter.create(5,1000, TimeUnit.MILLISECONDS);
创建方式&#xff1a;RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)&#xff0c;permitsPerSecond表示每秒新增的令牌数&#xff0c;warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。速率是梯形上升速率的&#xff0c;也就是说冷启动时会以一个比较大的速率慢慢到平均速率&#xff1b;然后趋于平均速率&#xff08;梯形下降到平均速率&#xff09;。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。
写一个测试接口测试
private final RateLimiter limiter &#61; RateLimiter.create(5.0);&#64;PostMapping("/test2")public String test2(){try {boolean flag &#61; limiter.tryAcquire(1,3, TimeUnit.SECONDS);if (flag){log.info("处理请求&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;>");Thread.sleep(1000);}else {return "系统繁忙&#xff01;";}}catch (Exception e){log.error("error");}return "success";}
每秒产生5个令牌&#xff0c;请求进来每次获取一个&#xff0c;然后超时时长是3秒&#xff0c;还是设置50的并发量&#xff1a;
![](https://img8.php1.cn/3cdc5/188e2/61b/cabb7b878c0d5757.jpeg)
查看输出结果&#xff1a;
![](https://img8.php1.cn/3cdc5/188e2/61b/d47d68441861cec8.jpeg)
可以看到&#xff0c;一开始预消费进来了8个请求&#xff0c;随后的时间里每次进来5个请求&#xff0c;在这之间有超时3秒没有获取到令牌的都返回超时了。返回结果如下&#xff1a;
![](https://img8.php1.cn/3cdc5/188e2/61b/f4d8e17673224d8a.jpeg)
至此&#xff0c;我所了解的解决接口并发调用的方法就是这些&#xff0c;欢迎大佬指出错误和不足。