如何用PHP解决高并发与大流量问题

2023-11-08

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

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

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

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

重启与过载保护

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

高并发下的数据安全

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

  1. 超发的原因

某个抢购场景中,共有100个商品,最后一刻,已经消耗了99个商品,剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

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

<?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);
   }
 ?>
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

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

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

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

<?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");
 }
 ?>
  1. FIFO队列思路

我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁

2.文件锁的思路

使用非阻塞的文件排他锁

<?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);
  ?>
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
 <?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);
  ?>

1.乐观锁思路

Redis中的watch

<?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;
     }
 }
 ?>

总结;

在互联网时代,并发,高并发通常是指并发访问。也就是在某个时间点,有多少个访问同时到来。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何用PHP解决高并发与大流量问题 的相关文章

  • 当数据验证失败时保留表单字段中的值

    我在弄清楚验证失败时如何保留用户数据时遇到问题 我对 PHP 有点陌生 所以我的逻辑可能会犯一些巨大的错误 目前 如果验证失败 所有字段都会被清除 并且 Post 数据也会消失 这是一些代码 假设用户输入无效电子邮件 我希望保留 名称 字段
  • 优雅地处理没有数据的 amcharts

    我想知道我的 dataProvider 是否为空 amCharts绘制时默认为null 我怎样才能动态地处理它 var chart AmCharts makeChart chartdiv theme none type serial dat
  • MySQL/PDO::quote() 尽管使用 PDO::PARAM_INT 参数,但仍在整数周围加上引号

    无论我传递给什么值 数据类型对 它都会出现 pdo gt quote value type 它总是将其引用为字符串 echo pdo gt quote foo PDO PARAM STR foo as expected echo pdo g
  •  
    标记内删除

    我制作了简单的 BBCode 脚本 一切正常 但后来我使用了一个 javascript 库来美化我的代码 pre pre 现在我面临的唯一问题是 br 每行代码后面的标签 pre pre tags 所以问题是我怎样才能删除 br 标记哪些在
  • Apache 访问 Linux 中的 NTFS 链接文件夹

    在 Debian jessie 中使用 Apache2 PHP 当我想在 Apache 的文档文件夹 var www 中创建一个新的小节时 我只需创建一个指向我的 php 文件所在的外部文件夹的链接 然后只需更改该文件夹的所有者和权限文件夹
  • PHP严格标准:声明应该兼容

    我有以下类层次结构 class O Base class O extends O Base abstract class A Abstract public function save O Base obj class A extends
  • 从 smarty 访问 PHP 文件的变量(本地或全局)

    我有一个 php 文件 其中包含一些本地和全局变量 例如 foo 从此文件中调用 smarty 对象 如何在不更改 PHP 文件的情况下从 smarty 脚本访问 foo Thanks 如果你有一个名为 BASE 的常量变量 并且定义如下
  • 如何在原则 2 迁移中删除外键

    我想在原则 2 迁移中删除外键 但没有 dropForeignKeyConstraint 有谁知道怎么丢掉吗 public function down Schema schema table schema gt getTable table
  • 从 .phar 存档中提取文件

    对于 Phar 文件 我完全错过了一些东西 我正在安装一个需要 phpunit pdepend 和其他依赖项的项目 我将它们作为 phar 文件获取 但是 我无法使用命令行工具 php 命令 从中提取文件 我用谷歌搜索了这个问题 但没有发现
  • 为什么 iconv 在 php:7.4-fpm-alpine docker 中返回空字符串

    给出以下代码
  • PHP 在输入流中使用 fwrite 和 fread

    我正在寻找将 PHP 输入流的内容写入磁盘的最有效方法 而不使用授予 PHP 脚本的大量内存 例如 如果可以上传的最大文件大小为 1 GB 但 PHP 只有 32 MB 内存 define MAX FILE LEN 1073741824 1
  • PHP 脚本可以在终端中运行,但不能在浏览器中运行

    我正在尝试执行exec命令 但我遇到了问题 当我运行以下代码时 当我通过浏览器运行它时它不起作用 但如果我把输出 str将其复制并粘贴到终端中 它工作得很好 造成这种情况的原因是什么 我该如何解决 目前我正在运行localhost php
  • 覆盖控制器 Symfony 3.4/4.0

    我目前正在尝试覆盖 FOSUserBundle 中的控制器 在新的文档中 https symfony com doc 3 4 bundles override html https symfony com doc 3 4 bundles o
  • 通过 $_SESSION 从一个脚本发送到另一个脚本期间数据丢失

    我正在尝试将一个充满属性的对象从一个 PHP 发送到另一个 PHP SESSION object obj where obj是一个用 foreach 循环指定的对象 foreach array of objects as obj SESSI
  • 有没有好的方法支持 Redis 排序集中的 pop 成员?

    有没有好的方法可以像 List 的 api LPOP 一样支持 Redis 排序集中的 pop 成员 我发现从 Redis 排序集中弹出消息是使用 ZRANGE ZREM 但是它不是线程安全性 并且当多个线程从不同主机同时访问它们时需要分布
  • PHP 中只保留数组的前 N ​​个元素? [复制]

    这个问题在这里已经有答案了 有没有办法只保留数组的前 N 个 例如 10 个 元素 我知道有array pop 但是有没有更好 更优雅的方法呢 您可以使用array slice http php net array slice or arr
  • 使用 Ajax.Request 将 JSON 从浏览器传递到 PHP 的最佳方法

    您好 我有一个 JSON 对象 它是一个二维数组 我需要使用 Ajax Request 将其传递给 PHP 我知道的唯一方法 现在我使用js函数手动序列化我的数组 并获取以下格式的数据 s 1 d 3 4等 我的问题是 有没有办法更直接 有
  • 一次播种多行 laravel 5

    我目前正在尝试为我的用户表播种 如果我像这样尝试 2 行 就会失败 如果我只使用单个数组而不是 users 数组内的 2 个数组来创建一些假数据 那么效果很好 我做错了什么 正确的方法是什么 class UserTableSeeder ex
  • 如何在 Laravel 中使用 PUT http 动词提交表单

    我知道这个问题可能已经提出 但我就是无法让它发挥作用 如果有人可以帮助我 我将非常感激 我安装了 colletive form 但答案也可以是 html 表单标签 现在列出我的表格 我的路线和我的例外情况 Form model array
  • 为什么 Composer 降级了我的包?

    php composer phar update这样做了 删除了 2 3 0 软件包并安装了整个 2 2 5 Zend Framework php composer phar update Loading composer reposito

随机推荐

  • 前程无忧招聘信息爬取

    爬取前程无忧招聘信息 本文是关于招聘数据爬取 我们选取的网站是前程无忧 百度直接搜索前程无忧 或者51job 我们将看到搜索栏 在搜索栏中输入 数据分析师 将可以看到工作信息 至于分析网站在这里就不在解释了 本爬虫只是简单爬取一点数据 所以
  • 计算机怎么盲打键盘,如何练习盲打 键盘盲打指法练习技巧-电脑教程

    很多人在电脑上打字都还需要看着键盘 不会盲打 看到电脑高手噼里啪啦的打字速度着实令人羡慕 很多小白朋友所自己笨 不会盲打 其实不会盲打并不是因为笨 而是没找到一种简单易行的练习方法 按照标准指法 看着键盘按照从A到Z的顺序打26个字母 最多
  • Mysql多对多关系,分组拼接把多个数据查询到一条数据上

    GROUP CONCAT str 分组字符串拼接 与分组一起使用 案例 查询企业信息以及企业分类信息 其中企业分类信息和企业是多对多的关系 按普通的联表查询 我们会查询到一条企业信息对应多个企业分类 会出现多个记录 如果想实现把同一个企业的
  • 全面理解java中的构造方法以及this关键字的用法(超详细)

    Hello 各位铁汁们 我是小 儿哈 今天我又来更新我的Java基础学习博客了 本篇主要内容概述 1 如何用构造方法初始化对象 2 为啥要有this这个关键字 3 this 属性名访问成员变量 成员方法 4 this 方法名 this 的用
  • Pandas进阶筛选和取数操作

    总结了pandas各种进阶操作与使用技巧 并且对各方法间的效率进行比较 创建一个pandas的dataframe对象作为下文样例 import pandas as pd import numpy as np df pd DataFrame
  • 陀螺研究院:《2019年分布式金融商业趋势及落地情况分析报告》

    2018年末开始 以DeFi为代表的分布式金融 在业内引起了广泛的讨论 传统的金融模式以中央银行 商业银行 非银金融机构为核心展开支付 借贷 保险等场景内的应用 但DeFi彻底摆脱了原有的核心 以分布式账本作为清算依据 从而降低了金融服务中
  • 前馈全连接神经网络和函数逼近、时间序列预测、手写数字识别

    https www cnblogs com conmajia p annt feed forward fully connected neural networks html 前馈全连接神经网络和函数逼近 时间序列预测 手写数字识别 And
  • springboot中JDBC连接超时问题

    最近项目中有一个问题 电子保卡信息要写入数据库 但写入失败 报错 息是这样的 The last packet successfully received from the server was 57 704 088 milliseconds
  • Stream流

    Stream流 Stream 流 是一个来自数据源的元素队列并支持聚合操作 元素是特定类型的对象 形成一个队列 Java中的Stream并不会存储元素 而 是按需计算 数据源 流的来源 可以是集合 数组等 聚合操作 类似SQL语句一样的操作
  • Bes 充电盒协议总结

    1 开盖 上升沿信号开机 a 充电脚设成3 0 v 然后延迟160ms b 充电脚设成5v 然后延时100 ms c充电脚设成3 0 v 2 合盖 a 开5v 然后延时3s b 关5v 然后延时45ms c 发送复位pattern 0101
  • c++ 字符串相等比较

    介绍 在C 中比较字符串的技术 Techniques to Compare Strings in C Strings in C can be compared using either of the following techniques
  • mysql命令 show_mysql--SHOW命令大全

    SHOW AUTHORS 顾名思义 这个要展示的是各位MYSQL开发者的信息 包括姓名 住址及相关注解 e g 1 mysql gt show authors G 1 row Name Brian Krow Aker Location Se
  • LeetCode 62. Unique Paths

    题目链接 题目描述 A robot is located at the top left corner of a m x n grid marked Start in the diagram below The robot can only
  • Microsoft Store无法打开解决方案 错误代码:0x80131500

    这种情况大部分是设置了Vpn代理 提供两种解决方案 一 打开 运行 输入 inetcpl cpl 点还原高级设置 注意看看勾选了TLS 1 2没有 二 如果上述方法没有解决 那么就打开Internet选项 gt 安全选项卡 gt 点一下 将
  • pip安装opencv-python

    文章目录 前言 一 基本概念 二 操作步骤 1 删除旧版本 2 pip升级 3 opencv python安装 总结 前言 OpenCV的全称是Open Source Computer Vision Library 是一个跨平台的计算机视觉
  • 跳转至tabBar页面不触发页面的onLoad,点击底部tabar不触发onLoad

    小程序想跳转tabar页面带参数 使用了全局变量app js的全局 跳转到页面后发现不是每次都执行onLoad方法 传参失败 更换跳转的方法解决 由wx switchTab改为wx reLaunch 就可以了 点击底部导航不触发解决 js
  • Ubuntu挂载Win10下的NTFS硬盘出错的解决方案

    概述 在Ubuntu下打开Win10的NTFS硬盘总是提示出错了 而且是全部的NTFS盘都出错 其中sdb1错误显示如下 he disk contains an unclean file system 0 0 Metadata kept i
  • matplotlib函数总结

    导入matplotlib import matplotlib pyplot as plt import matplotlib Figures对象包含一个或多个Asex对象 方法 matplotlib rc figure figsize 14
  • 在Ubuntu18.04.3系统中安装谷歌拼音输入法(Google Pinyin)

    一 安装前的准备 在Ubuntu18 04下 谷歌拼音输入法是基于Fcitx输入法的 因此 我们需要首先安装Fcitx 一般来说 Ubuntu最新版中都默认安装了Fcitx 但是为了确保一下 我们可以在系统终端中运行如下命令 sudo ap
  • 如何用PHP解决高并发与大流量问题

    举个例子 高速路口 1秒钟来5部车 每秒通过5部车 高速路口运作正常 突然 这个路口1秒钟只能通过4部车 车流量仍然依旧 结果必定出现大塞车 5条车道忽然变成4条车道的感觉 同理 某一个秒内 20 500个可用连接进程都在满负荷工作中 却仍