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 验证码的实现代码
Jul 17 PHP
基于php下载文件的详解
Jun 02 PHP
ThinkPHP之N方法实例详解
Jun 20 PHP
php上传图片之时间戳命名(保存路径)
Aug 15 PHP
Yii实现的多级联动下拉菜单
Jul 13 PHP
php 解决substr()截取中文字符乱码问题
Jul 18 PHP
php利用gd库为图片添加水印
Nov 09 PHP
php连接微软MSSQL(sql server)完全攻略
Nov 27 PHP
PHP 7.1中AES加解密方法mcrypt_module_open()的替换方案
Oct 17 PHP
PHP实现的解汉诺塔问题算法示例
Aug 06 PHP
PHP数组常用函数实例小结
Aug 20 PHP
laravel-admin 在列表页添加自定义按钮的例子
Sep 30 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
利用php来自动调用不同服务器上的flash
2006/10/09 PHP
php出现Cannot modify header information问题的解决方法大全
2008/04/09 PHP
JQuery实现表格中相同单元格合并示例代码
2013/06/26 Javascript
浅谈JSON.parse()和JSON.stringify()
2015/07/14 Javascript
浅析Node.js实现HTTP文件下载
2016/08/05 Javascript
微信小程序使用第三方库Underscore.js步骤详解
2016/09/27 Javascript
localStorage实现便签小程序
2016/11/28 Javascript
原生JS+Canvas实现五子棋游戏实例
2017/06/19 Javascript
VUE2.0+Element-UI+Echarts封装的组件实例
2018/03/02 Javascript
jQuery获取随机颜色的实例代码
2018/05/21 jQuery
浅析Vue实例以及生命周期
2018/08/14 Javascript
浅谈高大上的微信小程序中渲染html内容—技术分享
2018/10/25 Javascript
Vue入门学习笔记【基本概念、对象、过滤器、指令等】
2019/04/13 Javascript
mock.js模拟前后台交互
2019/07/25 Javascript
Vue内部渲染视图的方法
2019/09/02 Javascript
深入理解javascript prototype的相关知识
2019/09/19 Javascript
Vue列表如何实现滚动到指定位置样式改变效果
2020/05/09 Javascript
vue+iview分页组件的封装
2020/11/17 Vue.js
Python实现的检测web服务器健康状况的小程序
2014/09/17 Python
在Python下使用Txt2Html实现网页过滤代理的教程
2015/04/11 Python
Python分析学校四六级过关情况
2017/11/22 Python
对python特殊函数 __call__()的使用详解
2019/07/02 Python
Python环境Pillow( PIL )图像处理工具使用解析
2019/09/12 Python
python数据化运营的重要意义
2019/11/25 Python
xadmin使用formfield_for_dbfield函数过滤下拉表单实例
2020/04/07 Python
Pandas将列表(List)转换为数据框(Dataframe)
2020/04/24 Python
详解python变量与数据类型
2020/08/25 Python
CSS3实现的炫酷菜单代码分享
2015/03/12 HTML / CSS
iHerb俄罗斯:维生素、补品和天然产品
2020/07/09 全球购物
茱莉蔻美国官网:Jurlique美国
2020/11/24 全球购物
村官学习十八大感想
2014/01/15 职场文书
大学生上课迟到检讨书
2014/10/15 职场文书
社区干部培训心得体会
2016/01/06 职场文书
2016年领导干部廉政承诺书
2016/03/24 职场文书
心得体会该怎么写呢?
2019/06/27 职场文书
nginx限制并发连接请求数的方法
2021/04/01 Servers