PHP 并发场景的几种解决方案


Posted in PHP onJune 14, 2019

在秒杀,抢购等并发场景下,可能会出现超卖的现象,在PHP语言中并没有原生提供并发的解决方案,因此就需要借助其他方式来实现并发控制。

列出常见的解决方案有:

  • 使用队列,额外起一个进程处理队列,并发请求都放到队列中,由额外进程串行处理,并发问题就不存在了,但是要额外进程支持以及处理延迟严重,本文不先不讨论这种方法。
  • 利用数据库事务特征,做原子更新,此方法需要依赖数据库的事务特性。
  • 借助文件排他锁,在处理下单请求的时候,用flock锁定一个文件,成功拿到锁的才能处理订单。

一、利用 Redis 事务特征

redis 事务是原子操作,可以保证订单处理的过程中数据没有被其它并发的进程修改。

示例代码:

<?php
$http = new swoole_http_server("0.0.0.0", 9509);  // 监听 9509

$http->set(array(
  'reactor_num' => 2, //reactor thread num
  'worker_num' => 4  //worker process num
));

$http->on('request', function (swoole_http_request $request, swoole_http_response $response) {
  $uniqid = uniqid('uid-', TRUE);  // 模拟唯一用户ID
  $redis = new Redis();
  $redis->connect('127.0.0.1', 6379);  // 连接 redis

  $redis->watch('rest_count'); // 监测 rest_count 是否被其它的进程更改

  $rest_count = intval($redis->get("rest_count")); // 模拟唯一订单ID
  if($rest_count > 0){
    $value = "{$rest_count}-{$uniqid}"; // 表示当前订单,被当前用户抢到了

    // do something ... 主要是模拟用户抢到单后可能要进行的一些密集运算
    $rand = rand(100, 1000000);
    $sum=0;
    for ($i=0;$i<$rand;$i++){ $sum+=$i; }

   // redis 事务
    $redis->multi();
    $redis->lPush('uniqids', $value);
    $redis->decr('rest_count');
    $replies = $redis->exec(); // 执行以上 redis 事务

   // 如果 rest_count 的值被其它的并发进程更改了,以上事务将回滚
    if(!$replies){
      echo "订单 {$value} 回滚".PHP_EOL;
    }
  }
  $redis->unwatch();
});

$http->start();

使用 ab 测试

$ ab -t 20 -c 10 http://192.168.1.104:9509/

二、利用文件排他锁(阻塞模式)

阻塞模式下,如果进程在获取文件排他锁时,其它进程正在占用锁的话,此进程会挂起等待其它进程释放锁后,并自己获取到锁后,再往下执行。

示例代码:

<?php
$http = new swoole_http_server("0.0.0.0", 9510);

$http->set(array(
  'reactor_num' => 2, //reactor thread num
  'worker_num' => 4  //worker process num
));

$http->on('request', function (swoole_http_request $request, swoole_http_response $response) {

  $uniqid = uniqid('uid-', TRUE);
  $redis = new Redis();
  $redis->connect('127.0.0.1', 6379);

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

  // 阻塞(等待)模式, 要取得独占锁定(写入的程序)
  if(flock($fp,LOCK_EX))  //锁定当前指针
  {
   // 成功取得锁后,放心处理订单
    $rest_count = intval($redis->get("rest_count"));
    $value = "{$rest_count}-{$uniqid}";
    if($rest_count > 0){
      // do something ...
      $rand = rand(100, 1000000);
      $sum=0;
      for ($i=0;$i<$rand;$i++){ $sum+=$i; }

      $redis->lPush('uniqids', $value);
      $redis->decr('rest_count');
    }

   // 订单处理完成后,再释放锁
    flock($fp,LOCK_UN);
  }
  fclose($fp);

});

$http->start();

使用 ab 测试

$ ab -t 20 -c 10 http://192.168.1.104:9510/

三、利用文件排他锁(非阻塞模式)

非阻塞模式下,如果进程在获取文件排他锁时,其它进程正在占用锁的话,此进程会马上判断获取锁失败,并且继续往下执行。

示例代码:

<?php
$http = new swoole_http_server("0.0.0.0", 9511);

$http->set(array(
  'reactor_num' => 2, //reactor thread num
  'worker_num' => 4  //worker process num
));

$http->on('request', function (swoole_http_request $request, swoole_http_response $response) {

  $uniqid = uniqid('uid-', TRUE);
  $redis = new Redis();
  $redis->connect('127.0.0.1', 6379);

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

  // 非阻塞模式, 如果不希望 flock() 在锁定时堵塞,则给 lock 加上 LOCK_NB
  if(flock($fp,LOCK_EX | LOCK_NB))  //锁定当前指针
  {
   // 成功取得锁后,放心处理订单
    $rest_count = intval($redis->get("rest_count"));
    $value = "{$rest_count}-{$uniqid}";
    if($rest_count > 0){
      // do something ...
      $rand = rand(100, 1000000);
      $sum=0;
      for ($i=0;$i<$rand;$i++){ $sum+=$i; }

      $redis->lPush('uniqids', $value);
      $redis->decr('rest_count');
    }

   // 订单处理完成后,再释放锁
    flock($fp,LOCK_UN);
  } else {
   // 如果获取锁失败,马上进入这里执行
    echo "{$uniqid} - 系统繁忙,请稍后再试".PHP_EOL;
  }
  fclose($fp);

});

$http->start();

使用 ab 测试

$ ab -t 20 -c 10 http://192.168.1.104:9511/

最后给出三种处理方式的测试结果比较

redis 事务方式:

......
Concurrency Level:   10
Time taken for tests:  20.005 seconds
Complete requests:   17537
Failed requests:    0
Total transferred:   2578380 bytes
HTML transferred:    0 bytes
Requests per second:  876.62 [#/sec] (mean)
Time per request:    11.407 [ms] (mean)
Time per request:    1.141 [ms] (mean, across all concurrent requests)
Transfer rate:     125.86 [Kbytes/sec] received
......

文件排他锁(阻塞模式):

......
Concurrency Level:   10
Time taken for tests:  20.003 seconds
Complete requests:   8205
Failed requests:    0
Total transferred:   1206282 bytes
HTML transferred:    0 bytes
Requests per second:  410.19 [#/sec] (mean)
Time per request:    24.379 [ms] (mean)
Time per request:    2.438 [ms] (mean, across all concurrent requests)
Transfer rate:     58.89 [Kbytes/sec] received
......

文件排他锁(非阻塞模式):

......
Concurrency Level:   10
Time taken for tests:  20.002 seconds
Complete requests:   8616
Failed requests:    0
Total transferred:   1266846 bytes
HTML transferred:    0 bytes
Requests per second:  430.77 [#/sec] (mean)
Time per request:    23.214 [ms] (mean)
Time per request:    2.321 [ms] (mean, across all concurrent requests)
Transfer rate:     61.85 [Kbytes/sec] received
......

经测试结果对比,redis 事务方式优于文件排他锁方式,而文件排他锁方式中,非阻塞模式优于阻塞模式。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
PHP 分页类(模仿google)-面试题目解答
Sep 13 PHP
PHP+Mysql+jQuery实现动态展示信息
Oct 08 PHP
php中引用符号(&amp;)的使用详解
Nov 13 PHP
php中chdir()函数用法实例
Nov 13 PHP
PHP图像处理之imagecreate、imagedestroy函数介绍
Nov 19 PHP
php绘制一个矩形的方法
Jan 24 PHP
PHP访问Google Search API的方法
Mar 05 PHP
优化WordPress的Google字体以加速国内服务器上的运行
Nov 24 PHP
微信支付PHP SDK ―― 公众号支付代码详解
Sep 13 PHP
PHP面向对象之领域模型+数据映射器实例(分析)
Jun 21 PHP
PHP二维数组实现去除重复项的方法【保留各个键值】
Dec 21 PHP
php在windows环境下获得cpu内存实时使用率(推荐)
Feb 08 PHP
PHP 实现文件压缩解压操作的方法
Jun 14 #PHP
php反射学习之依赖注入示例
Jun 14 #PHP
php反射学习之不用new方法实例化类操作示例
Jun 14 #PHP
PHP反射学习入门示例
Jun 14 #PHP
PHP如何实现阿里云短信sdk灵活应用在项目中的方法
Jun 14 #PHP
PHP中常用的三种设计模式详解【单例模式、工厂模式、观察者模式】
Jun 14 #PHP
PHP面向对象程序设计子类扩展父类(子类重新载入父类)操作详解
Jun 14 #PHP
You might like
深入phpMyAdmin的安装与配置的详细步骤
2013/05/07 PHP
php操作redis缓存方法分享
2015/06/03 PHP
PHP创建word文档的方法(平台无关)
2016/03/29 PHP
php设计模式之模板模式实例分析【星际争霸游戏案例】
2020/03/24 PHP
CentOS7系统搭建LAMP及更新PHP版本操作详解
2020/03/26 PHP
Date对象格式化函数代码
2010/07/17 Javascript
js获取键盘按键响应事件(兼容各浏览器)
2013/05/16 Javascript
jquery序列化表单去除指定元素示例代码
2014/04/10 Javascript
AngularJS Bootstrap详细介绍及实例代码
2016/07/28 Javascript
自动化测试读写64位操作系统的注册表
2016/08/15 Javascript
Javascript中关于Array.filter()的妙用详解
2016/12/04 Javascript
jQuery的事件预绑定
2016/12/05 Javascript
JQuery页面随滚动条动态加载效果的简单实现(推荐)
2017/02/08 Javascript
Web安全之XSS攻击与防御小结
2018/12/13 Javascript
vue+element项目中过滤输入框特殊字符小结
2019/08/07 Javascript
一起来了解一下JavaScript的预编译(小结)
2021/03/01 Javascript
深入Python函数编程的一些特性
2015/04/13 Python
Python使用一行代码获取上个月是几月
2018/08/30 Python
Python实现的爬取百度文库功能示例
2019/02/16 Python
基于python的BP神经网络及异或实现过程解析
2019/09/30 Python
关于Numpy中的行向量和列向量详解
2019/11/30 Python
jupyter notebook读取/导出文件/图片实例
2020/04/16 Python
Django 解决distinct无法去除重复数据的问题
2020/05/20 Python
详解CSS3中@media的实际使用
2015/08/04 HTML / CSS
基于 HTML5 的 WebGL 3D 版俄罗斯方块的示例代码
2018/05/28 HTML / CSS
Myprotein西班牙官网:欧洲第一大运动营养品牌
2020/02/24 全球购物
EJB timer的种类
2014/10/28 面试题
Servlet方面面试题
2016/09/28 面试题
经济管理专业求职信
2014/06/09 职场文书
2014光棍节单身联谊活动策划书
2014/10/10 职场文书
2014年统战工作总结
2014/12/09 职场文书
撤诉书怎么写
2015/05/19 职场文书
音乐会主持人开场白
2015/05/28 职场文书
区域销售大会开幕词
2016/03/04 职场文书
2019年二手房买卖合同范本
2019/10/14 职场文书
Sql-Server数据库单表查询 4.3实验课
2021/04/05 SQL Server