秒杀系统超全技术


秒杀项目超全面经

  • 一、讲一下你这个秒杀项目?
    • 秒杀令牌的作用
  • 二、秒杀系统面临的主要问题有哪些?
    • 1、秒杀系统中如何处理超卖问题的?
    • 2、秒杀系统如何解决少卖问题?
    • 3、秒杀系统中如何解决重复下单问题?
    • 4、热点数据失效(缓存击穿)问题如何解决?
    • 5、缓存和数据库之间的一致性如何保证?
    • 6、库存扣除成功了,订单生成失败了,怎么办?
    • 7、多机器扣减库存,如何保证线程安全?
    • 8、如何解决客户的恶意下单问题?
    • 9、做了什么削峰限流的措施?
  • 三、如何扣减redis中的库存?
  • 四、 如果项目中的redis宕机,如何减轻数据库的压力?
  • 五、项目中用redis都做了什么
  • 六、项目中用RabbitMQ都做了什么?
  • 七、线程池技术中核心线程数的取值有经验值吗?
  • 八、TPS提升了多少?
  • 九、一个人同时用电脑和手机去抢购商品,会颁发几个token?
  • 十、秒杀系统中MySQL中的表字段是怎么设计的?
  • 十一、项目中的线程池问题
    • 1、如何利用线程池实现了流量削峰?
    • 2、线程池的核心参数?
    • 3、线程池的执行流程?
    • 4、被线程池拒绝掉的那部分用户的秒杀令牌还有效吗?
  • 十二、项目中的JVM问题?
    • 1、项目上线之后想看JVM的GC情况在Linux中用什么命令?
  • 十三、你项目中的难点是什么?
    • 1、分布式会话实现的原理
    • 2、Feign远程调用时请求头丢失的问题


一、讲一下你这个秒杀项目?

这是一个模拟了高并发场景的商城系统,它具备秒杀功能。我结合秒杀项目的【高并发】【高一致性】【高可用】三个特性,分别制定了一系列的解决方案。

  • 在前端层面上:分别对【秒杀页面渲染】、【安全性校验】、【削峰限流策略】进行了秒杀系统设计的优化。

    层面 解决方案
    在秒杀页面渲染上 进行动静分离,将静态资源缓存至浏览器(最佳是CDN节点),用户信息等动态资源通过服务端异步的方式加载,同时结合FreeMarker进行页面动态渲染,前后端分离以降低服务端的压力,加快用户访问速度。
    在安全性问题上 使用双重MD5密码校验,隐藏秒杀接口地址,设置秒杀令牌机制,根据IP访问频率设置接口防刷。
    在限流削峰策略上 通过Nginx进行最大连接数的限制,同时在用户下单时设置数学公式验证码,分散同一时刻下单的用户数量。
  • 在服务端层面上:针对三高的问题,分别在【缓存预热】、【削峰限流】、【安全性校验】、【下单逻辑】上进行优化,保证服务端能够高效且安全的处理秒杀请求。具体的:

    层面 解决方案
    在缓存预热上 (1)系统初始化时,利用定时任务或者基于Bean的生命周期初始化,将商品信息及库存数量,以及秒杀令牌预先加载到Redis中做缓存预热,通过redis的单线程特性及其内存读取,保证线程安全的同时,提高响应速度。
    在削峰限流上 (2)为了保证高并发下的一致性问题,首要就要进行削峰限流,避免大量用户同一时间操作数据库导致数据不一致。直接对数据库加排他锁,会导致秒杀系统的吞吐量非常低,系统响应慢,用户购物体验差。

    【秒杀令牌】 针对数据库直接加锁的吞吐量低下的问题,采用秒杀令牌的方式进行解决,在秒杀活动开始时,用户下单时服务端首先会为用户生成秒杀令牌(skill_token),然后存储到redis中。然后下单时,与redis中存储的令牌进行比对,比对成功后才能访问进行下单,否则返回秒杀令牌校验失败。在削峰的同时还避免了脚本恶意抢购。但是秒杀令牌只要活动一开始就可以无限制的生成,生成的海量令牌会影响秒杀系统的性能;

    【秒杀大闸】 针对无限制生成秒杀令牌的问题,采用秒杀大闸的方式进行解决。具体的,根据redis中秒杀商品的数量,提前设定秒杀令牌的数量(一般为商品的数倍),用户下单时。服务端为其生成秒杀令牌,首先获取该商品对应秒杀令牌的剩余数量,若还有剩余令牌,则为其生成令牌。若剩余数量为0,则无法生成令牌,返回抢购失败。就能一定程度上的解决无限制生成秒杀令牌的问题。但是当秒杀商品库存特别多的时候,以数倍的方式发送令牌依然会造成大量秒杀请求的涌入,数据库依然会承受很大的瞬时压力。

    【队列泄洪】 针对多商品、多库存下的瞬时流量涌入问题,采用队列泄洪的方式解决。依靠排队和下游拥塞的程度调整队列释放流量的大小。比如支付宝银行网关队列,虽然支付宝能够支持大并发,但是支付是在银行的数据库上操作的,银行支持不了如此高的并发量。因此,支付宝会调整释放流量的大小给银行处理,保证在银行能够支持的并发量能力范围内。因此,我们可以借鉴这种解决方案。

      方法一: 利用线程池实现队列泄洪: 针对秒杀请求创建一个线程数固定阻塞队列大小为(秒杀商品总数量 - 核心线程数)的线程池,使得线程池同一时间只能处理固定数量的请求,其它的请求放在队列中等待,从而实现队列泄洪。保证打到数据库上的请求量,都在数据库的并发处理范围内。

      方法二: 利用令牌桶算法来实现队列泄洪,导入 guava limiter 依赖包,设置服务器每秒处理的请求数量,在用户下单时校验,若服务器处理不了就返回秒杀失败或者正忙。

      方法三: 利用redis的分布式队列等实现队列泄洪,当队列出现问题时,采用降级的方式切回本地内存队列。

    通过【秒杀令牌】+【秒杀大闸】+【队列泄洪】机制进行削峰限流后,就能够很大程度上的减轻服务端的压力,从而提高秒杀系统的吞吐量。
    在安全性校验上 (3)服务端收到收到秒杀请求后,首先验证秒杀是否合法(秒杀时间 / 秒杀数量 / 秒杀令牌)。具体的,就是基于Lua脚本或者利用redis中的原子性increase命令,进行预扣库存,当Redis中的库存数量不足时,直接返回秒杀失败,并将预扣的库存加回来,防止超买超卖问题;

    (4)同时,为了解决用户刷新页面时,订单重复生成的问题。规定用户一旦秒杀成功,就利用SETNX原子性操作将“userId_goodsId”放到redis中占坑,并根据秒杀规则设置token的过期时间(比如一场秒杀只能参与一次,token过期时间就设置到秒杀活动结束),token设置成功后,才能继续参与秒杀活动。若SETNX失败,则返回秒杀失败。
    在数据库层面的下单逻辑上 (5)经过上述的缓存预热、削峰限流、安全性校验等步骤后。用户可以下单成功,针对高并发下的库存一致性问题。通常有两种解决方案,一种就是付款减库存,但是这种模式下很有可能出现用户明明已经下单成功,却显示没有库存的问题。另一种就是就是下单减库存,能够保证下单成功的用户能够付款成功。

      我的项目是采用的【下单减库存 + MQ异步通知型事务】的方式来实现订单与库存的最终一致性。创建订单时首先远程调用库存系统进行锁库存,锁库存的SQL语句通过版本号的机制(update自带的排他锁)来实现,若库存数量锁定后小于0,则锁定失败,返回秒杀失败,从而在数据库层面上解决超买超卖问题。库存锁定成功后,立刻向库存服务交换机发送一条解锁库存的消息,防止库存刚一锁定成功,订单系统就宕机了,从而确保库存一定能够释放。
      库存锁定成功后,通过Feature异步的方式进行封装订单,同时将订单消息发送到订单系统交换机,路由到30min的延时队列,订单消息过期后经过死信路由器转发给订单系统,判断该订单的支付状态。若支付则完成收单,若未支付则向库存服务的交换机发送释放库存的消息,保证库存能够顺利回滚,解决少卖问题。
  • 最后通过Jmeter压力测试,秒杀系统的QPS从初始的120/s提升到5000/s。


秒杀令牌的作用

概述 描述
原理 (1)秒杀接口需要依靠随机码(准入令牌)才能进入,随机码是在商品缓存预热的时候已经设置好了。当用户去刷新秒杀页面时,即调用getCurrentSeckillSkus()时,服务端会进行秒杀时间段的校验,如果秒杀时间段符合,才会为用户颁发随机码,用于下单时拿着这个随机码,就可以去访问秒杀接口进行下单(从session中获取随机码)。如果不在秒杀时间段内,则不为用户返回随机码,防止在秒杀前脚本恶意抢购的风险。

(2)秒杀的令牌由秒杀活动模块负责生成

(3)秒杀活动模块对秒杀令牌生成全权处理,逻辑收口

(4)秒杀下单前需要先获得秒杀令牌
作用 (1)秒杀令牌只有在秒杀开始后才会生成,在有秒杀令牌后才能抢商品,所以就避免了提前通过token、itemId、promoId和url的脚本抢商品。

(2)验证代码放在生成令牌中,与下单代码隔离开,实现低耦合。

二、秒杀系统面临的主要问题有哪些?

秒杀系统中存在的问题 解决方法
超买超卖问题 1、秒杀系统中如何解决超买超卖问题的
少买问题 2、秒杀系统如何解决少卖问题?
重复下单问题 3、秒杀系统中如何解决重复下单问题?
缓存的三大问题 4、热点数据失效(缓存击穿)问题如何解决?
数据库缓存不一致 5、缓存和数据库之间的一致性如何保证?
6、库存扣除成功了,订单生成失败了,怎么办?
分布式线程安全问题 7、多机器扣减库存,如何保证线程安全?
脚本恶意抢购 8、如何解决客户的恶意下单问题?
高并发 9、限流削峰策略

1、秒杀系统中如何处理超卖问题的?

解决层面 描述
Redis缓存层面 利用redis的单线程特性预减库存处理秒杀超卖问题!

(1)在系统初始化时,将商品以及对应的库存数量预先加载到Redis缓存中;

(2)服务端收到到秒杀请求时,在Redis中基于Lua脚本进行预减库存 (商品数量减到负数后再加回来,保证减增商品数量的操作是原子性);当Redis中的库存不足时,直接返回秒杀失败,否则继续进行第3步;

(3)将请求放入异步队列中,返回正在排队中;

(4)服务端异步队列(MQ)将请求出队,出队成功的请求可以生成秒杀订单,减少数据库库存,返回秒杀订单详情。
数据库层面 借助版本号机制,依靠MySQL中的排他锁,利用SQL语句避免超买超卖问题;
updata table set num = num - #{cnt} where id = #{id} and num > #{cnt};

2、秒杀系统如何解决少卖问题?

方法 描述
MQ异步通知型事务实现库存数量的最终一致性 利用【下单减库存】 + 【MQ的延时队列】,实现订单消息的延时确认及库存回滚操作,保证商品库存的最终一致性。具体地:

(1)创建订单时,首先远程调用库存服务的接口减库存。

(2)库存系统扣除对应商品库存后,发送一条延时释放库存的消息到MQ,等待45min后回滚库存。这是为了避免库存系统刚扣完库存,订单系统就炸了,导致库存迟迟加不回来,从而出现少买问题。

(3)回到订单系统,锁定完库存后,订单创建成功,同时发送一条延时订单确认的消息到MQ,等待30min后确认消费。30min后检查订单状态:若是已支付状态,则完成收单。若是未支付或者取消状态,则进行关单,同时发送一条释放库存的消息给MQ,进行库存回滚,防止少卖问题。

3、秒杀系统中如何解决重复下单问题?

方法 描述
唯一索引 + SETNX命令 利用【唯一索引:“userId_goodsId”】+【redis的SETNX命令占坑】,保证秒杀接口的幂等性

为了解决订单重复下单的问题。规定用户一旦秒杀成功,就利用SETNX原子性操作将 “userId_goodsId” 放到redis中占坑,并根据秒杀规则设置token的过期时间(比如一场秒杀只能参与一次,token过期时间就设置到秒杀活动结束),token设置成功后,才能继续参与秒杀活动。若SETNX失败,则返回秒杀失败。

4、热点数据失效(缓存击穿)问题如何解决?

阶段 解决方法
秒杀活动–前期 (1)设置秒杀商品的缓存永不过期
秒杀活动–中期 (2)分布式互斥锁:缓存中为数据为空时,通过互斥锁来控制访问数据库的线程数量,某个线程抢到了互斥锁后就更新缓存,其它线程就可以在缓存中查到数据了。

(3)熔断机制与服务降级:当流量达到一定阈值,直接返回"系统拥挤",防止过多的请求打到数据库上。以此保证秒杀系统的部分可用性。
秒杀活动–后期 (4)开启redis持久化(AOF/RDB),当然秒杀系统不建议开启,这只是常规解决方案。

5、缓存和数据库之间的一致性如何保证?

方式 描述
引入canal组件 canal组件可以使得服务直接监听MySQL的binlog日志,将自己伪装成M有SQL的从库,只负责接收数据而不做其它处理。使得redis缓存可以实时的与数据库保持同步。
先更新数据库立即删除缓存 更新数据库后立即删缓存,然后下一次查缓存找不到数据后会再次从数据库同步到缓存。同时,为了避免缓存删除失败的情况,采用订阅MySQL的binlog日志的方式(canal组件),进行缓存删除失败的补偿,确保缓存一定能够删除成功。

6、库存扣除成功了,订单生成失败了,怎么办?

场景 解决方式
非分布式场景下 使用Spring提供的事务功能即可
分布式场景下 将【扣除库存】与【生成订单】操作组合为一个事务。分配全局事务唯一ID,要么一起成功,要么一起失败。常见的分布式事务协议有如下:

(1)全局消息;(2)MQ异步通知型事务;(3)TCC协议;(4)最大努力通知;(5)2pc协议

7、多机器扣减库存,如何保证线程安全?

采用分布式锁的机制,保证线程安全。常见的分布式锁技术如下:

方式 描述
redis实现分布式锁 (1)LUA 脚本:(SETNX命令 + expire键过期) + watch Dog机制 + LUA (删除时判断锁是不是自己要删的那把)

(2)Redisson框架: 提供了显式lock锁、unlock锁等命令,以及分布式信号量等机制;
Zookeeper实现分布式锁 (3)基于Zookeeper的临时顺序节点: 监听前一个zk节点(序号最小)的状态,当前一个删除后,若当前节点的序号是最小的,则获取锁。

8、如何解决客户的恶意下单问题?

层面
前端限制 (1)用户下单时输入数学公式验证码,增加购买复杂性。

(2)一次下单后,按钮置灰几秒钟。

(3)封IP地址,Nginx可以设置单个IP访问频率和次数多了之后进行拉黑
后端限制 (1)秒杀接口需要依靠随机码(准入令牌)才能访问,随机码是在商品缓存预热的时候已经设置好了(每个商品都有对应的随机码)。当用户去刷新秒杀页面时,即调用getCurrentSeckillSkus()时,服务端会进行秒杀时间段的校验,如果秒杀时间段符合,才会为用户颁发随机码,用于下单时拿着这个随机码,就可以去访问秒杀接口进行下单(从session中获取随机码)。如果不在秒杀时间段内,则不为用户返回随机码,防止在秒杀前脚本恶意抢购的风险。

(2)而且下单时,还要校验当前用户是否拥有秒杀令牌(非随机码)。秒杀令牌由秒杀活动模块负责生成,切秒杀活动模块对秒杀令牌生成全权处理,逻辑收口。

(3)由于秒杀令牌的设置,用户没有令牌,就要先去获得令牌(秒杀大闸导致了秒杀令牌的数量上线)。用户有令牌的话直接返回 “正在抢购中”,不允许再下单。

9、做了什么削峰限流的措施?

层面 限流方式
前端限流 (1)通过Nginx设置每秒最大连接数。

(2)在用户下单时设置数学公式验证码,分散同一时刻下单的用户数量。
服务端限流 通过【秒杀令牌】+【秒杀大闸】+【队列泄洪】的机制去削峰限流;

(1)在秒杀活动开启前,提前将商品信息缓存到redis中做缓存预热。并生成5倍商品数量的秒杀令牌,存入redis中;

(2)用户下单需要先从redis中获取秒杀令牌后,才有资格参与抢购秒杀。

(3) 经过秒杀令牌+秒杀大闸的处理后,已经能够阻挡住大部分的请求了。但是在多商品、多库存下的场景下,依然会存在瞬时流量涌入问题,针对于此项目采用队列泄洪的方式解决。

  比如支付宝银行网关队列,虽然支付宝能够支持大并发,但是支付是在银行的数据库上操作的,银行支持不了如此高的并发量。因此,支付宝会调整释放流量的大小给银行处理,保证在银行能够支持的并发量能力范围内。因此,我们可以借鉴这种解决方案。

  方法一: 利用线程池实现队列泄洪:针对秒杀请求创建一个线程数固定,阻塞队列无界的线程池(即FixedThreadPool线程池),使得线程池同一时间只能处理固定数量的请求,其它的请求放在队列中等待,从而实现队列泄洪。保证打到数据库上的请求量,在数据库的并发处理范围内。

  方法二: 利用令牌桶算法来实现队列泄洪,导入 guava limiter 依赖包,设置服务器每秒处理的请求数量,在用户下单时校验,若服务器处理不了就返回秒杀失败或者正忙。

  方法三: 利用redis的单线程分布式队列等实现队列泄洪,当redis队列出现问题时,采用降级的方式切回本地内存队列。

通过【秒杀令牌】+【秒杀大闸】+【队列泄洪】机制进行削峰限流后,就能够很大程度上的减轻服务端的压力,从而提高秒杀系统的吞吐量。

三、如何扣减redis中的库存?

方式 描述
原子性指令 decrement API减库存,increment API回增库存。以上的指令都是原子性的。
Lua脚本 利用Lua脚本,如果扣减到负数,则加回来。两个操作是原子性的。

四、 如果项目中的redis宕机,如何减轻数据库的压力?

搭建redis集群,分为主从模式、哨兵模式。

模式 描述
主从模式中 如果主机宕机,使用slave of no one 断开主从关系,并且把从机升级为主机。
哨兵模式中 自动监控master / slave的运行状态,基本原理是:心跳机制 + 投票裁决。

(1)每个sentinel会向其它sentinel、master、slave定时发送消息(哨兵定期给主或者从和slave发送ping包(IP:port),正常则响应pong,ping和pong就叫心跳机制),以确认对方是否“活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的“主观认为宕机” Subjective Down,简称SDOWN)。

(2)若master被判断死亡之后,通过选举算法,从剩下的slave节点中选一台升级为master。并自动修改相关配置。

五、项目中用redis都做了什么

应用 描述
分布式锁 多台机器抢购商品,通过redis的分布式锁保证并发安全。

(1)LUA 脚本:(SETNX命令 + expire键过期) + watch Dog机制 + LUA (删除时判断锁是不是自己要删的那把)

(2)Redisson框架: 提供了显式lock锁、unlock锁等命令,以及分布式信号量等机制;
缓存预热 提前将秒杀商品的信息存储到redis中,作为缓存中间件,减轻数据库访问压力。
预减库存 内存读取,提高服务器响应速度,且redis的单线程模型天然具备分布式特性。
分布式会话 通过redis + token机制实现分布式会话。
秒杀令牌 将秒杀令牌存到redis中,进行限流。
解决重复下单 利用【唯一索引:“userId_goodsId”】生成的token令牌+【redis的SETNX命令占坑】,保证秒杀接口的幂等性。用户一旦秒杀成功,则取redis中进行占坑。占坑失败则表示已经购买过。
解决MQ消息重复性消费 消息一旦消费后,则去redis中进行占坑。如果设置失败,则表示消息已经消费过,消费者直接ACK不做其他处理。

六、项目中用RabbitMQ都做了什么?

应用 描述
MQ异步下单 利用队列排队异步下单,加快响应速度,减轻数据库的并发压力
MQ延时队列 延时队列实现用户下单等待30min
MQ通知型事务 设计库存回滚逻辑,保证商品库存最终一致性

七、线程池技术中核心线程数的取值有经验值吗?

类型 核心线程数取值
CPU密集型业务 N+1
IO密集型业务 2N+1

八、TPS提升了多少?

基础架构下的tps是120/s,经过动静分离、nginx反向代理做分布式扩展后,通过秒杀系统的设计过后,TPS达到了5400/s;


九、一个人同时用电脑和手机去抢购商品,会颁发几个token?

多台设备登录属于单点登录(SSO)问题,用户登录一端之后另外一端可以通过扫码等形式登录。虽然用户登录了多台设备,但是用户信息是一样的。项目中为同一用户颁发的token是相同的,因此多台设备登录时只会颁发一个token。


十、秒杀系统中MySQL中的表字段是怎么设计的?

表结构 描述
秒杀用户表 持有秒杀令牌的用户信息
商品信息表 记录商品信息
秒杀商品表 记录该商品的秒杀始末时间,秒杀价和剩余数量等信息
秒杀订单表 记录秒杀用户名和秒杀的商品还有订单号
订单详情表 通过秒杀订单号来查找对应的订单详情,里面记载更详实的业务信息

十一、项目中的线程池问题

1、如何利用线程池实现了流量削峰?

限流削峰 描述
利用线程池实现队列泄洪 针对秒杀请求创建一个线程数固定阻塞队列大小为(秒杀商品总数量 - 核心线程数)的线程池,使得线程池同一时间只能处理固定数量的请求,其它的请求放在队列中等待,从而实现队列泄洪。保证打到数据库上的请求量,都在数据库的并发处理范围内。

2、线程池的核心参数?

参数 描述
corePoolSize
(核心线程数)
当提交一个任务时,线程池会创建一个新线程执行任务,此时线程不会复用。如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。此时如果核心线程有空闲,回去阻塞队列中领取任务,此时核心线程复用。
maximumPoolSize
(最大线程数
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,此时若当前线程数小于maximumPoolSize,则可以创建新的线程(此时就是属于非核心线程)执行任务,
keepAliveTime
(非核心线程存活时间)
当线程池中的线程数量大于corePoolSize时,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被销毁,最终会收缩到corePoolSize的大小。
TimeUnit keepAliveTime的时间单位
workQueue
(阻塞队列)
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务

LinkedBlockingQuene:基于链表结构的无界阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;

SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

priorityBlockingQuene:具有优先级的无界阻塞队列;

DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素;
handler
(拒绝策略)
线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种拒绝策略:
AbortPolicy:直接抛出异常,默认策略;

CallerRunsPolicy:用调用者所在的线程来执行任务;

DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务(最老的任务),并执行当前任务;

DiscardPolicy:直接丢弃任务,不报错;
ThreadFactory
(线程工厂)
创建线程的工厂,可以设定线程名、线程编号等;默认使用 Executors.defaultThreadFactory() 来创建线程
通过自定义线程工厂,使用Thread.setDefaultUncaughtExceptionHandler()方法可以捕获线程池中的异常

3、线程池的执行流程?

步骤 方法 描述
Step1 执行execute(任务) / submit(任务) ① 调用ctl.get()方法,获取当前线程池状态(Running-Shutdown-Stop-Tidying-Terminated),若当前线程池状态不是Running状态或者为Shutdown状态但是工作队列已经为空,那么就直接返回,执行失败;

② 调用workerCount()方法,获取当前【工作线程数】;基于此判断是执行拒绝策略还是执行addWorker()方法

③ 当【工作线程数 > 核心线程数】并且 【阻塞队列未满】,那就向阻塞队列中添加任务;添加成功后,再次调用ctl.get()方法判断线程池当前状态;若此时线程池状态不是Running,则删除任务并执行拒绝策略
Step2 addWorker(任务,boolean)方法执行时机;

传入true用核心线程执行任务,false用非核心线程执行任务
① 当【工作线程数 < 核心线程数】那就调用addWorker(任务,true),创建一个核心线程去执行任务;

② 若任务已经添加到了阻塞队列中,但是此时工作线程数为0,那么调用addWorker(null,false),创建一个没有绑定任务的非核心线程,当runWorker()中的getTask()方法获取到任务后,会将阻塞队列中的任务置换到Worker对象中的null任务;

③ 若任务【工作线程数 > 核心线程数】并且 【阻塞队列已满】,那么也调用addWorker(任务,true)方法;
Step3 执行addWorker(任务,boolean)方法,返回值是boolean;

任务执行成功为true,执行失败则为false;
① 首先调用wokerCount(),获取当前线程池工作线程数量,若【工作线程数 > 线程池最大容量】,则返回false,并执行【拒绝策略】

② 如果没有执行拒绝策略,那么正常执行;创建一个Worker对象,并与当前要执行的任务绑定【即new Worker(任务)】,将这个Worker对象添加到Workers容器(HashSet)中;若添加成功则在addWorker()的最后调用了Worker中任务的start()方法,使得任务进入就绪状态,等待run()方法执行;

Worker对象继承了AQS并实现了Runnable接口,因此可以当作一个并发安全的线程使用,线程池中真正工作的线程就是绑定了任务的Worker线程;
Step4 分配一个线程执行run()方法,底层调用的是runWorker(Worker w) 方法
其中还有一个getTask()方法
runWorker(Worker w) 方法取出Worker对象中封装的Runnable任务

② 若任务为null,那么就while死循环的调用getTask()方法,从阻塞队列中获取任务

getTask()方法通过调用workcount()方法获取当前线程池的工作线程数量,判断当前线程是核心线程还是非核心线程1) 若是非核心线程:那么就调用workQueue.poll(keepAliveTime)方法,在存活时间内从阻塞队列中获取任务,若超过了keepAliveTime还没有获得任务,则该非核心线程就被回收掉,这就是【非核心线程被回收的原理】2) 若是核心线程:那么就调用workQueue.take()一直从阻塞队列中获取任务,直到获取成功,没有期限【这也是为什么核心线程能够不被回收的原理】

④ 获取到任务后,Worker对象内部的空Runnable任务会置换为阻塞队列中获取的Runnable对象,来达到线程复用的效果;任务不为null之后,那么就直接执行接下来的逻辑;首先执行beforeExecute()方法,做一些线程执行任务之前的工作;然后执行任务.run(),开始真正的执行任务;最后在执行afterExecute()方法,做一些线程执行任务完的一些工作;

⑤ 当任务执行结束后,就会从Workers容器中移除

4、被线程池拒绝掉的那部分用户的秒杀令牌还有效吗?

无效,会从redis中删除。当该用户再去秒杀抢购时,需要重新获取秒杀令牌。


十二、项目中的JVM问题?

1、项目上线之后想看JVM的GC情况在Linux中用什么命令?

jstat -gc vmid count
jstat -gc 12538 5000 // 表示将12538进程对应的Java进程的GC情况,每5秒打印一次

…其它JVM问题待补充


十三、你项目中的难点是什么?

1、分布式会话实现的原理

难点 描述
问题 在单机环境下时,由于HTTP协议是无状态的,导致服务器无法记录我们的登录状态,此时我们是可以用【cookies】+【session】解决的。

但是进行分布式扩展后,会发现我们在已经登陆后,分别访问不同的微服务时,系统依然提示我们去登录。
原因 针对这个问题,我查阅了相关资料。发现session的本质实际上时tomcat为我们创建的一个对象,它使用ConcurrentHashMap保存属性值。 但是tomcat本质也是一个Java程序,由于每个微服务监听的是不同的端口。那么从一个tomcat容器中(8084)创建的session,另外一个tomcat容器(8085)是肯定获取不到的,因此也就出现了分布式扩展后,提示我们登录的问题。
解决 针对分析的原因后,我们可以通过【设置cookie跨域分享】+ 【redis存储token】+ 【SpringSession】的方式解决。解决原理如下:

(1)首先针对tomcat这个问题,我通过定义一个CookieSerializer方法,放大Cookies的作用域,以方便进行跨域。

(2)用户在某个微服务中登录后,将session中的用户登录信息以token的形式存储在redis中。

(3)然后开始引入SpringSession。具体的:

  ① 通过@EnableRedisHttpSession注解,导入RedisHttpSessionConfiguration配置;

  ② 该注解给SpringBoot容器中添加了一个组件SessionRepository,这个类是session的增删改查封装类;

  ③ 配置完成后,每个request请求都会经过一个SessionRepositoryFilter类,这个类是一个增强器,它的内部将原始的request、response进行了包装增强,形成了新的类( SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper)。

  ④ 经过包装的request,在调用request.getSession()方法时,实际上调用的是SessionRepositoryRequestWrapper的getSession()方法,相当于从redis中获取session。从而实现了分布式session会话。

至此,解决了分布场景下提示登录的问题。

2、Feign远程调用时请求头丢失的问题

项目中难点 描述
背景 我们在写商城服务端项目的时候,总会限制对某些资源的访问,最常见的就是要求用户先登录才能访问资源。一般情况下,当用户登录后就会将此次会话信息保存进session,同时返回给浏览器指定的cookie键值,下次浏览器再次访问,请求头中就会携带这个cookie,我们也以次来识别用户的登录状态,做出正确响应。
问题 在项目的订单系统中,生成订单的方法里,首先要通过Feign进行远程调用库存服务进行库存锁定,然后再生成订单。但是却显示远程调用失败,原因是库存服务说我们没有登录。但是我们从浏览器直接调用库存服务的接口却能得到成功的响应,也就是说通过Feign远程调用时,请求头的数据丢失了。
查看Feign源码解决问题的过程 针对Feign远程调用时请求头丢失的问题,我查阅了Feign的底层源码,发现在 executeAndDecode() 方法内部,Feign通过targetRequest()方法构造出了一个新的request对象,库存服务的response就是对这个request请求的响应,但是这个request中不含有原来request中的任何信息。因此,会出现库存服务提示我们未登录的现象。
解决过程 (1)问题在于feign自己创建出requestTemplate,再用它构建一个新的request对象去发送请求,而这个新的request不包含任何请求头信息。我们应该在它创造出这个request之后,在它真正发送请求之前,把原始请求头中的数据给它复制过去。

(2)然后我看到最后Feign最后构建出创建request对象的 targetRequest方法里边,调用了一系列的RequestInterceptor中的apply()方法进行了增强,最后才返回,但是默认情况下这些拦截器都是空的。

(3)因此 ,我们需要自己实现一个 RequestInterceptor 拦截器,在它的apply()方法中将原始请求头中的数据复制到 Feign 创建出的新的request中,并且将这个拦截器注入Spring容器中,这样Feign在执行目标方法之前会被其拦截,先对其进行增强后,然后再调用。
新的问题 解决了Feign远程调用过程中request请求头丢失的问题后,在生成订单的方法内,为了提高订单封装的效率。通常会使用异步编排的方式去优惠服务、库存服务、用户服务等查询相关信息进行封装,但是在我们实现了RequestInterceptor 拦截器后,依然会提示我们未登录。

后边又查看了相关源码,才知道当Feign调用出现在异步线程体内的时候,RequestInterceptor拦截器拦截到当前线程后,再使用RequestContextHolder获取的已经不是原来线程了,而获取的是线程池中不携带原请求头信息的线程。因此会出现,依然未登录的提示。
解决异步编排下的Feign请求头丢失问题 为了解决这个问题,我们可以提前同步数据,保证线程池中进行异步编排的的线程都有着主线程的请求头信息。具体地:

在进入新线程之前,可以拿出原线程绑定的requestAttributes,在新的线程体内,feign调用之前,将其赋值到本线程绑定的request中。这样,在执行Feign远程调用时,被拦截器拦截时,当前线程就已经持有了主线程中地请求头信息,就不会出现未登录的问题了。

你可能感兴趣的:(redis,缓存,java)