目录

设计一个秒杀系统

所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。

秒杀应该考虑哪些问题

超卖问题

分析秒杀的业务场景,最重要的有一点就是超卖问题,假如备货只有100个,但是最终超卖了200,一般来讲秒杀系统的价格都比较低,如果超卖将严重影响公司的财产利益,因此首当其冲的就是解决商品的超卖问题。

高并发

秒杀具有时间短、并发量大的特点,秒杀持续时间只有几分钟,而一般公司都为了制造轰动效应,会以极低的价格来吸引用户,因此参与抢购的用户会非常的多。短时间内会有大量请求涌进来,后端如何防止并发过高造成缓存击穿或者失效,击垮数据库都是需要考虑的问题。

接口防刷

现在的秒杀大多都会出来针对秒杀对应的软件,这类软件会模拟不断向后台服务器发起请求,一秒几百次都是很常见的,如何防止这类软件的重复无效请求,防止不断发起的请求也是需要我们针对性考虑的

秒杀url

对于普通用户来讲,看到的只是一个比较简单的秒杀页面,在未达到规定时间,秒杀按钮是灰色的,一旦到达规定时间,灰色按钮变成可点击状态。这部分是针对小白用户的,如果是稍微有点电脑功底的用户,会通过F12看浏览器的network看到秒杀的url,通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人,一请求就直接实现秒杀了。这个问题我们需要考虑解决

数据库设计

秒杀有把我们服务器击垮的风险,如果让它与我们的其他业务使用在同一个数据库中,耦合在一起,就很有可能牵连和影响其他的业务。如何防止这类问题发生,就算秒杀发生了宕机、服务器卡死问题,也应该让他尽量不影响线上正常进行的业务

大量请求问题

按照1.2的考虑,就算使用缓存还是不足以应对短时间的高并发的流量的冲击。如何承载这样巨大的访问量,同时提供稳定低时延的服务保证,是需要面对的一大挑战。我们来算一笔账,假如使用的是redis缓存,单台redis服务器可承受的QPS大概是4W左右,如果一个秒杀吸引的用户量足够多的话,单QPS可能达到几十万,单体redis还是不足以支撑如此巨大的请求量。缓存会被击穿,直接渗透到DB,从而击垮mysql.后台会将会大量报错

  • order_id
  • user_id
  • Product_id
  • State 1已支付 2 未支付
  • create_time

id

id
product_id
Product_name
stock 库存
version
start_time
end_time

瞬时高并发的场景

  • 页面静态化
  • CDN加速
  • 秒杀按钮置灰
  • 读多写少改用缓存,比如:redis。
    • 缓存击穿,分布式锁,针对这种情况,最好在项目启动之前,先把缓存进行预热。
    • 缓存穿透,就需要把不存在的商品id也缓存起来。需要特别注意的是,这种特殊缓存设置的超时时间应该尽量短一点。不然容易占用过多内存,最好通过布隆过滤器过滤一次。

mq 异步处理 异步下单

真实的秒杀场景中,有三个核心流程:秒杀、下单、支付。真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。

防止消息丢失

  • 加一张消息发送表。(也可解决重复消费问题)
  • 事务消息

重复消费问题,加一张消息发送表,下单和写消息处理表,要放在同一个事务中,保证原子操作。

延迟消费问题,通常情况下,如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会被自动取消,回退库存。使用延迟队列。rocketmq,自带了延迟队列的功能。

限流、降级、熔断、隔离

这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了

  • 限流,顶不住就挡一部分出去但是不能说不行

  • 降级,降级了还是被打挂了

  • 熔断,至少不要影响别的系统

  • 隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。

  • 基于nginx限流

  • 基于redis限流

一种验证码叫做:移动滑块,它生成速度比较慢,但比较安全,是目前各大互联网公司的首选。

  • 对同一用户限流
  • ip 限流
  • 接口限流
  • 加验证码

限流这里我觉得应该分为前端限流后端限流

前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。

后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。

Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。

库存问题

扣减库存 –> 创建订单 –> 支付,其他顺序会导致“超卖”的现象

扣减库存,一般需要先查询是否有库存,这2个操作非原子的。可以通过加锁,但更好的是通过数据库乐观锁实现。

1
update product set stock = stock-1 where id = 10 and stock>0;

在高并发的场景下,可能会造成系统雪崩。而且,容易出现多个请求,同时竞争行锁的情况,造成相互等待,从而出现死锁的问题。

库存预热

秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。

redis 扣减库存

redis的incr方法是原子性的,可以用该方法扣减库存。

lua脚本扣减库存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
StringBuilder lua = new StringBuilder();
  lua.append("if (redis.call('exists', KEYS[1]) == 1) then");
  lua.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
  lua.append("    if (stock == -1) then");
  lua.append("        return 1;");
  lua.append("    end;");
  lua.append("    if (stock > 0) then");
  lua.append("        redis.call('incrby', KEYS[1], -1);");
  lua.append("        return stock;");
  lua.append("    end;");
  lua.append("    return 0;");
  lua.append("end;");
  lua.append("return -1;");
  1. 先判断商品id是否存在,如果不存在则直接返回。
  2. 获取该商品id的库存,判断库存如果是-1,则直接返回,表示不限制库存。
  3. 如果库存大于0,则扣减库存。
  4. 如果库存等于0,是直接返回,表示库存不足。

分布式锁

使用redis分布式锁,还有锁竞争问题、续期问题、锁重入问题、多个redis实例加锁问题等。这些问题使用redisson可以解决。

这做法效率低下,不合适做秒杀。简单的方法可以在 Redis 里维护虚拟库存,用原子性减操作来判定是否超卖。