在PHP接口开发中,当多个用户同时发起请求时,很容易出现重复操作、数据覆盖、库存超扣等并发问题,我们需要同时做好并发控制和问题调试两方面的工作。下面结合实际场景介绍具体的实现和调试方法。

PHP接口并发控制的常用方案
1. 文件锁实现本地并发控制
如果是单机部署的PHP接口,可以使用文件锁来限制同一时间只有一个请求能执行核心逻辑,避免资源竞争。示例如下:
<?php
/**
* 使用文件锁控制接口并发
* @param string $lockFile 锁文件路径
* @param callable $callback 要执行的核心逻辑
* @return mixed
*/
function handleWithFileLock($lockFile, $callback) {
// 打开锁文件,不存在则创建
$fp = fopen($lockFile, 'w+');
if (!$fp) {
return ['code' => 500, 'msg' => '锁文件打开失败'];
}
// 尝试获取排他锁,阻塞等待
if (flock($fp, LOCK_EX)) {
try {
// 执行核心业务逻辑
$result = $callback();
// 释放锁
flock($fp, LOCK_UN);
fclose($fp);
return $result;
} catch (Exception $e) {
flock($fp, LOCK_UN);
fclose($fp);
return ['code' => 500, 'msg' => '业务执行异常:' . $e->getMessage()];
}
} else {
fclose($fp);
return ['code' => 500, 'msg' => '获取锁失败'];
}
}
// 接口调用示例
$lockPath = __DIR__ . '/order_lock.txt';
$response = handleWithFileLock($lockPath, function() {
// 模拟库存扣减逻辑
$stock = 10;
if ($stock > 0) {
// 模拟数据库操作耗时
usleep(100000);
$stock--;
return ['code' => 200, 'msg' => '下单成功,剩余库存:' . $stock];
} else {
return ['code' => 400, 'msg' => '库存不足'];
}
});
echo json_encode($response);
?>2. Redis分布式锁实现多机并发控制
如果是多机部署的接口,文件锁就无法生效,此时可以使用Redis实现分布式锁,借助setnx命令的原子性来保证同一时间只有一个请求能获取锁。示例如下:
<?php
/**
* 使用Redis分布式锁控制接口并发
* @param Redis $redis Redis连接实例
* @param string $lockKey 锁的键名
* @param int $expire 锁的过期时间,单位秒
* @param callable $callback 核心业务逻辑
* @return mixed
*/
function handleWithRedisLock($redis, $lockKey, $expire, $callback) {
$lockValue = uniqid();
// 尝试获取锁,setnx保证原子性,同时设置过期时间避免死锁
$lockResult = $redis->set($lockKey, $lockValue, ['nx', 'ex' => $expire]);
if (!$lockResult) {
return ['code' => 429, 'msg' => '请求过于频繁,请稍后再试'];
}
try {
$result = $callback();
// 释放锁,先检查锁的值是否是自己设置的,避免误删其他请求的锁
$currentValue = $redis->get($lockKey);
if ($currentValue == $lockValue) {
$redis->del($lockKey);
}
return $result;
} catch (Exception $e) {
$currentValue = $redis->get($lockKey);
if ($currentValue == $lockValue) {
$redis->del($lockKey);
}
return ['code' => 500, 'msg' => '业务执行异常:' . $e->getMessage()];
}
}
// 使用示例
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lockKey = 'order_lock_' . date('Ymd');
$response = handleWithRedisLock($redis, $lockKey, 5, function() {
// 模拟库存扣减逻辑
$stock = 10;
if ($stock > 0) {
usleep(100000);
$stock--;
return ['code' => 200, 'msg' => '下单成功,剩余库存:' . $stock];
} else {
return ['code' => 400, 'msg' => '库存不足'];
}
});
echo json_encode($response);
?>资源竞争问题的调试方法
1. 添加详细业务日志
在核心逻辑的前后添加日志,记录请求的ID、执行时间、操作前后的数据状态,方便后续排查问题。示例如下:
<?php
// 记录日志的辅助函数
function writeLog($content) {
$logFile = __DIR__ . '/interface_log_' . date('Ymd') . '.log';
$logContent = '[' . date('Y-m-d H:i:s') . '] ' . $content . PHP_EOL;
file_put_contents($logFile, $logContent, FILE_APPEND);
}
// 在核心逻辑中添加日志
$requestId = uniqid('req_');
writeLog("请求{$requestId}开始执行,当前库存:10");
// 执行库存扣减逻辑
$stock = 10;
usleep(100000);
$stock--;
writeLog("请求{$requestId}执行完成,剩余库存:{$stock}");
?>2. 模拟并发请求测试
可以使用Apache Bench或者自己写简单的并发脚本模拟多个请求同时调用接口,观察返回结果是否符合预期。以下是简单的PHP并发测试脚本:
<?php
// 并发测试脚本
$url = 'http://127.0.0.1/order_api.php';
$requestNum = 20; // 模拟20个并发请求
$chList = [];
$mh = curl_multi_init();
for ($i = 0; $i < $requestNum; $i++) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_multi_add_handle($mh, $ch);
$chList[] = $ch;
}
// 执行并发请求
do {
$status = curl_multi_exec($mh, $active);
if ($active) {
curl_multi_select($mh);
}
} while ($active && $status == CURLM_OK);
// 获取结果
foreach ($chList as $ch) {
$response = curl_multi_getcontent($ch);
echo "请求结果:" . $response . PHP_EOL;
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
?>3. 校验数据一致性
执行完并发测试后,检查数据库中的最终数据是否符合预期,比如库存扣减的总数是否等于成功的请求数,是否存在超扣的情况。如果出现数据不一致,可以结合日志定位是哪个环节出现了问题。
注意事项
- 使用文件锁时要注意锁文件的权限,避免权限不足导致获取锁失败
- Redis分布式锁的过期时间要设置合理,既不能太短导致业务没执行完锁就过期,也不能太长导致异常时锁长时间不释放
- 调试过程中不要直接在正式环境进行高并发测试,避免影响正常用户使用
- 所有涉及资源操作的代码都要做好异常处理,避免异常导致锁无法释放引发死锁