订单系统秒杀与抢购的设计
2016-07-17 19:35:49 最后更新: 2016-07-17 19:35:49 访问数量:2597
2016-07-17 19:35:49 最后更新: 2016-07-17 19:35:49 访问数量:2597
高并发的抢购、秒杀功能是一个 web 系统面临的很大的一个挑战
由于销售平台的促销活动,销售系统的 web 后台接口将承受平常几倍甚至几十倍的压力,这样,服务器的 CPU、内存等是否会成为保证服务质量的瓶颈,如何顺利度过抢购、秒杀的高峰期,怎么让有限的资源承受突如其来的压力就成了服务端工程师不得不考虑的一个问题了
在此前的博客中,我们介绍了 web 服务需要考虑的六大因素
其中,我们介绍了如何构建稳定、可持久的 web 服务,应对高并发、高请求量的实际访问压力,然而,秒杀环节中,仅仅为了流量的巨大、临时性增长,而去扩容一套可以应对相应流量的系统,显然是十分浪费而又不现实的,因此,这就需要我们在选择去拒绝一部分访问流量,从而降低后台服务器的压力,提高服务的可用性
那么如何选择需要拒绝的那部分流量呢?
一旦促销活动开始进行,往往会有大量用户反复抢购,为原本压力巨大的服务造成了雪上加霜的效果,根据 IP 来限制用户的访问就成了一个比较好的方案
nginx 可以方便的使用 limit_req_zone 和 limit_req 指令配合使用来达到限制,一旦 IP 访问过多,就会返回 503 错误,我们只需要再配置 503 错误的静态页面返回友好的提示信息即可
上面的这个简要的配置定义了一个 allips 的限制规则,他使用 10M 内存来存储访问信息,同时,它限制了每个 IP 每秒不能超过 20 个请求
burst 配置意味着允许在 5 秒的时间内 IP 有一次超过 20 个请求
nodelay 配置需要在设置 burst 配置以后进行配置,他意味着,允许的这一次超过 20 个请求的一秒钟,服务器只处理 20 个请求,剩余的 5 次请求将 delay 到下一秒进行处理
通过上述配置,我们就实现了限制一个 IP 每秒最多的访问次数
当然,有了上述 nginx 配置的思路,我们也可以通过 session 缓存进行我们自己丰富的自定义的处理
可以在加密的用户认证 cookie 中写入用户已经进行的访问次数,让超过一定次数的用户自动退出登录
也可以通过 session 缓存中加入更多的逻辑与限制
抢购场景中,最容易遇到的问题就是超卖问题
上面的图中,展示了超发问题,也就是说多个用户在查询商品是否有剩余时,商品仍然有剩余,此时他们都认为自己可以下单并抢购成功,结果,当他们同时抢购成功时,商品数量已不足以扣减,导致了支付成功、商品不足的问题
这样的问题是并发销售系统尤其是在抢购、秒杀活动中最需要关注和解决的问题
异步处理即是针对并发查询商品量的环节来解决这个问题,将抢购与下单进行分割,用户抢购后并不知道自己是否抢购成功,直到系统处理完毕
通常的做法是,将 N 倍商品剩余量的用户请求放入消息队列或缓存,由有限个 worker 进程对这些请求进行消费,每个被 worker 处理的请求即为抢购成功请求
用户得知自己抢购成功后,通常系统会限定用户须在一定时间内进行支付,否则视为放弃,此时,worker 在规定时间间隔后再次启动,对队列中剩余请求进行消费,并重复上述过程
这样一来,有限个 worker 对队列中有限个请求的消费就变成了完全可控的操作,从根源上杜绝了超发的可能
然而,这样做的不足是显而易见的,首先,评估队列中存储多少个请求成为了一个不确定因素,是否会有大量抢购成功的用户放弃支付呢?其次,在用户体验上,用户需要等待 worker 处理完成才能知道是否抢购成功,很大程度上降低了焦急等待的用户的用户体验,当然了,除非我们可以让这样的异步过程瞬时完成,让用户认为是同步过程
解决并发问题的一个重要思路就是加锁
我们可以在缓存中放置一个锁变量,可以通过 redis hash 结构实现
每个请求都原子性的增加锁变量的值(执行 hincrby 并关注返回值),一旦锁变量达到某一阈值(库存量),则对所有请求返回暂时等待,直到由于此前的某个请求未能支付成功
事实上,这样的解决方案与上述的异步处理方案有很多相似之处,不过对于抢购成功的用户而言,他们的购买流程是同步的,提升了用户体验
在悲观锁的基础上,我们可以利用 redis 的事务功能实现乐观锁
所有用户都在抢购环节开始时执行 redis 的 multi 操作开启事务,并 watch 同一锁变量,直到提交订单前执行 exec 指令提交事务,提交成功则可以提交订单
我们需要做的仅仅是控制这个锁变量什么时候发生改变
欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤