秒杀项目开发优化过程记录

单台虚拟机(mysql和Java程序运行在同一个服务器上)

对于springboot 2.1.16,Tomcat的默认配置如下:

  • server.tomcat.accept-count:等待队列长度,默认100
    
  • server.tomcat.max-connections:最大连接数,默认8192
  • server.tomcat.max-threads:最大工作线程数,默认200
  • server.tomcat.min-spare-threads:最小工作线程数,默认10

由此,可以看出默认配置下,连接超过8192后会出现拒绝连接情况,触发的请求超过200(最大工作线程数)+100(最大等待队列长度)后拒绝处理。

据说,对于4核8G的服务器,最大工作线程数的最优经验值在800,那对于我这个1核2G的虚拟机还不是200线程就够用了。。。

未改变Tomcat默认配置的情况下,Jmeter使用1000个线程循环50次来测试/item/getItem?id=4接口,结果如下:

具体就是平均时延465ms,吞吐量(QPS??)为1226.0每秒。好吧,重新用默认配置又测了一次,性能表现下降很多(无法理解,玄学。。。),就以这个为标准吧,因为后面修改配置的结果比这个好。。。具体就是平均时延1109ms,吞吐量为687.1每秒。

很奇怪,最大等待队列长度设置为1000,然后最小工作线程数设置为100后,压测的结果反而变差了(对比第二次测是变好的),结果如下: 

第一次测试是平均时延741ms,吞吐量是897.0每秒。第二次测试是平均时延965ms,吞吐量是758.3每秒。感觉这东西就很玄学,每次测都不太一样。而且,这个修改配置好像提升并不大,可能springboot对于Tomcat的默认配置就是挺适合1核2G的机器的吧。。。

4台虚拟机(一台运行nginx,两台运行Java应用程序,一台运行mysql数据库和redis)

 首先是nginx负责获取静态资源和反向代理Java程序,数据库服务器只运行mysql,还没有运行redis,压测一下性能(压测参数与之前相同):

具体就是平均时延593ms,吞吐量是983.0每秒,的确提升了一点,但是没有到达两倍(原因后面会分析)。

而且,这里出现了异常的请求,就是返回了错误的请求。我自己看了下,发现Tomcat服务器并没有报错,只有nginx服务器报错,而且错误都是nginx socket() failed (24: Too many open files)这种。网上查了一下,感觉应该是linux的单个进程打开的文件句柄数的限制导致的。

改为打开文件数量限制后,又出现了nginx的worker_connections are not enough的错误。。。nginx的配置文件也相应的改改吧。

把允许单个进程打开的文件数修改为102400和nginx的work_connections修改为2048后,压测结果:

具体就是平均时延325ms,吞吐量是1576.7每秒,异常返回的请求没有了。比较惊喜的是,性能直接就达到了单机版的两倍了!延时降低这么多的原因,可能是Java程序有某种缓存机制吧,之前请求过,现在再请求就快很多了。

下一步改进:因为nginx默认与后端Java程序是以http1.0的短连接方式建立连接的,所以会把大量时间浪费在nginx与应用服务器之间建立连接与断开连接。而至于应用服务器与mysql之间的连接,因为有druid数据库连接池的管理,所以这个建连和断连的耗时可以忽略不计。

把nginx的反向代理设置为http1.1的长连接方式(keepalive)后,压测结果如下:

具体就是平均时延193ms,吞吐量是2021.9每秒。对比之前,性能的确有提升,而且我感觉提升还挺多的,主要就是时延降低了(应该是减少了nginx与应用服务器的建连和断连的耗时),吞吐量突破2000了!!!

集群化部署后,需要考虑如何做session同步的问题:

本项目采用redis集中化存储session的方法来解决多个服务器间的session同步问题。其实因为使用了redis来集中存储session,所以就避免了服务器间session同步的问题。

对于springboot项目,要做到这点,其实不困难,在pom文件里面加入下面两个依赖即可:

如果想要修改springboot对于session管理默认配置,可以创建一个配置类,如下所示:

注意:把session存储在redis中之后,存放在session的对象要实现serializable接口,不然会存放失败。因为要把对象存在redis中,就必须对对象进行序列化。 

啊啊啊啊啊!这种做法有大坑!!!会出现Spring Session产生的sessionid与cookies中的sessionid不一样的问题!!!导致一直生成新的session。好像根源不是这个问题。。。

具体为啥session不起作用(就是登录之后,下单还是提示的用户未登录,并且这个请求会生成新的session。。),原因还不清楚。。。

破案了,原来是spring-session-data-redis对于跨域的操作,会导致session失效。具体原因是,参考的https://www.cnblogs.com/jarjune/p/11912277.html

比正常情况多了SameSite=Lax

于是就带着这个参数去搜索,

SameSite

Cookie 的SameSite属性用来限制第三方 Cookie,从而减少安全风险。也就是防止CSRF攻击

 于是解决办法就是,自己手工创建默认的CookieSerializer,覆盖掉默认的samesite=Lax:

对读请求的优化:

 商品详情页查询优化之使用Redis缓存商品数据

 对于如何在springboot正确的使用redis来缓存数据,这里简单的说说4点:

1、首先在pom文件里面加入spring-boot-starter-data-redis依赖

2、在application.properties配置文件中加入对redis的配置信息

3、在controller层中使用redis缓存,不是在service层中!!!在相应的controller中注入RedisTemplate,使用redisTemplate来对要缓存数据进行存储和获取。

4、因为默认的redistemplate对于key和value的序列化方法不好用,主要是序列化后,在redis里面查看基本是一堆乱码,看不懂,所以需要定制化RedisTemplate的序列化方式,让其将类对象序列化成json格式的数据。

最后给出使用jmeter压测商品详情接口的测试结果:

具体就是平均时延是48ms,吞吐量是2912.4每秒。延迟降低了不少,然后吞吐量也提升了不少。总得来说,性能提升符合预期。

商品详情页查询优化之使用本地热点缓存

这是对使用Redis缓存的进一步性能提升。原因:操作Redis缓存必须通过网络IO,所以肯定会比直接操作Java进程本地的内存空间要慢。

但是,因为Java进程的本地内存空间极其珍贵,所以只有极少数的,非常热点的数据才能进入本地热点缓存。

本地热点缓存的特点:

  • 热点数据:因为Java进程的堆空间大小极其有限,所以不可能把redis缓存的全部内容都存在这里。因此,这里只缓存非常热点的数据,比如每秒访问上千上万次之类的。这样既能减少访问redis的网络开销,又能减少redis服务器的压力。
  • 脏读不敏感:因为本地热点缓存是每个JVM里面有一个,所以当缓存的数据被修改后,无法做到像redis这种集中式缓存那样统一的一步到位的修改缓存状态,只能一个一个Java进程进行缓存的修改。这样一个一个的修改,无疑性能开销是非常大的。因此,本地热点缓存基本不会去进行缓存数据的修改。于是,本地热点缓存所存储的数据只能是脏读不敏感类型的数据。因为需要脏读不敏感,所以需要本地热点缓存所存储的数据的生命周期不会特别长(这样才能尽量减少脏读带来的影响),一般会比redis缓存的生命周期短很多。因此本地热点缓存更多的是做一些热点数据的瞬时访问。
  • 内存可控:内存可控指的是,本地热点缓存的大小需要能设置一个最大值,从而避免占用过多的内存空间。如果缓存空间超过了设置的最大值,就要用淘汰机制删除一些缓存数据,比如LRU之类的淘汰机制。

因为商品详情信息符合本地热点缓存的特点,所以可以使用本地热点缓存来加速商品详情数据的获取。

本项目使用Guava cache来作为本地热点缓存的实现。

不使用JDK自带的ConcurrentHashMap的原因是:

  • 1、基于段的方式去加锁,对于put操作发生的时候,在加上写锁后,会对读锁的性能产生很大的影响。(其实这个我还不懂。。。没研究过ConcurrentHashMap,以后记得研究一下。。。)
  • 2、做缓存,就必须要考虑失效数据的淘汰时间机制,ConcurrentHashMap并没有提供这样的机制。 

Guava cache的特点:

  • 1、本质上也是一个可并发的HashMap。
  • 2、可控制map的容量和key的超时时间。
  • 3、可配置的lru淘汰策略。
  • 4、线程安全的。(其实从支持并发访问就可以得出线程安全了)

Java代码的实现:

1、首先在pom文件中加入对guava包的依赖。

2、创建CacheService和对应的实现

3、在相应的controller中使用CacheService

最后给出使用本地热点缓存后的性能压测结果:

总的来说,平均延迟降低到8ms,降低很多。但是吞吐量才提升到3279左右,感觉提升不是很大。其实我在测的时候发现线程数总是上不去,我觉得应该是设置的Ramp-Up时间太长了,而现在请求的响应时间又很短,所以很多线程的任务都结束了(循环发送请求50次),后面的线程还没创建出来,从而导致吞吐量提升不大。

于是,我把Ramp-Up时间从15秒变为5秒,测试结果为:

这次压测的平均延迟提升到了75ms,这个可以理解,因为请求压力变大了,每个请求处理的时间就会相应变长。惊喜的是,吞吐量直接飙到了5361.4每秒,这提升也太大了吧,有点吓人了!!!可能这就是本地热点缓存的威力了吧!!!不过,我觉得能使用到本地热点缓存的场景和数据类型很少。。。

总结,一般来说,越靠近用户的缓存速度越快,但是更新策略就越复杂,所以没有通用的缓存方案能够解决所有的问题,更多的是要结合业务场景,看一下对应的更新策略、缓存热点的程度以及业务上用户对于脏读的忍受程度,来共同决定采用什么样的缓存方案。

对写请求的优化:

不进行任何优化的商品下单接口的性能压测,/order/create(itemId=4&amount=1&promoId=):

可以看到性能极低,延迟平均值很高,为6753ms,吞吐量很低,才142.2每秒。因为这个接口涉及到大量的读表操作和写表操作,而且很多写表的操作还是针对同一行的数据进行修改,所以会受到行锁的影响,所以性能会很低。

进行交易参数验证逻辑的优化:

这个优化主要就是把参数验证中本来需要在数据库中查出来的itemModel和userModel,缓存到redis中,从而减少对数据库的读操作,从而提升了性能。

压测结果为:

可以看到性能提升极其有限。平均延迟从6753ms降低到5326ms,吞吐量从142.2每秒提升到176.1每秒。

不过我想了一下,这个结果也是合理的。上图是压测的时候,redis和mysql服务器(redis和mysql部署在同一台服务器上)的top命令界面。可以看到,即使做了一定的优化,系统的运行压力还是在mysql上。

我觉得是因为真正耗费大量时间的是写操作,而不是读操作。而这次优化的都是读操作,所以性能的提升并不明显。

至于课程视频里面为何提升那么大(吞吐量都到1200左右了。。),应该是他数据库里面的库存已经为0了,从而导致后续的减库存、订单入库和增加销量等操作都没有了。

进行库存扣减和销量增加逻辑异步化的优化(其实我一直有一个疑惑,就是如果异步化的写操作执行崩了,咋办。。。,好像没法回滚之前的操作吧。。。):

这一步优化操作的核心思想就是把写表操作中,涉及到行锁竞争的操作进行异步化。其中,涉及到行锁竞争的操作有扣减库存(同一个itemId)、增加销量(同一个itemId)和生成订单号(同一个name)。

然后,对于生成订单号的优化放到下面再做,因为视频中没有涉及。这步优化针对扣减库存和增加销量这两个操作。

其实,优化的核心就是使用MQ来达到异步调用扣减库存和增加销量的目的,从而能够提前将处理结果返回给用户(不用执行完前面所述的两个耗时的操作就能返回),降低响应延迟,提高吞吐量。

又因为这个异步调用操作需要做到分布式事务,来确保最终一致性,所以选用了rocketmq来作为本项目所使用的MQ,因为rocketmq(后面要研究一下rocketmq啊,怕面试被问啊。。。)支持事务型消息,简化了分布式事务的实现。

至于怎么用rocketmq来正确的实现分布式事务,就回去看代码吧,这里就不细说了。。。

最后贴一下性能压测结果(结果不太好。。。):

可以看到,与上面的压测结果对比,性能基本没有提升,而且因为引入了额外的组件(MQ),从而导致延迟的最大值还提升了。。。

从这个结果就可以看出,只要存在行锁的竞争操作,不论操作的数量多少(从3个变为1个),最终的性能都会差不多。也就是说,只要存在行锁竞争的操作,就会严重制约整体的性能,原因应该是行锁竞争,从而导致这个操作在数据库里只能串行化执行,从而导致支持的并发量大大减低,使得硬件性能大量闲置(因为大部分时间都在等待行锁)。

这个是压测时候的mysql进程的性能占用,只占用了50%左右的cpu。其实比上面的压测有所提高,但是因为还有一个行锁存在,所以吞吐量变化不大。 

进行生成订单号操作的缓存化优化:

其实,从上一步优化就可以看出,这步优化就是针对生成订单号这个操作进行的。这个操作也是一个行锁竞争的操作,所以会严重制约性能。

我的想法就是,把订单序列号存储在redis中,从而就只需要在redis中进行加1操作即可,避免了写数据库的操作。

不过,这么做的话,就必须做好redis的持久化配置,因为如果redis挂了,但是没有及时做好持久化,那么就会导致订单号的序列的混乱。

其实,我感觉这个问题其实就是全局唯一有序id生成的问题,就是如何高效的生成全局唯一有序id,日后可以查查相关资料,学习一下!!!

最后放一下压测结果:

可以看到,性能提升了很多,延迟的降低到了2002ms,吞吐量提升到了429.6每秒。这个项目对于写请求的优化,终于取得了明显的效果。

优化的思想就是在返回响应前,彻底消灭涉及到数据库行锁竞争的操作。其实,这些操作有些是必须做的,优化的思路就是把这些操作异步化,放到返回响应之后再做,从而降低了响应延迟,提升了用户体验。

还有,就是我们可以看到, 即使进行了优化, 写请求的吞吐量还是远低于读请求的。不过,这也是可以理解的,因为写请求毕竟要进行数据库写操作(主要是insert和update),写操作比读操作慢是很显然的,所以也就导致了写请求的吞吐量远低于读请求。

这个是压测时候的mysql进程的性能占用,可以看到,这次占用了84%的cpu,说明优化之后充分利用了cpu的性能。(其实不太懂,可能是因为返回响应前的写操作不受行锁竞争的影响,从而支持的并发量有所提高,从而提升了cpu使用率,也就提升了整体的性能。)

流量削峰相关:

其实,我感觉这部分的内容讲得太虚了,没什么意思。其实,我感觉这部分内容和提升系统可支持的并发量已经关系不大了,主要探讨的是在既定可支持的并发量的情况下,如何尽量的保护系统,防止其过载。

视频里面主要就分为三部分,分别是秒杀令牌、秒杀大闸和队列泄洪。

其中,我感觉秒杀令牌和秒杀大闸没啥用,我就不打算尝试实现了。然后,我尝试实现了一下队列泄洪。其实,这个队列泄洪没啥,这里的队列就是一个线程池,然后就是在OrderController的方法里面使用一个线程池来排队执行耗费大量时间的操作,然后controller还是要等待剩下的操作执行完才能返回(这里不是异步实现),所以我感觉对性能提升不大,甚至会限制了性能的发挥,因为限制了总的并发量了。

其实,在我的印象中,这个队列泄洪应该是和MQ相关的吧?。。。

加了线程池后,压测结果是:

压测结果也证实了我的想法。

其实,课程里面对于队列泄洪的说法是:排队有些时候比并发更高效(例如redis的单线程执行命令模型)。不可否认,这个说法的确合理,但是我觉得在这个秒杀场景里面,大部分时候并不是排队比并发更高效的。而且如果硬要限制单机的并发量的话,我感觉也不用在controller里面用一个线程池来限制吧,直接配置Tomcat的最大工作线程数不就可以达到同样的效果吗。。。

当然了,这也是我自己的想法,也不一定对。。。

防刷限流相关:

我感觉这部分的内容也是讲得很虚。。。其实,这部分内容和提升系统可支持的并发量已经关系不大了,主要探讨的是在既定可支持的并发量的情况下,如何尽量的保护系统,防止其过载。

视频里面主要分为三部分,分别是验证码技术、限流技术和防刷技术。其中,验证码技术和限流技术讲到了代码实现,而防刷技术只讲了实习思路,没有讲具体的代码实现。

其中,限流技术和防刷技术对于我这个玩具项目来说,感觉还用不上。。所以,我只尝试实现一下验证码,然后我感觉验证码技术其实已经可以起到一定的防刷作用了。

注意,虽然这个验证码用在了下单操作,但是验证码是与用户挂钩的,而不是与商品挂钩的。也就是说,在同一时刻,用户与验证码是一一对应的,即使一个用户同时打开多个页面,也只有一个验证码是有效的。具体的就是最后获取的那个验证码有效,因为后面获取的验证码会把之前的验证码覆盖掉,在redis里面。

其实吧,我感觉如果允许用户同时抢购多个商品的话,也可以让验证码与用户和商品的组合一一对应,这样实现也不难,就是在验证码的redis的key中再加上商品id就可以了。

实现完的前端效果:

面过一次试之后,感觉还是加上限流技术吧,起码能写的东西多一点,让项目经历看起来没有那么单薄。

要进行限流的原因:

1、流量远比你想的要多

2、系统活着比挂了要好

3、宁愿只让少数人能用,也不要让所有人都不能用(其实就是避免大量的请求使得系统超负荷运行,导致系统崩了)

限流方案:

1、限并发

2、令牌桶算法

3、漏桶算法

限流力度:

1、接口维度(本项目采用这个方案)

2、总维度(通常比接口维度的总和少20%)

限流范围:

1、集群限流:依赖redis或其他的中间件技术做统一计数器,往往会产生性能瓶颈(因为全部请求都会先访问这个统一计数器)

2、单机限流:负载均衡的前提下单机平均限流效果更好(这个可以通过配置Tomcat的相关参数来实现)

3、接口限流:使用限流器对特定的热点接口进行限流(本项目的方案)

最后本项目使用了guava中的RateLimiter来对秒杀下单接口进行限流,因为经过压测得到两台机器的TPS大概是400多,所以一台机器的RateLimiter大小设置为200。

具体的代码为:

①构造限流器:

②下单接口的最开始先使用限流器进行拦截

至此,这个项目就做得差不多了。秒杀项目的开发和优化暂告一段落。这个项目会成为我的简历里面的项目经历的主要内容。日后会常常回来看看这篇博客,因为面试会经常问项目的内容,所以必须对项目非常熟悉。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值