下单页面如何将请求拦截在上游
用户进入下单页面时,主要有两个操作动作:进入下单页面、提交订单。
进入下单页面
为了防止别人通过爬虫抓取下单页面信息,从而给服务器增加压力,需要在下单页面做以下两层防护,从而防止恶意请求重复提交。
页面URL后台动态获取:按照正常的活动设计流程,用户只有在秒杀活动开启后才可进入下单页面,但难免有人在活动开启前直接获取其URL并不断刷新,这样恶意请求就到了后台服务器。虽然后台服务器也可以拦截恶意请求,但是这会给它徒增不少压力。此时主要使用一个特别的URL进行处理(不把它放在静态页面中,而是通过后台动态获取)。前面介绍了JS可以用来判断秒杀开始时间,秒杀时间一到,它便可以通过另一个请求获取这个URL。
用户点击下单页面的购买按钮后,将此按钮设为Disable(不可用),防止用户不断点击它。
提交订单
在订单提交环节,要想尽一切办法在系统各个分层中把一些不必要的请求过滤掉。
网关层面过滤请求
对系统而言,如果可以在网关层面拦截用户请求,那么这个方案的性价比就很高。要是能在这一层过滤95%以上的请求,整个系统也将很稳定。
在网关层面可以做 3 种限制。
限定每个用户的访问频率,比如每5秒下单一次。
限定每个IP的访问频率。这种方式是为了避免有人通过机器人自动下单,导致错杀真实用户。
把一个时间段内的请求拦截掉一定比例,或者只允许特定数量的请求进入后台服务器。这里可以使用限流的漏桶或令牌桶算法。
前两种限制比较简单,在nginx上就能快速完成配置。
后台服务器过滤请求
请求进入后台服务器后,目标已经不是如何过滤请求了,而是如何保证特价商品不超卖,以及如何保证特价商品订单数据的准确性。
主要考虑以下4点:
商品库存放入缓存Redis中:如果每个请求都前往数据库查询商品库存,数据库将无法承受,因此需要把商品库存放在缓存中,这样每次用户下单前,就先使用decr操作扣减库存,判断返回值。
如果Redis的库存扣减后小于0,说明秒杀失败,将库存用incr操作恢复(为了退货操作能够正确执行);
如果Redis的库存扣减后不小于0,说明秒杀成功,开始创建订单。
把库存放入Redis时,下单的逻辑都是基于缓存的库存为第一现场。但是如果这时候有别的服务或者代码修改了数据库里面的库存,怎么办?这时的做法就是确保在秒杀期间不做上架或修改库存之类的业务操作,即不通过技术,而是通过业务流程来保证。
订单写入缓存中:订单数据先不放入数据库,而是放到缓存中,然后每隔一段时间(比如100毫秒)批量插入一批订单。
用户下单后,首先进入一个等待页面,然后这个页面向后台定时轮询订单数据。
轮询过程中,后台先在Redis中查询订单数据,查不到就说明数据已经落库,再去数据库查询订单数据,查到后直接返回给用户,用户收到消息通知后可以直接进入付款页面支付;
在数据库查询订单数据时,查不到说明秒杀失败(理论上不会查不到,如果一直查不到就需要抛出异常并跟踪处理)。
订单批量落库:需要定期将订单批量落库,且在订单落库时扣减数据库中的库存。
Redis停止工作(挂掉)怎么办:在秒杀架构里面,最重要的是网关层的限流,它挡住了大部分的流量,进入后台服务器的流量并不多。不过仍然要考虑针对Redis停止工作的情况,分别处理前面的3种状况:
读Redis中的库存时,如果失败了,那就让它直接去数据库扣减库存,把那些incr和decr的逻辑放到数据库去;
若是把订单写入缓存的时候失败了,那就直接将订单数据写入数据库中,然后就不需要处理后面批量落库的逻辑了。