秒杀的基本知识点,了解一下(一)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
应用型负载均衡 ALB,每月750个小时 15LCU
简介: 秒杀的基本知识点,了解一下

1. 大型高并发系统架构


高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。下边是一个简单的示意图:


0afc43ef9cd5812eafac3b162bfcbc5a_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


1.1 负载均衡简介


上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡:

  • OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称IGP)。OSPF通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF会自动计算路由接口上的Cost值,但也可以通过手工指定该接口的Cost值,手工指定的优先于自动计算的值。OSPF计算的Cost,同样是和接口带宽成反比,带宽越高,Cost值越小。到达目标相同Cost值的路径,可以执行负载均衡,最多6条链路同时执行负载均衡。
  • LVS (Linux VirtualServer),它是一种集群(Cluster)技术,采用IP负载均衡技术和基于内容请求分发技术。调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。
  • Nginx想必大家都很熟悉了,是一款非常高性能的http代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。Nginx实现负载均衡的方式主要有三种:轮询、加权轮询、ip hash轮询,下面我们就针对Nginx的加权轮询做专门的配置和测试


1.2 Nginx加权轮询的演示


Nginx实现负载均衡通过upstream模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。下面是一个加权轮询负载的配置,我将在本地的监听3001-3004端口,分别配置1,2,3,4的权重:

#配置负载均衡
    upstream load_rule {
       server 127.0.0.1:3001 weight=1;
       server 127.0.0.1:3002 weight=2;
       server 127.0.0.1:3003 weight=3;
       server 127.0.0.1:3004 weight=4;
    }
    ...
    server {
    listen 80;
    server_name load_balance.com www.load_balance.com;
    location / {
       proxy_pass http://load_rule;
    }
}

我在本地/etc/hosts目录下配置了 www.load_balance.com的虚拟域名地址,接下来使用Go语言开启四个http端口监听服务,下面是监听在3001端口的Go程序,其他几个只需要修改端口即可:

package main
import (
  "net/http"
  "os"
  "strings"
)
func main() {
  http.HandleFunc("/buy/ticket", handleReq)
  http.ListenAndServe(":3001", nil)
}
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
  failedMsg := "handle in port:"
  writeLog(failedMsg, "./stat.log")
}
//写入日志
func writeLog(msg string, logPath string) {
  fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
  defer fd.Close()
  content := strings.Join([]string{msg, "\r\n"}, "3001")
  buf := []byte(content)
  fd.Write(buf)
}


我将请求的端口日志信息写到了./stat.log文件当中,然后使用ab压测工具做压测:

ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket


统计日志中的结果,3001-3004端口分别得到了100、200、300、400的请求量,这和我在nginx中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。具体的实现大家可以参考nginx的upsteam模块实现源码,这里推荐一篇文章:Nginx 中 upstream 机制的负载均衡


2.秒杀抢购系统选型


回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢?从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的QPS也是非常高的。如何将单机性能优化到极致呢?要解决这个问题,我们就要想明白一件事:通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段,我们系统要做的事情是要保证火车票订单不超卖、不少卖,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。这三个阶段的先后顺序改怎么分配才更加合理呢?我们来分析一下:


2.1 下单减库存


当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。但是这样也会产生一些问题,第一就是在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的;第二是如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制IP和用户的购买订单数量,这也不算是一个好方法。83e1b6ecd7a2c5faa4ea1a021bfeabcf_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


2.2 支付减库存


如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单,当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘IO

adb983995860990a0430bbe267a52a0d_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


2.3 预扣库存


从上边两种方案的考虑,我们可以得出结论:只要创建订单,就要频繁操作数据库IO。那么有没有一种不需要直接操作数据库IO的方案呢,这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办?我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。订单的生成是异步的,一般都会放到MQ、kafka这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。

e6a8248294bbe1d977f1e95327c4b89b_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


3. 扣库存的艺术


从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?在单机低并发情况下,我们实现扣库存通常是这样的:


e7156b3c4d917f262c9e01448e544601_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多IO,对数据库的操作又是阻塞的。这种方式根本不适合高并发的秒杀系统。接下来我们对单机扣库存的方案做优化:本地扣库存。我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。改进过之后的单机系统是这样的:

e0051553e7f8cb7a6a1b98c73ba9b4f6_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

这样就避免了对数据库频繁的IO操作,只在内存中做运算,极大的提高了单机抗并发的能力。但是百万的用户请求量单机是无论如何也抗不住的,虽然nginx处理网络请求使用epoll模型,c10k的问题在业界早已得到了解决。但是linux系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。上面我们提到了nginx的加权均衡策略,我们不妨假设将100W的用户请求量平均均衡到100台服务器上,这样单机所承受的并发量就小了很多。然后我们每台机器本地库存100张火车票,100台服务器上的总库存还是1万,这样保证了库存订单不超卖,下面是我们描述的集群架构:

ec5fbbecc44af2b7e646656c2aa38576_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这100台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“buffer库存”用来防止机器中有机器宕机的情况。我们结合下面架构图具体分析一下:



ff53995b2a8cc317e7a227faaa182af6_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

我们采用Redis存储统一库存,因为Redis的性能非常高,号称单机QPS能抗10W的并发。在本地减库存以后,如果本地有订单,我们再去请求redis远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。当机器中有机器宕机时,因为每个机器上有预留的buffer余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。buffer余票设置多少合适呢,理论上buffer设置的越多,系统容忍宕机的机器数量就越多,但是buffer设置的太大也会对redis造成一定的影响。虽然redis内存数据库抗并发能力非常高,请求依然会走一次网络IO,其实抢票过程中对redis的请求次数是本地库存和buffer库存的总量,因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑,这在一定程度上也避免了巨大的网络请求量把redis压跨,所以buffer值设置多少,需要架构师对系统的负载能力做认真的考量。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
消息中间件 NoSQL Redis
秒杀的设计思路与实践
秒杀的设计思路与实践
65 1
|
2月前
|
缓存 NoSQL Java
秒杀圣经:10Wqps秒杀,16大架构绝招,一文帮你秒变架构师 (2)
高并发下的秒杀系统设计是一个复杂的挑战,涉及多个关键技术点。40岁老架构师尼恩在其读者交流群中分享了16个关键架构要点,帮助解决高并发下的秒杀问题,如每秒上万次下单请求的处理、超卖问题的解决等。这些要点包括业务架构设计、流量控制、异步处理、缓存策略、限流熔断、分布式锁、消息队列、数据一致性、存储架构等多个方面。尼恩还提供了详细的实战案例和代码示例,帮助读者全面理解和掌握秒杀系统的架构设计。此外,他还分享了《尼恩Java面试宝典》等资源,帮助读者在面试中脱颖而出。如果你对高并发秒杀系统感兴趣,可以关注尼恩的技术自由圈,获取更多详细资料。
秒杀圣经:10Wqps秒杀,16大架构绝招,一文帮你秒变架构师 (2)
|
7月前
|
缓存 NoSQL 关系型数据库
秒杀项目实战:遇到的问题及解决方案分享
构建了一个基于Springboot2的秒杀系统。项目利用K8S上的主从结构部署Redis和MySQL,通过Traefik作为网关。RabbitMQ在本地虚拟机的docker环境中,用Prometheus+Grafana监控。设计思路包括隐藏秒杀地址以防止脚本攻击,使用Lua脚本保证库存预扣原子性,但初期版本未处理重复订单校验。为防止MQ故障,将订单信息先保存到Redis,再通过脚本发送到MQ。采用分布式锁防止用户重复下单和缓存击穿问题,使用编程式事务确保库存扣减与订单保存一致性。项目通过JMeter测试,观察性能并分析Redis和RabbitMQ的使用情况。完整代码可在GitHub找到。
187 1
秒杀项目实战:遇到的问题及解决方案分享
|
7月前
|
缓存 前端开发 安全
秒杀系统架构分析与实战
秒杀系统架构分析与实战
198 1
|
消息中间件 存储 缓存
面试官问我:如何设计一个秒杀场景?
在之前的工作经历中,我做过营销相关项目,接触过关于票券秒杀的高并发场景,秒杀场景也算是最热门的高并发场景之一了。 下面我就把我对秒杀场景的一些理解简单写下来,仅供大家参考,欢迎留言纠错或者补充。
694 0
面试官问我:如何设计一个秒杀场景?
|
存储 负载均衡 NoSQL
秒杀的基本知识点,了解一下(二)
秒杀的基本知识点,了解一下
|
缓存 NoSQL 安全
秒杀系统的设计思路
你好看官,里面请!今天笔者讲的是秒杀系统的设计思路。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途,如需转载请跟我联系。
434 2
|
缓存 负载均衡 NoSQL
30.【学习心得】学习心得-秒杀架构
【学习心得】学习心得-秒杀架构
30.【学习心得】学习心得-秒杀架构
|
JSON 前端开发 NoSQL
高并发-【抢红包案例】之一:SSM环境搭建及复现红包超发问题
高并发-【抢红包案例】之一:SSM环境搭建及复现红包超发问题
71 0
|
缓存 NoSQL 前端开发
秒杀系统设计的5个要点
比如有10件商品要秒杀,可以放到缓存中,读写时不要加锁。 当并发量大的时候,可能有25个人秒杀成功,这样后面的就可以直接抛秒杀结束的静态页面。进去的25个人中有15个人是不可能获得商品的。所以可以根据进入的先后顺序只能前10个人购买成功。后面15个人就抛商品已秒杀完。
345 0