系统:Windows 10 10.0_x64
内存:16G
CPU:I7-7700K
开发工具:IntelliJ IDEA 2019.1
maven:Apache Maven 3.6.0
java:jdk_1.8.0_201
springboot:2.0.7
mysql:5.7.21
mybatis:3.5.1
redis:Redis 3.2.100
rabbitmq:3.7.9
压测工具:Apache JMeter 5.1.1
模板引擎:thymeleaf-3.0.11
电商类的活动秒杀抢购,对我们来说,永远都是一个无法规避的问题。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要。
在开发高并发系统时有三把利器用来保护系统:缓存、降级、限流
- 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。
- 降级:降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
- 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
1.假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
2.桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
3.所有的请求在处理之前都需要拿到一个可用的令牌才会被处理,处理完业务逻辑之后,将令牌直接删除;
4.令牌桶有最低限额n,当桶中的令牌达到最低限额n时候,请求将不会被处理,以此保证足够的限流;
漏桶算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求。
-
漏斗有一个进水口 和 一个出水口,出水口以一定速率出水,并且有一个最大出水速率:
-
如果进水速率小于等于最大出水速率,那么,出水速率等于进水速率,此时,不会积水,如果进水速率大于最大出水速率,那么,漏斗以最大速率出水,此时,多余的水会积在漏斗中
Fixed-Window算法
这个算法的思路是比较简单的, 考虑一个固定长度的时间窗口,比如10s、1min、1h等, 在这个时间窗口内统计请求次数, 但请求次数超过阈值就触发限流。这个算法有两点第一时间窗口的长度是固定的并对应一个计数器, 第二每个时间窗口开始时计数器清零。
此项目模拟的是某次电商活动中一个商品列表的商品的秒杀场景。商品列表中有N种商品,每个商品都有自己的库存数目,每个商品的秒杀开始时间也不相同。每个用户只可以秒杀成功一次某种商品,不同的商品可以分别秒杀。
项目应对并发的业务详解
-
项目前端采用模板引擎将页面缓存在用户端通过接口动态的更新数据;
-
后端动态生成商品秒杀的路径,等到商品秒杀的时间获取秒杀路径的接口才会返回秒杀路径;
-
为了使接口在一瞬间接收很多的请求采用了验证码来控制一瞬间的请求过大的情况,秒杀接口会判断秒杀路径;
-
使用内存缓存来过滤不正确的商品id的请求和已经秒杀完毕的商品请求;
-
使用redis缓存来保存商品的库存避免商品出现超卖的情况;
-
用户秒杀成功的请求会进入消息队列排队,对应的业务会慢慢处理用户秒杀成功的请求;
-
在秒杀业务中会进行减库存的操作,还会判断用户是否有重复秒杀的请求和商品库存是否正确;
-
秒杀成功的请求产生的订单会保存一份到redis中以供秒杀成功后查询订单;
这里先在不同的情况下测试Tomcat的承受并发的能力
100个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 100 | 1 | 1 | 2 | 2 | 2 | 0 | 3 | 0.000% | 100.70493 | 7.18 | 13.57 |
TOTAL | 100 | 1 | 1 | 2 | 2 | 2 | 0 | 3 | 0.000% | 100.70493 | 7.18 | 13.57 |
1000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 1000 | 0 | 1 | 1 | 1 | 2 | 0 | 11 | 0.000% | 970.87379 | 69.21 | 130.84 |
TOTAL | 1000 | 0 | 1 | 1 | 1 | 2 | 0 | 11 | 0.000% | 970.87379 | 69.21 | 130.84 |
10000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 10000 | 379 | 102 | 1263 | 1546 | 2009 | 0 | 2035 | 0.000% | 2449.17952 | 174.60 | 330.07 |
TOTAL | 10000 | 379 | 102 | 1263 | 1546 | 2009 | 0 | 2035 | 0.000% | 2449.17952 | 174.60 | 330.07 |
结论:由于这个接口没有处理任务业务,请求完成后就返回,由此可见一个Tomcat的并发数还是很可观的!
100个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 100 | 2 | 2 | 3 | 4 | 20 | 1 | 30 | 0.000% | 100.80645 | 14.37 | 13.59 |
TOTAL | 100 | 2 | 2 | 3 | 4 | 20 | 1 | 30 | 0.000% | 100.80645 | 14.37 | 13.59 |
1000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 1000 | 1 | 1 | 2 | 2 | 5 | 0 | 12 | 0.000% | 968.05421 | 138.02 | 130.46 |
TOTAL | 1000 | 1 | 1 | 2 | 2 | 5 | 0 | 12 | 0.000% | 968.05421 | 138.02 | 130.46 |
10000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 10000 | 884 | 520 | 2094 | 2262 | 2517 | 0 | 2857 | 0.000% | 1999.20032 | 285.04 | 269.42 |
TOTAL | 10000 | 884 | 520 | 2094 | 2262 | 2517 | 0 | 2857 | 0.000% | 1999.20032 | 285.04 | 269.42 |
结论:从redis查询了一次数据后就返回和没有任务业务的对比起来差别并不是很明显,可以看出redis的查询处理速度是相当快的!
100个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 100 | 15 | 15 | 18 | 24 | 29 | 12 | 29 | 0.000% | 99.20635 | 33.13 | 14.24 |
TOTAL | 100 | 15 | 15 | 18 | 24 | 29 | 12 | 29 | 0.000% | 99.20635 | 33.13 | 14.24 |
1000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 1000 | 1102 | 972 | 2524 | 2753 | 2860 | 11 | 3062 | 0.000% | 243.84297 | 81.44 | 35.00 |
TOTAL | 1000 | 1102 | 972 | 2524 | 2753 | 2860 | 11 | 3062 | 0.000% | 243.84297 | 81.44 | 35.00 |
10000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 10000 | 22394 | 22029 | 41119 | 43453 | 45152 | 23 | 47803 | 0.000% | 202.72051 | 67.71 | 29.10 |
TOTAL | 10000 | 22394 | 22029 | 41119 | 43453 | 45152 | 23 | 47803 | 0.000% | 202.72051 | 67.71 | 29.10 |
结论:结果很明显使用当接口的并发大到一定量的时候mysql的查询速度就会是接口响应的瓶颈,mysql可以承受的最大吞吐量是200左右。
100个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 100 | 12 | 3 | 39 | 88 | 129 | 1 | 139 | 0.000% | 100.50251 | 22.00 | 23.46 |
TOTAL | 100 | 12 | 3 | 39 | 88 | 129 | 1 | 139 | 0.000% | 100.50251 | 22.00 | 23.46 |
1000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 1000 | 2 | 2 | 3 | 5 | 20 | 1 | 27 | 0.000% | 914.07678 | 202.63 | 213.34 |
TOTAL | 1000 | 2 | 2 | 3 | 5 | 20 | 1 | 27 | 0.000% | 914.07678 | 202.63 | 213.34 |
10000个线程,1秒钟,循环一次请求的结果如下
Label | 样本 | 平均值 | 中位数 | 90% 百分位 | 95% 百分位 | 99% 百分位 | 最小值 | 最大值 | 异常 % | 吞吐量 | 接收 KB/sec | 发送 KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP请求 | 10000 | 379 | 202 | 1004 | 1044 | 1102 | 0 | 1152 | 0.000% | 2363.50745 | 523.94 | 551.64 |
TOTAL | 10000 | 379 | 202 | 1004 | 1044 | 1102 | 0 | 1152 | 0.000% | 2363.50745 | 523.94 | 551.64 |
注意:在使用JMeter测试的时候(第一次使用10000个线程测试,第二次还是使用10000个线程测试)两次测试的时间间隔太小的话会出(java.net.BindException: Address already in use: connect )现异常,建议两次测试的时间间隔稍微大一些,至于出现异常的原因可能是Tomcat在第一次测试的时候新建了大量的线程,如果时间间隔太小JVM还没有完全回收这些资源所以会出现连接一直被占用的情况
总结:使用rabbitmq异步解耦和redis缓存的方式会大大改善接口承受并发的能力,保证接口应对大量的请求时能瞬间返回