PHP高并发高负载下的3种实战场景解决方法示例

 更新时间:2023年05月26日 14:49:53   作者:北桥苏  
这篇文章主要为大家介绍了PHP高并发高负载下的3种实战场景解决方法示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言:

在实际开发项目中,产品一旦推广开来,总能遇到一些小问题。比如某个接口突然就请求崩掉了,某个提交接口明明做了限制为什么就多出了好多重复的记录。还有是某个记录超过限制进行修改了,以下就以这几个小问题总结一下平时采取的解决方法。 

1.缓存失效场景,就比如某个接口做了数据缓存,缓存过期导致突然某个时刻大量请求直接读数据库。解决方法设置redis缓存回调事件,订阅失效频道。所以这个也可以用来处理某些业务场景到期处理方式。

2.接口幂等性场景,就比如注册接口,通过手机号查询是否存在记录。但有时出现网络延迟用户连点等情况,会出现数据库出现几条一样的用户数据记录。

3.商品库存超卖场景,比如某个活动商品下单,多个用户同时下一个商品的订单,从而导致库存超卖的现象。解决方法可以使用乐观锁或者悲观锁解决此问题。

场景一,缓存失效回调。

1. 设置Redis回调事件方法。

(1). 打开Redis客户终端,输入命令非持久性的回调事件设置

config set notify-keyspace-events Ex

(2). windows平台打开Redis安装目录中找到"redis.windows-service.conf",然后打开编辑找到notify-keyspace-events那一行,去掉"#",改为notify-keyspace-events “Ex"。

(3). 其中Redis还可以设置订阅键名的回调,比如订阅某个键名的del操作等,可以在conf中设置不同的,方法网上也有的。

2. 订阅redis某个库的键失效的频道名。

可以在命令测试,也可以通过PHP代码订阅然后cli环境下运行脚本。

命令:
subscribe __keyevent@0__:expired

3. 设置有限期

重新打开一个新的redis客户终端输入一个带有效期的键值对,如下(键名test_key_name, 时间30s, 值ceshi)

命令:
setex test_key_name 30 ceshi

4. 查看键失效回调订阅的命令窗口是否出现失效的键名。

5. 代码实现键名的失效事件订阅。

<?php
//设置php脚本执行时间
set_time_limit(0);
//设置socket连接超时时间
ini_set('default_socket_timeout', -1);
class redisSubscribe 
{
    protected $config = [
        "host" => "127.0.0.1",
        "password" => "6379"
    ];
    protected $redis;
    public function __construct() 
    {
        try {
            $this->redis = new \Redis();
            $this->redis->pconnect($this->config['host'],$this->config['password']);
        } catch(\Exception $e) {
            echo "redis错误:".$e->getMessage().PHP_EOL;
        }
    }
    // 普通消息订阅
    public function normal()
    {
        //声明频道名称
        $channelName = "test";
        try {
            $this->redis->subscribe([$channelName], function ($redis, $channel, $msg)
            {
                echo 'channel:' . $channel . ',message:' . $msg . PHP_EOL;
                file_put_contents('subscribe.log',"\n-".$msg."-\n",FILE_APPEND);
            });
        } catch (\Exception $e) {
            echo $e->getMessage();
        }
    }
    // 订阅Key失效事件的频道
    public function keyNotify()
    {
        echo "wathc keyNotify start~~".PHP_EOL;
        // Key事件回调
        //$channel = "__keyevent@0__:expired";        // 0号库的Key过期事件频道名
        $channel = "__keyevent@*__:expired";        // 所有库的Key过期事件频道名
        try {
            $this->redis->subscribe([$channel], function ($redis, $channel, $msg)
            {
                echo 'channel:' . $channel . '===========' . ',message:' . $msg . PHP_EOL;
                file_put_contents('subscribe.log',"\n-".$msg."-\n",FILE_APPEND);
            });
        } catch (\Exception $e) {
            echo $e->getMessage();
        }
    }
}
(new redisSubscribe())->keyNotify();
?>

6. 通过PHP-cli运行该脚本

然后也可以setex一个短时间的键,然后查看命令是否输出该失效的键名。

7. 缓存失效应用展开。

(1). 代码中设置的所有键名都配置到项目的全局配置文件中。

(2). 服务器中开一个守护进程(持续运行订阅某个库或者所有库的键失效回调事件脚本)。

(3). 当该脚本有回调时,取出键名去全局缓存键名数组中匹配。

(4). 规则业务可以自行设计。

(5). 比如取出一个"cate5"的键名,则可以取资讯表中查询分类ID为5的所有数据然后再进行缓存。

(6). 缓存失效事件还一个高端玩法,就是取代某些定时任务。比如可以将某个订单作为键名缓存,当该键名失效就可以取出键名拿到ID去数据库中将订单状态修改为失效。 

场景二,接口幂等性。

接口重复数据也就是在高并发下的数据添加场景。最典型的是注册接口,用户在网络延迟大或者信号不稳定的情况下。并且同时大量用户在进行注册操作,用户点击了一次没反应然后再次点击多个。    

在没有做幂等性处理只是拿到手机号查询数据库是否存在,用户表又没分库分表,查询缓慢,查询出来后,多条并发的请求都绕过了手机号已经存在的条件判断,所以就出现了ID不同,但是其他字段一样的记录。

  • 对于高并发数据添加,可以使用Redis的setnx。
  • setnx是设置键并且在有效期内有值时,再次对该键名进行重复赋值无法进行,会返回0。

  • 可以代码在对某些条件查询是否存在时,可以将条件组成键名赋值。添加记录时再次对键名重新赋值,返回null则表示已经存在。
  • 以下代码是项目中的一个测试方法,使用的redis是封装的,借鉴需要修改。
/**
     * @Notes: 高并发防止重复提交(插入数据) 【保证接口的幂等性】
     * @Interface preventRepeatSubmit
     * @return mixed
     * @author: bqs
     * @Time: 2020/6/19 14:56
     */
    public function preventRepeatSubmit()
    {
        /* 比如查询某条(什么条件)记录是否存在,分布式锁机制[redis的原子性setnx]
         * 1. 通过条件拼接为唯一的键名,将键名setnx设置一个30s有效期的值
         * 2. setnx设置键名不成功(返回0)表示已经存在,接口则直接返回记录已经存在
         * 3. 根据该条件查询数据库记录,如果存在,接口再返回记录已经存在
         * 【只要添加记录前需要查询什么是否存在则都需要考虑高并发情况,则通过此方案】
        */
        $redis = Redis::db(0);
        $no = date('YmdHis',time()).mt_rand(1000,9999);
        //$no = 202006191537447811;
        // 是否添加锁表
        $addLock = false;
        if ($redis->setnx($no,1)) {
            $redis->expire($no,30);     //设置30s过期时间
        } else {
            $addLock = true;        // 订单已经存在则锁住
        }
        // 数据库查询是否存在
        $isExist = Db::name('ztest')->where(['no'=>$no])->find();
        if ($isExist) {
            $addLock = true;
        }
        if (!$addLock) {
            $data = [
                "no" => $no,
                "tab_num" => 2,
                "stock" => 20,
                "create_time" => time()
            ];
            $res = Db::name('ztest')->insertGetId($data);
        }
        return "添加数据成功";
    }

场景三,库存超卖。

库存超卖是一个很常见的秒杀或者其他高并发场景下的数据更新问题。网络上的解决方法也是多种多样,对该问题延伸的数据库乐观锁,悲观锁的知识点也是数不胜数。   

所以,这里我也不再介绍数据库的存储引擎机制,事务,表锁等概念。直接以代码展现,以下是以乐观锁实现的数据库更新问题。

  • 高并发下,对单条记录的修改。一般修改前会对某字段进行判断,但是并发情况下,拿查询的结果进行拦截是极其的不靠谱。不过也可以对查询进行加锁,但是需要在同一事务中。
  • 库存字段添加无符号的字段约束,所以再大的并发在修改为0之后也不会出现负数了,在修改的操作时捕捉修改为负数时的数据库异常。

  • 表中添加version字段,这个也是网上盛传的乐观锁经典实例了,后面的原理和流程我就不介绍了,代码也是这样写的,所以直接贴代码了。
/**
     * @Notes: 高并发乐观锁 - (更新数据)
     * @Interface testConcurrence
     * @return mixed
     * @author: bqs
     * @Time: 2020/6/19 14:25
     */
    public function testConcurrence()
    {
        // 开启事务
        Db::startTrans();
        // 查询ID25当前的库存和版本号
        $curr = Db::name('ztest')->field('stock,version')->where(['id'=>25])->find();
        // 判断库存是否小于0
        if ($curr && $curr['stock'] <= 0) {
            throw new \Exception('物品已售罄',302);
        }
        try {
            // 修改库存 - 获取ID25的行琐
            $updateRes = Db::name('ztest')->where(['id'=>25,'version'=>$curr['version']])->update(['stock'=>$curr['stock']-1,'version'=>$curr['version']+1]);
            // 标识并发过来修改的,拿到的version太旧,事务回滚重新回到查询再走一遍
            if (!$updateRes) {
                Db::rollback();
            }
        } catch(\Exception $e) {
            Db::rollback();
            // 记录日志,或者返回
        }
        // 事务提交
        Db::commit();
        return '购买成功了';
    }

以上就是PHP高并发高负载下的3种实战场景解决方法示例的详细内容,更多关于PHP高并发高负载场景的资料请关注脚本之家其它相关文章!

相关文章

  • PHP 中关于ord($str)&gt;0x80的详细说明

    PHP 中关于ord($str)&gt;0x80的详细说明

    为了识别双字节的字符,比如汉字或日文韩文等都是占两字节的,每字节高位为1,而一般西文字符只有一个字节,七位有效编码,高位为0而0x80对应的二进制代码为1000 0000,最高位为一,代表汉字.汉字编码格式通称为10格式. 一个汉字占2字节,但只代表一个字符
    2012-09-09
  • 遍历指定目录下的所有目录和文件的php代码

    遍历指定目录下的所有目录和文件的php代码

    遍历指定目录下的所有目录和文件的php代码,需要的朋友可以参考下。
    2011-11-11
  • php实现比较全的数据库操作类

    php实现比较全的数据库操作类

    这篇文章主要介绍了php实现比较全的数据库操作类,可实现基本的数据库连接、执行SQL语句及错误提示等相关技巧,需要的朋友可以参考下
    2015-06-06
  • php中Ctype函数用法详解

    php中Ctype函数用法详解

    这篇文章主要介绍了php中Ctype函数用法,以实例形式较为详细的分析了Ctype函数的具体用法,对于进行字符串检测具有一定的实用价值,需要的朋友可以参考下
    2014-12-12
  • PHP PDO fetch 模式各种参数的输出结果一览

    PHP PDO fetch 模式各种参数的输出结果一览

    这篇文章主要介绍了PHP PDO fetch 模式各种参数的输出结果一览,本文目的是方便根据实际需求选择不同的参数,从而不用一个一个测试过去,需要的朋友可以参考下
    2015-01-01
  • PHP中的访问修饰符简单比较

    PHP中的访问修饰符简单比较

    在本篇文章里小编给大家分享了关于PHP中的public,private和protected的简单比较,有需要的朋友们学习下。
    2019-02-02
  • PHP获取网页所有连接的方法(附demo源码下载)

    PHP获取网页所有连接的方法(附demo源码下载)

    这篇文章主要介绍了PHP获取网页所有连接的方法,涉及PHP基于curl针对网页的操作技巧,并带附demo源码供读者下载参考,需要的朋友可以参考下
    2016-03-03
  • PHP laravel实现导出PDF功能

    PHP laravel实现导出PDF功能

    有时候我们会需要使用PHP导出pdf。这篇文章主要是记录一下laravel实现导出PDF的两种方式。文中的示例代码讲解详细,需要的可以参考一下
    2022-10-10
  • 浅谈Swoole并发编程的魅力

    浅谈Swoole并发编程的魅力

    PHP语言是一个短生命周期的Web编程语言,很多PHPer已经形成了fpm下编程的思维定势。实际上在Swoole出现之后,这种串行化编程的模式早已被打破。使用Swoole完全可以轻易实现更灵活的并发编程。
    2021-05-05
  • PHP pthreads v3下同步处理synchronized用法示例

    PHP pthreads v3下同步处理synchronized用法示例

    这篇文章主要介绍了PHP pthreads v3下同步处理synchronized用法,结合实例形式分析了PHP pthreads v3下同步处理synchronized原理、使用方法及相关操作注意事项,需要的朋友可以参考下
    2020-02-02

最新评论