文档章节

php如何应对秒杀抢购高并发思路

大唐6777
 大唐6777
发布于 2017/05/23 20:31
字数 3378
阅读 13
收藏 0
点赞 0
评论 0

我们常用QPS(Query Per Second,每秒处理请求数)来衡量一个web应用的吞吐率,解决每秒数万次的高并发场景,这个指标非常关键。

举个栗子:假设一个业务请求平均为100ms,同时系统内有20台apache web服务器,MaxClients(apache的最大连接数)设置为500,那么理论QPS峰值就是20*500/0.1=100000(理论与实际肯定有差异)。

这系统貌似理论上来说很强大1秒钟处理100000个请求,实际当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

 

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回

高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。)

 

优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

<?php

//优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false

include('./mysql.php');

$username 'wang'.rand(0,1000);

//生成唯一订单

function build_order_no(){

  return date('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

}

//记录日志

function insertLog($event,$type=0,$username){

    global $conn;

    $sql="insert into ih_log(event,type,usernma)

    values('$event','$type','$username')";

    return mysqli_query($conn,$sql);

}

function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number)

{

      global $conn;

      $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)

      values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";

     return  mysqli_query($conn,$sql);

}

//模拟下单操作

//库存是否大于0

$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";

$rs=mysqli_query($conn,$sql);

$row $rs->fetch_assoc();

  if($row['number']>0){//高并发下会导致超卖

      if($row['number']<$number){

        return insertLog('库存不够',3,$username);

      }

      $order_sn=build_order_no();

      //库存减少

      $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";

      $store_rs=mysqli_query($conn,$sql);

      if($store_rs){

          //生成订单

          insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);

          insertLog('库存减少成功',1,$username);

      }else{

          insertLog('库存减少失败',2,$username);

      }

  }else{

      insertLog('库存不够',3,$username);

  }

 

 

?>

  

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

 

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

优化方案2:使用MySQL的事务,锁住操作的行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

<?php

//优化方案2:使用MySQL的事务,锁住操作的行

include('./mysql.php');

//生成唯一订单号

function build_order_no(){

  return date('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

}

//记录日志

function insertLog($event,$type=0){

    global $conn;

    $sql="insert into ih_log(event,type)

    values('$event','$type')";

    mysqli_query($conn,$sql);

}

 

//模拟下单操作

//库存是否大于0

mysqli_query($conn,"BEGIN");  //开始事务

$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行

$rs=mysqli_query($conn,$sql);

$row=$rs->fetch_assoc();

if($row['number']>0){

    //生成订单

    $order_sn=build_order_no();

    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)

    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";

    $order_rs=mysqli_query($conn,$sql);

    //库存减少

    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";

    $store_rs=mysqli_query($conn,$sql);

    if($store_rs){

      echo '库存减少成功';

        insertLog('库存减少成功');

        mysqli_query($conn,"COMMIT");//事务提交即解锁

    }else{

      echo '库存减少失败';

        insertLog('库存减少失败');

    }

}else{

  echo '库存不够';

    insertLog('库存不够');

    mysqli_query($conn,"ROLLBACK");

}

?>

  

3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

 

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

4. 文件锁的思路

对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题。但如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失

优化方案4:使用非阻塞的文件排他锁

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

<?php

//优化方案4:使用非阻塞的文件排他锁

include ('./mysql.php');

//生成唯一订单号

function build_order_no(){

  return date('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

}

//记录日志

function insertLog($event,$type=0){

    global $conn;

    $sql="insert into ih_log(event,type)

    values('$event','$type')";

    mysqli_query($conn,$sql);

}

 

 

$fp fopen("lock.txt""w+");

if(!flock($fp,LOCK_EX | LOCK_NB)){

    echo "系统繁忙,请稍后再试";

    return;

}

//下单

$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";

$rs =  mysqli_query($conn,$sql);

$row $rs->fetch_assoc();

if($row['number']>0){//库存是否大于0

    //模拟下单操作

    $order_sn=build_order_no();

    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)

    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";

    $order_rs =  mysqli_query($conn,$sql);

    //库存减少

    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";

    $store_rs =  mysqli_query($conn,$sql);

    if($store_rs){

      echo '库存减少成功';

        insertLog('库存减少成功');

        flock($fp,LOCK_UN);//释放锁

    }else{

      echo '库存减少失败';

        insertLog('库存减少失败');

    }

}else{

  echo '库存不够';

    insertLog('库存不够');

}

fclose($fp);

 

 

 ?>

  

5. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

 

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

优化方案5:Redis中的watch

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

<?php

 

$redis new redis();

 $result $redis->connect('127.0.0.1', 6379);

 echo $mywatchkey $redis->get("mywatchkey");

 

/*

  //插入抢购数据

 if($mywatchkey>0)

 {

     $redis->watch("mywatchkey");

  //启动一个新的事务。

    $redis->multi();

   $redis->set("mywatchkey",$mywatchkey-1);

   $result = $redis->exec();

   if($result) {

      $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());

      $watchkeylist = $redis->hGetAll("watchkeylist");

        echo "抢购成功!<br/>";

        $re = $mywatchkey - 1;  

        echo "剩余数量:".$re."<br/>";

        echo "用户列表:<pre>";

        print_r($watchkeylist);

   }else{

      echo "手气不好,再抢购!";exit;

   

 }else{

     // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");

     //  $watchkeylist = $redis->hGetAll("watchkeylist");

        echo "fail!<br/>";   

        echo ".no result<br/>";

        echo "用户列表:<pre>";

      //  var_dump($watchkeylist); 

 }*/

 

 

$rob_total = 100;   //抢购数量

if($mywatchkey<=$rob_total){

    $redis->watch("mywatchkey");

    $redis->multi(); //在当前连接上启动一个新的事务。

    //插入抢购数据

    $redis->set("mywatchkey",$mywatchkey+1);

    $rob_result $redis->exec();

    if($rob_result){

         $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);

        $mywatchlist $redis->hGetAll("watchkeylist");

        echo "抢购成功!<br/>";

      

        echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";

        echo "用户列表:<pre>";

        var_dump($mywatchlist);

    }else{

          $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');

        echo "手气不好,再抢购!";exit;

    }

}

?>

如果帮到了您,可以支持一下,谢谢您的支持!

© 著作权归作者所有

共有 人打赏支持
大唐6777
粉丝 0
博文 28
码字总数 3378
作品 0
海淀
程序员
秒杀系统常见问题1

由于最近的面试老是遇到面试官问设计一个秒杀系统,当时也只是粗略的看了看,然后今天去陌陌面试又被问到了,被教育了一顿,然后晚上的笔试又让设计一个秒杀系统,我真是XXXXK,所以搜罗一波...

努力的C
2017/10/17
0
0
SpringBoot开发案例从0到1构建分布式秒杀系统

前言 最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路。俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了...

小柒2012
05/16
0
0
Web系统大规模并发:电商秒杀与抢购

一、大规模并发带来的挑战 在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻而易举地陷入到异常...

天下杰论
2016/01/14
226
1
徐汉彬:Web系统大规模并发——电商秒杀与抢购

【导读】徐汉彬曾在阿里巴巴和腾讯从事4年多的技术研发工作,负责过日请求量过亿的Web系统升级与重构,目前在小满科技创业,从事SaaS服务技术建设。 电商的秒杀和抢购,对我们来说,都不是一...

ljianbing
2017/04/01
0
0
热点推荐:秒杀系统架构分析与实战

1 秒杀业务分析 正常电子商务流程(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货 秒杀业务的特性(1)低廉价格;(2)大幅推广;(3)瞬时售空;(...

洋哥6
2016/04/14
359
1
小柒2012/spring-boot-seckill

分布式秒杀系统 开发环境 JDK1.7、Maven、Mysql、Eclipse、SpringBoot1.5.10、zookeeper3.4.6、kafka_2.11、redis-2.8.4、curator-2.10.0 友情提示 由于工作原因,项目正在完善中(仅供参考)...

小柒2012
05/19
0
0
PHP秒杀系统 高并发高性能的极致挑战

先说一下体会,这个老师讲的算是比较慢的了,而且语义化的地方很多,我推荐大家睡觉的时候听,老师主要时讲解代码,大家可以看一遍代码,然后就听就行了,我这样学习就是享受啊,因为我眼睛一...

含笑666
06/06
0
0
昨天php的面试题

1、中国大概有16亿人口,你的名字的重复率最高,如何快速的找出叫你的名字的人? 2、如何防止前台表单的重复提交? 3、抢购时如何应对高并发? 4、php如何实现sso? 5、分表的算法 期待各位的...

开源中国创始人
2014/05/21
2.4K
22
秒杀 计数器 直播--php 实现数据库连接池、直播平台

yaf项目快速开发(兼容php7): yaf project rapid development, integration of the db action class support chain operation, support separate read and write, pdo, mysqli, mongo, up......

qieangel
2015/09/07
11.5K
3
限时抢码(秒杀)设计

做之前看了不少关于秒杀商品的文章,其中有一篇觉得很好:【问底】徐汉彬:Web系统大规模并发——电商秒杀与抢购 抢码的逻辑比秒杀商品要简单太多,虽然简单但是高并发的问题却还是存在的,有...

尚小胖
2016/02/26
244
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Centos7通过yum安装nginx

添加源地址(直接install可能不是最新版本的) sudo rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm 安装 sudo yum install -y ng......

iplusx
5分钟前
0
0
ef .core Dapper Helper

using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Data.SqlClient; using System.Threading.Tasks; using Dapper; using Dap......

Lytf
7分钟前
0
0
iOS 小笔记

1.以下代码打印什么     __block int val = 10;    void (^blk)(void) = ^{        printf("val=%d\n",val);        };       val = 2;    blk(); /...

风了个1
9分钟前
0
0
【Spring Boot 系列 Spring Boot示例程序】

入门程序步骤,创建一个Maven项目。继承Spring Boot官方提供的父工程。再引入一个Web的应用启动器。 1、选择一个合适的IDEA工具 创建一个Maven工程,并添加如下配置 <parent> <...

HansonReal
10分钟前
0
0
217. Contains Duplicate - LeetCode

Question 217. Contains Duplicate Solution 题目大意:判断数组中是否有重复元素 思路:构造一个set,不重复就加进去,重复返回true,如果数据量大的话,可以用布隆过滤器 Java实现: publ...

yysue
14分钟前
0
0
istio 处理失败 (理论)

Envoy提供了一套开箱即用的选择加入故障恢复功能,可以通过应用程序中的服务进行利用。功能包括: 超时 具有超时预算和重试之间的可变抖动的有界重试 限制并发连接数和对上游服务的请求 对负...

xiaomin0322
16分钟前
0
0
eclipse解决git冲突举例

本地修改了两个文件,提交时提示有冲突,想来应该是没有从远程仓库下载最新代码导致的。通过右击项目 -> Team -> Sychronized WorkSpace,比较本地仓库和远程仓库的异同:   此时没有更好的...

Code辉
24分钟前
0
0
运行.jar后缀的文件

前提必须安装了jdk,正确配置环境变量。 在dos窗口执行以下命令即可。 java -jar C:\Users\10492\Desktop\turn.jar

haha360
27分钟前
0
0
Java程序员如何做代码压力测试?【JWordPress前台项目实战】

代码 pom.xml文件引入包 <dependency><groupId>com.taobao.stresstester</groupId><artifactId>stresstester</artifactId><version>1.0</version></dependency> 编写测试代码 /**......

迷你芊宝宝
31分钟前
0
0
面试宝典-什么是缓存穿透?

缓存穿透是说收到了一个请求,但是该请求缓存里没有,只能去数据库里查询,然后放进缓存。 这里面有两个风险,一个是同时有好多请求访问同一个数据,然后业务系统把这些请求全发到了数据库;...

suyain
37分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部