在秒杀,抢购等并发场景下,可能会出现超卖的现象,在 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:      10Time taken for tests:   20.005 secondsComplete requests:      17537Failed requests:        0Total transferred:      2578380 bytesHTML transferred:       0 bytesRequests 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:      10Time taken for tests:   20.003 secondsComplete requests:      8205Failed requests:        0Total transferred:      1206282 bytesHTML transferred:       0 bytesRequests 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:      10Time taken for tests:   20.002 secondsComplete requests:      8616Failed requests:        0Total transferred:      1266846 bytesHTML transferred:       0 bytesRequests 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教程》

更多相关文章

  1. Linux下查看PHP配置文件php.ini的位置
  2. 分享一个生成文件层级树类
  3. 包含文件include和require在php中的区别(含详解)
  4. PHP获取文件扩展名的实例解析
  5. php代码如何在html文件里面执行(详解)
  6. php实现文件上传到服务器(含代码)
  7. PHP如何实现断点续传大文件?
  8. 关于PHP进程防止内存溢出的排查
  9. PHP如何解压缩zip文件?(代码示例)

随机推荐

  1. android基础知识点复习之短信发送
  2. android studio 0.6.0 下载地址
  3. 手工下载android sdk或者system images等
  4. Android SeekBar
  5. Android SDK Manager 更新SDK问题
  6. mac 编译 Android 系统杂记
  7. Android设计中的.9.png
  8. textAppearance 解答,android系统主题样式
  9. Android 天气预报源码
  10. GridView示例2(自动增长)