秒杀项目常见问题

1.讲一下你这个高并发抢购系统(秒杀)

        是什么,用了什么(Redis、动静分离、隐藏链接、限流、削峰),压测结果
        这个系统主要是模拟秒杀活动,实现一个支持高并发的服务。
        为了解决高并发带来的问题,我用Redis实现了缓存预热、预减库存的功能,通过使用Nginx实现动静分离来降低服务器的压力,加快用户访问速度。隐藏了秒杀接口地址,防止用户提前知道秒杀地址。还通过输入验证码的方式防止恶意秒杀,同时还起到了削峰的作用。最后通过Jmeter压力测试,QPS从最初的150/s提升到2000/s左右。

2.秒杀中如何处理超卖问题?

        在定时上架秒杀商品时,使用分布式信号量作为商品的库存缓存到Redis中,当活动开始接收到秒杀请求时,在Redis中进行预减库存,如果Redis中的库存不足时,直接返回秒杀失败;如果有库存,就将请求放入异步队列中,给用户返回一个秒杀成功,正在创建订单,然后订单服务监听这个队列,生成秒杀订单,减少数据库库存等一系列操作。

3.秒杀中如何解决重复下单问题?(普通订单也一样操作)

        mysql唯一索引(商品索引)+ 分布式锁

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

        因为每一场的秒杀活动持续的时间都是确定的嘛,所以可以把热点数据的过期时间设置的跟活动持续时间一样,这样活动一结束,热点数据自己就删除了,也不会导致在活动期间数据就失效了。

5.Redis缓存和数据库数据一致性如何保证?

        使用canal组件实现(canal的原理,模拟MySQL的主从复制机制)
        更新数据库后立即删缓存,然后下一次查缓存找不到数据后会再次从数据库同步到缓存。

6.★减库存成功了,但是生成订单失败了,该怎么办?(分布式事务的数据一致性问题)

        使用mq来解决这个问题。如果扣减库存成功了但是下单失败了,订单服务就给mq发送一个消息,然后库存服务就来监听这个mq,如果能从mq中拿到消息说明订单失败了,然后就进行回滚操作。
【tips】seata虽然能控制分布式事务,但是不适合高并发,像下订单就是一个高并发操作,所以我们不用seata来实现下订单的分布式事务。因为seata实现分布式事务需要加锁,一加锁相当于把高并发变成串行化了

7.做了什么限流削峰的措施?

(1)点击秒杀按钮以后要先填一个验证码(前端);
(2)在定时上架秒杀商品时,使用分布式信号量作为商品的库存缓存到Redis中,当活动开始接收到秒杀请求时,在Redis中进行预减库存,如果Redis中的库存不足时,直接返回秒杀失败;

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

        如果客户想伪造请求来快速秒杀,通过我们的链接加密可以实现。
        使用脚本的恶意请求可以用验证码来解决。

9.多机器扣减库存,如何保证它的线程安全的?

        用redission实现分布式锁。在扣减库存前,先获取分布式锁getLock(),然后上锁lock,再进行扣减,最后再释放锁。

(1)如果扣减失败,没有释放锁会导致什么情况?怎么解决?

        会造成死锁。
        给锁设置过期时间,防止出意外释放不了锁造成死锁。

(2)怎么保证释放的就是自己的锁?

        在getLock时可以给锁一个名字,只要名字一样就是同一把锁。

10.如何去减Redis中的库存?

        在秒杀系统中Redis中的商品库存是用分布式信号量存的,通过tryAcquire()来获取信号量,获取信号量的操作就相当于扣减了库存。
【tips】tryAcquire()是不阻塞的,tryAcquire()不传参是获取一个信号量,tryAcquire(int n)是获取n个信号量(有这么多的话)。

11.缓存中的数据突然失效,导致请求全部打到了数据库,如何解决?

        (典型的缓存雪崩问题)
        给缓存中的数据的过期时间加随机数

12.如果项目中的Redis挂掉,如何减轻数据库的压力?

        可以搭建redis集群,通过哨兵机制来监控主节点,如果主节点挂了就从从节点中选一个新的主节点,实现故障转移。

13.页面静态化

        那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

14.秒杀系统面临的问题有哪些?

        高并发、脚本恶意请求、加了缓存之后的缓存三大问题(击穿、穿透、雪崩)。

15.秒杀系统如何设计?

        独立部署+动静分离+限流+熔断降级+缓存预热+异步订单+秒杀链接加密
(1)独立部署
        把秒杀系统单独作为一个微服务,这样一旦秒杀系统实在顶不住了,也不会影响其他服务。
(2)动静分离
        使用Nginx实现动静分离,让静态资源从nginx直接返回,减轻服务器的压力。
(3)限流
        前端限流+后端限流。
        前端可以使用一个验证码,通过手速快慢可以进行一部分限流,还可以过滤机器人的恶意请求。
        后端可以使用sentinel在网关设置一些限流规则,比如url必须正确;还可以用分布式信号量,在Redis中保存用分布式信号量代替库存,抢不到信号量就不用去查数据库了。
(4)熔断降级
        如果出现问题,给前端返回一个快速失败的提示,将用户引导到一个降级页面,这样可以保证整个调用链不是阻塞的。
(5)缓存预热
        使用定时任务在每天凌晨三点上架要秒杀的商品,将商品信息提前保存到Redis中。
(6)异步订单
        秒杀成功后发送消息给mq然后给用户返回"秒杀成功,正在创建订单",让订单服务监听mq,然后异步进行订单创建、扣减库存等操作,这样可以防止一条秒杀请求阻塞等待订单完成才给用户返回结果。
(7)秒杀链接加密
        缓存商品数据时给每个商品随机生成一个uuid,在发送秒杀请求时,必须保证请求带上skuId和这个随机码才能进来。

16.分布式会话问题?

        在实现用户社交登录时,发现有时候已经登录过了但是系统还是会提示去登录,后来发现是session和cookie的问题,这个问题其实是在分布式下session不能共享的问题。用户第一次登录时把登录信息放在session里(session.setAttribute("sessionName",value)),然后把session保存在服务器里,并把token返回给浏览器的cookie,这样用户第二次请求时就带着有token的cookie,服务器就认为已经登陆过了。但是在分布式下会存在一些问题,首先这个过程只能在当前域名下(同一微服务)起作用,其次如果有多台服务器的话,两次请求转发到不同的服务器,即使是同一服务也不能共享session数据。
        解决方案是通过SpringSession,自定义一个cookie的序列化器,先将作用域扩大到整个项目(setDomainName())。同时在用户第一次登录时,将session都存到Redis里,让所有服务都去Redis里根据sessionId去获取对应的session,
        然后通过设置cookie跨域分享以及利用redis存储token信息得以解决。

17.线程池的执行过程?

        当任务进来后,会先判断线程池中的核心线程数是否小于corePoolSize,如果小于的话会直接创建一个核心线程去处理任务;如果大于说明没有核心线程了,就将任务放入阻塞队列中等待。如果核心线程和阻塞队列都满了,就创建非核心线程,去处理阻塞队列中的任务。当线程数达到了maximumPoolSize并且阻塞队列也满了,就会采用拒绝策略来处理任务。

18.你项目中的难点是什么?

(1)Feign远程调用丢失请求头问题
        在进行远程调用时cookie需要带上sessionId,来标注用户登录状态的session,但是发现还是提示要先登录,而且cookie中并没有sessionId。后来查了资料发现feign在远程调用时会先构造一个新的请求,而这个新请求里面并没有请求头,所以导致没有携带sessionId。
        解决:feign在构造请求之前可以经过请求拦截器(RequestInterceptor),通过拦截器的apply方法可以给请求加一些东西,但是默认情况下是没有拦截器的,那么我们就可以自己定义一个RequestIterceptor并重写apply方法,通过RequestContextHolder来获取原来请求中的sessionId,然后让后面feign构造的新请求携带上sessionId。
        
(2)用户登录的问题(分布式session)
        在实现用户社交登录时,发现有时候已经登录过了但是系统还是会提示去登录,后来发现是session和cookie的问题。用户第一次登录时把登录信息放在session里(session.setAttribute("sessionName",value)),然后把session保存在服务器里,并把token返回给浏览器的cookie,这样用户第二次请求时就带着有token的cookie,服务器就认为已经登陆过了。但是在分布式下会存在一些问题,首先这个过程只能在当前域名下(同一微服务)起作用,其次如果有多台服务器的话,两次请求转发到不同的服务器,即使是同一服务也不能共享session数据
        解决方案是通过SpringSession,自定义一个cookie的序列化器,先将作用域扩大到整个项目(setDomainName())。同时在用户第一次登录时,将session都存到Redis里,让所有服务都去Redis里根据sessionId去获取对应的session。

19.项目中Redis都做了些什么?

(1)在项目中用Redis保存购物车数据;
(2)通过令牌机制实现接口的幂等性,将token保存在Redis中
(3)在实现社交登录时,将session存在Redis中来解决session共享问题
(4)在实现秒杀时,将秒杀商品信息缓存到Redis中。

20.项目中RabbitMQ都做了什么?

        作为异步下单的中间件,实现解锁库存定时关单
(1)解锁库存
        (超时未支付、用户自己取消订单、锁完库存但是其他远程调用失败了导致的订单回滚都要解锁库存)
        当需要解锁库存的时候,比如说订单回滚了,就给交换机发送一个消息,然后交换机根据路由键转发给解锁mq库存解锁服务监听到这个消息以后,就根据消息进行库存解锁的操作。——可以涉及到方法重载
【库存工作单表】记录了库存锁了多少,是哪个订单锁的,方便回滚。
(2)定时关单
        当订单创建成功了,就给交换机发送(rabbitTemplate.convertAndSend())一个消息(订单实体对象),然后交换机根据路由键将消息转发到延迟队列,延迟队列设置了30min的过期时间,当30分钟一过,延迟队列会把过期的消息,再发给交换机,由交换机转发给关单的消息队列,由关单服务进行监听,关单服务拿到消息以后要先判断订单状态,如果是待支付状态,就进行关单,把订单状态设置为已取消;如果是其他状态就不用操作了。

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

        CPU密集型业务:N+1
        IO密集型业务:2N

22.TPS(每秒处理的事务)提升了多少?如何提高tps?

        基础架构下的tps是200,经过做动静分离、引入redis缓存、nginx反向代理(集群)后达到了2000左右。

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

      (多台设备登录属于SSO问题,用户登录一端之后另外一端可以通过扫码等形式登录。
        虽然用户登录了多台设备,但是用户名是一样的,所以为用户颁发的token是相同的,只会颁发一个token。

24.线程池的拒绝策略能详细说一下吗?

  • AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException拒绝执行异常
  • DiscardPolicy:丢弃(discard)任务,但是不抛出异常;
  • DiscardOldestPolicy:丢弃位于阻塞队列最前面的任务(也就是最早进来的任务),然后重新提交被拒绝的任务
  • CallerRunsPolicy:由提交任务的线程处理该任务。

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

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

26.线程池中阻塞队列的大小设置为多少合适?

        设置为(秒杀商品的个数 - 核心线程数)。这样的话所有秒杀任务最终都可以被处理,而不会说明明有库存,用户秒杀了但是队列满了导致秒杀任务被丢失了。

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

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

29.能不能详细描述一下使用MQ异步减redis与MySQL库存的过程?

        redis中的库存减成功后,生成一条消息,包含了商品信息(包含了库存信息)和用户信息,然后将消息发送给交换机;交换机转发到生成订单服务监听的队列,然后该服务会消费这条消息,根据商品信息和用户信息创建一条订单,并修改数据库中对应商品的库存

30.做到了什么程度、库存量与并发度是多少?

        QPS:单机2000/s

31.秒杀系统中MySQL中的表是怎么设计的?

        秒杀用户表、商品信息表、秒杀商品表(记录该商品的秒杀始末时间,秒杀价和剩余量)、秒杀订单表(记录了秒杀用户名和秒杀的商品还有订单号)、订单详情表(通过秒杀订单号来查找对应的订单详情,里面记载更详实的业务信息)。

32.如何只使用MySQL保证商品没有超卖?

        将查库存、减库存两个sql语句作为一个事务进行控制,保证每一个库存只能被一个用户消费。两条语句都执行成功进行事务提交,否则回滚。
        缺点:低并发。

33.数据库改库存的SQL?

        update table set stock = stock-1 where sku_id = ? and stock > 1;

34.如何防止用户一直点击下单按钮?(防止重复下单、幂等性问题)

        前端限制:一次点击之后按钮置灰几秒钟。
        后端限制:由于秒杀令牌的设置,用户的一个下单请求会先判断用户当前是否已经持有令牌了,因为用户全局只能获取一次令牌,然后存入到Redis缓存中。用户有令牌的话直接返回 “正在抢购中”。

35.定时任务的框架选型?为什么选Quartz?

        我了解的实现定时任务的方法有Java自带的,比如Timer和ScheduledExecutor,还有Quartz,我在项目中用的就是Quartz。
  • Timer是通过创建一个TimerTask,实现自己的run方法,然后通过Timer调用schedule方法来调度这个任务,可以指定延迟一段时间后再调度。Timer的缺点就是所有的任务都是用一个线程来调度的,也就是说这些任务是串行执行,不适合用在分布式中。
  • ScheduledExecutor是基于线程池设计的,每一个调度的任务都由线程池中的一个线程去执行,所以说这些任务时并发执行的。但是ScheduledExecutor只能实现基于开始时间与重复间隔定时任务,像设置每天3点执行一个任务就实现不了了,所以也不满足我的需求。
  • Quartz支持分布式调度任务,可以通过Cron表达式来实现更复杂的定时任务。
    怎么使用Quartz?
            使用异步执行定时任务,来保证定时任务不阻塞。默认是阻塞的,需要等待上一次任务执行完才能执行下一个。因为默认情况下,定时任务线程池只设置了一个线程。
    首先要在配置类上@EnableSchedule开启定时任务功能,用@EnableAsync开启异步。然后创建一个调度类,在里面调用定时上架商品的业务逻辑(service),在方法的上面用@Scheduled注解标注每天3点的cron表达式。

36.定时上架商品的流程?

        一到我们指定的时间,会先远程调用优惠服务,查出最近三天的秒杀活动(select * from 秒杀活动表 where starttime between xxx and xxx;),如果能查到最近三天的秒杀活动,再查活动关联的秒杀商品信息。然后把活动场次和商品信息缓存到Redis,同时用分布式信号量作为库存进行扣减,只要有一个秒杀请求进来信号量就减1,减到0就说明没库存了。
  • 活动场次用起止时间作为key,关联的skuId作为value,这个value用list存;
  • 商品信息用hash存,hash的key用skuId,剩下的商品信息就是hash的value。
        分布式信号量要用Redisson的getSemaphore()方法获取,相当于创建了一个信号量对象,然后通过trySetPermits(库存数量)设置有几个信号量也就是有多少库存
        

37.如何进行秒杀链接加密?

        在缓存商品信息时,给每个sku设置一个uuid随机码,这样即使有人知道要秒杀商品的id,也无法提前写好请求地址进行恶意秒杀。
        

















全部评论
Wc,你们怎么这么吊
1
送花
回复
分享
发布于 04-23 11:29 北京
老哥,请问你做的是哪个秒杀项目呀
点赞
送花
回复
分享
发布于 2023-04-07 17:13 北京
滴滴
校招火热招聘中
官网直投
佬,请问你做的哪个项目
点赞
送花
回复
分享
发布于 2023-05-04 11:08 日本
m
点赞
送花
回复
分享
发布于 2023-09-26 16:19 陕西

相关推荐

23 283 评论
分享
牛客网
牛客企业服务