【学习笔记】秒杀系统的实现

高并发系统如何做到高可用

原理

减而治之:cdn;nginx限流,异步队列(高并发流量变成均摊流量)
分而治之:lvs+nginx负载均衡

特征

写强一致性(不能超卖)
读弱一致性(读可能有库存,但是不能下单)

核心实现

读服务实现
写服务实现
排队进度查询实现
链路流量优化如何做(lvs层, sever层,减少流量涌入)

流量漏斗

请求链路中,每层服务削峰,限流,自动降级,熔断

秒杀系统的实现【滴滴讲师的一个实例】

Ab压测接口可以承受的最大qps值

-n访问1000次, -c并发100个

【学习笔记】秒杀系统的实现_第1张图片

【学习笔记】秒杀系统的实现_第2张图片

Requests per second 是吞吐量,是以秒为单位的,所以这个值可以了;结尾qps。

限流

Tengine版本采用http_limit_req_module进行限制

具体连接请参考 http://tengine.taobao.org/doc...

和官方nginx类似,不过支持多个变量,并且支持多个limit_req_zone的设置。比如:

limit_req_zone $binary_remote_addr zone=one:3m rate=1r/s;
limit_req_zone $binary_remote_addr $uri zone=two:3m rate=1r/s;               # $uri:不带客户端请求参数
limit_req_zone $binary_remote_addr $request_uri zone=thre:3m rate=1r/s;      # $request_uri:带客户端请求参数

上面的第二个指令表示当相同的ip地址并且访问相同的uri,会导致进入limit req的限制(每秒1个请求)。

Nginx官方版本限制IP的连接和并发分别有两个模块:

  • limit_req_zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 "leaky bucket"
  • limit_req_conn 用来限制同一时间连接数,即并发限制

其中limit_req_conn模块可以根据源IP限制单用户并发访问的连接数或连接到该服务的总并发连接数。

两种算法的最大区别:令牌桶算法可以应对突发流量,漏铜算法不行。

【学习笔记】秒杀系统的实现_第3张图片

【学习笔记】秒杀系统的实现_第4张图片

可以参考学习:https://www.cnblogs.com/bigli...

漏桶算法

我们假设系统是一个漏桶,当请求到达时,就是往漏桶里“加水”,而当请求被处理掉,就是水从漏桶的底部漏出。水漏出的速度是固定的,当“加水”太快,桶就会溢出,也就是“拒绝请求”。从而使得桶里的水的体积不可能超出桶的容量。​主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。

示例如下:

http {

limit_conn_log_level error;

limit_conn_status 503;

limit_conn_zone **$binary_remote_addr** zone=one:10m;

limit_conn_zone **$server_name** zone=perserver:10m;

limit_req_zone **$binary_remote_addr** zone=allips:100m   rate=10r/s;   //其中$binary_remote_addr有时需要根据自己已有的**log\_format变量**配置进行替换

server {

                …………………….

                limit_conn  one  100;                                              

                limit_conn perserver 1000;

                limit_req   zone=allips  burst=5  nodelay;

                ………………….

         }

}

参数解释:

Zone=one或allips 表示设置了名为“one”或“allips”的存储区,大小为10兆字节

rate=10r/s 的意思是允许1秒钟不超过10个请求

burst=5 表示最大延迟请求数量不大于5。  如果太过多的请求被限制延迟是不需要的 ,这时需要使用nodelay参数,服务器会立刻返回503状态码。

limit_conn  one  100表示限制每个客户端IP的最大并发连接数100

limit_conn perserver 1000表示该服务提供的总连接数不得超过1000,超过请求的会被拒绝

示例如下:   

http {

    limit_req_zone $binary_remote_addr zone=one:100m   rate=10r/m;

    server {

              limit_req   zone=one  burst=1  nodelay;

    }

}

配置解释

rate=10r/m 

允许1秒钟不超过1个请求,最大延迟请求数量不大于5.

如果请求不需要被延迟,添加nodelay参数,服务器会立刻返回503状态码。如果没有该字段会造成大量的tcp连接请求等待。

http{  
    limit_zone one  $binary_remote_addr  10m;  
    server  
    {  
    ......  
    limit_conn  one  1;  
    ......  
    }  
   }

这里的 one 是声明一个 limit_zone 的名字,$binary_remote_addr是替代 $remore_addr 的变量,10m 是会话状态储存的空间。

limit_conn one 1 

限制客户端并发连接数量为1, allow only one connection per an IP address at a time(每次).

limit_req_zone的功能是通过漏桶原理来限制用户的连接频率,(这个模块允许你去限制单个地址指定会话或特殊需要的请求数 ) 。

而 limit_zone 功能是限制一个客户端的并发连接数。(这个模块可以限制单个地址的指定会话或者特殊情况的并发连接数) 。

一个是限制并发连接一个是限制连接频率,表面上似乎看不出来有什么区别,那就看看实际的效果吧~~~
在我的测试机上面加上这两个参数下面是我的部分配置文件

http{  
    limit_zone one  $binary_remote_addr  10m;  
    #limit_req_zone  $binary_remote_addr  zone=req_one:10m rate=1r/s;  
    server  
    {  
    ......  
    limit_conn   one  1;  
    #limit_req   zone=req_one  burst=120;  
    ......  
    }  
}

配置解释

limit_zone one  $binary_remote_addr  10m;  

这里的 one 是声明一个 limit_zone 的名字,$binary_remote_addr是替代 $remore_addr 的变量,10m是会话状态储存的空间

limit_conn one 1 

限制客户端并发连接数量为1

limit_zone两种工作情况
limit_reqzone=one burst=10;

默认情况下是这样配置的,这样每个请求就会有一个delay时间,

limit_req_zone$binary_remote_addr zone=one:100m rate=10r/m;

就是每分钟有10个令牌供用户使用,按照a的配置情况,就会有一个delay,每个请求时间就是60/10,那每个请求时间就是6s。

limit_reqzone=one burst=10 nodelay;

添加nodelay配置,这样就是根据你的网络状况访问,一分钟访问够10次后,服务器直接返回503。

limit_req_zone$binary_remote_addr zone=one:100m rate=10r/m;

就是每分钟有10个令牌供用户使用,按照b的配置情况,就会根据网络情况访问url,如果一分钟超过10个令牌,服务器返回503,等待下一个一分钟领取访问令牌。

rate=10r/m 

每个地址每分钟只能请求10次,也就是说根据漏桶原理burst=1 一共有1块令牌,并且每分钟只新增10块令牌,
1块令牌发完后多出来的那些请求就会返回503。

加上 nodelay之后超过 burst大小的请求就会直接返回503,如果没有该字段会造成大量的tcp连接请求等待。

http{
    ...
    #定义一个名为allips的limit_req_zone用来存储session,大小是10M内存,
    #以$binary_remote_addr 为key,限制平均每秒的请求为20个,
    #1M能存储16000个状态,rete的值必须为整数,
    #如果限制两秒钟一个请求,可以设置成30r/m
    limit_req_zone $binary_remote_addr zone=allips:10m rate=20r/s;
    ...
    server{
        ...
        location {
            ...
            #限制每ip每秒不超过20个请求,漏桶数burst为5
            #brust的意思就是,如果第1秒、2,3,4秒请求为19个,
            #第5秒的请求为25个是被允许的。
            #但是如果你第1秒就25个请求,第2秒超过20的请求返回503错误。
            #nodelay,如果不设置该选项,严格使用平均速率限制请求数,
            #第1秒25个请求时,5个请求放到第2秒执行,
            #设置nodelay,25个请求将在第1秒执行。
            limit_req zone=allips burst=5 nodelay;
            ...
        }
        ...
    }
    ...
}


cdn

【学习笔记】秒杀系统的实现_第5张图片

【学习笔记】秒杀系统的实现_第6张图片

负载均衡

【学习笔记】秒杀系统的实现_第7张图片

ip hash的一个问题,某个学校如果总的 ip 指定到 了某台机器,那么这个机器的访问量就会很大。

消息队列

由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达DB,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。

流量预估

【学习笔记】秒杀系统的实现_第8张图片

秒杀系统的特点难点

【学习笔记】秒杀系统的实现_第9张图片

【学习笔记】秒杀系统的实现_第10张图片

秒杀系统的策略

【学习笔记】秒杀系统的实现_第11张图片
秒杀系统,和其他服务隔离部署,所以也是一个单独的服 务,秒杀服务。

【学习笔记】秒杀系统的实现_第12张图片

实现需求

【学习笔记】秒杀系统的实现_第13张图片

秒杀系统的实现-扣库存方案

扣库存的几种方式

● 下单扣库存

当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精 确的一种,下单时直接通过数据库的事务机制控制商品 库存,这样一定不会出现超卖的情况。但是你要知道, 有些人下完单可能并不会付款。

● 付款扣库存

即买家下单后,并不立即减库存,而是等到 有用户付款后才真正减库存,否则库存一直保留给其他 买家。但因为付款时才减库存,如果并发比较高,有可 能出现买家下单后付不了款的情况,因为可能商品已经 被其他人买走了。

● 预扣库存

这种方式相对复杂一些,买家下单后,库存为 其保留一定的时间(如 10 分钟),超过这个时间,库存 将会自动释放,释放后其他买家就可以继续购买。在买 家付款前,系统会校验该订单的库存是否还有保留:如 果没有保留,则再次尝试预扣;如果库存不足(也就是 预扣失败)则不允许继续付款;如果预扣成功,则完成 付款并实际地减去库存。

以上减库存的几种方式存在的问题

由于购物过程中存在两步或者多步的操作,因此在不同 的操作步骤中减库存,就会存在一些可能被恶意买家利 用的漏洞,例如发生恶意下单的情况。

假如我们采用“下单减库存”的方式,即用户下单后就减去 库存,正常情况下,买家下单后付款 的概率会很高,所以不会有太大问题。但是有一种场景例 外,就是当卖家参加某个活动时,此 时活动的有效时间是商品的⻩金售卖时间,如果有竞争对手 通过恶意下单的方式将该卖家的商 品全部下单,让这款商品的库存减为零,那么这款商品就不 能正常售卖了。要知道,这些恶意 下单的人是不会真正付款的,这正是“下单减库存”方式的 不足之处。

付款减库存 既然“下单减库存”可能导致恶意下单,从而影响卖家的商 品销售,那么有没有办法解决呢? 你可能会想,采用“付款减库存”的方式是不是就可以了? 的确可以。但是,“付款减库存”又 会导致另外一个问题:库存超卖。

既然“下单减库存”和“付款减库存”都有缺点,我们能否 把两者相结合,将两次操作进行前后关联起来,下单时 先预扣,在规定时间内不付款再释放库存,即采用“预扣 库存”这种方式呢?

这种方案确实可以在一定程度上缓解上面的问题。但是否就 彻底解决了呢?其实没有!针对恶 意下单这种情况,虽然把有效的付款时间设置为 10 分钟, 但是恶意买家完全可以在 10 分钟 后再次下单,或者采用一次下单很多件的方式把库存减完。 针对这种情况,解决办法还是要结 合安全和反作弊的措施来制止。

如何解决下单扣库存问题

给经常下单不付款的买家进行识别打标(可以在被打标的买 家下单时不减库存)、给某些类目 设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的
操作进行次数限制等。

服务器性能优化(极限压榨cpu)

重点是:避免不必要的上下文切换。
进程,线程之间的切换,系统调用,cpu都需要耗费资源调度,这些是无效的动作。真正执行程序,才是有效动作,

线程,协程的使用,可以避免这两者。
所以可以看出,Php 并不是做扣库存,比较好比较合适的一⻔语言。

为了达到极致性能,php 可以做的是,减少阻塞时 IO(磁盘 文件读写,远程调用)。

【学习笔记】秒杀系统的实现_第14张图片

redis,单机单服务可以实现 10w qps。

【学习笔记】秒杀系统的实现_第15张图片

扣库存实现

Go 是单进程,php 是多进程。 代码实现:

【学习笔记】秒杀系统的实现_第16张图片
redis+lua 脚本,防止超卖。 商品信息和抢购进度的实现

【学习笔记】秒杀系统的实现_第17张图片
商品标题图片详情,这种不经常改变的,相对静态信息的, 可以做静态化 +cdn

价格,促销活动,库存,这些相对动态信息,可以做热点缓 存。

数组和 hash 表的数据结构区别,
最大区别:方式不同,数组通过 key 查找 value,hash 通过函数查找。

https://www.cnblogs.com/chenj...

减库存成功的用户,存到数组 A 里面,依次存入。 数组是索引数组,key 是索引值,值是 uid。
hash 表,key 是 uid,value 是数组 A 的索引值。

每次消费数据,就是从头到尾去消费,记录起来最近消费的 索引值,为 X,

得到排队进度,
数组和 hash 是在内存中存储的,数组和 hash 表的查询复杂度都是 O1,非常快。

【学习笔记】秒杀系统的实现_第18张图片

扣库存,放少部分流量,到统一扣库存的地方去,进行操 作。
写订单,异步消费。

请求链路实现漏斗流量

【学习笔记】秒杀系统的实现_第19张图片

举例:12306的验证码,就是为了削峰, 增加用户操作难 度,降低请求次数。也可以防机器人破解验证。

对访问库存数,这里注意限流,但不能限流太久,注意时间 间隔。允许用户低频次的读取数据,

读写分离,一主多从,可以有效大幅度提高读的访问能力

【学习笔记】秒杀系统的实现_第20张图片
海量并发的处理本质: 分治 ; 有效压榨 cpu。 单服务的性能提升,重点是压榨 cpu 性能。减少 IO(网络访

问,磁盘读写),减少 cpu 上下文切换, Cpu 上下文切换是无效的运算。

单进程和单线程,保证不会切换上下文,但是遇到 IO 可能 会阻塞,异步

多核 cpu,单机部署多实例(多个单进程单线程),有效利 用多核 cpu。

你可能感兴趣的:(秒杀,php)