PHP API并发控制实战指南:从原理到高并发解决方案
目录导读
- 为什么PHP API需要并发控制?
- 并发控制的核心原理与常见问题
- 基于文件锁的轻量级方案
- Redis分布式锁的成熟实践
- 队列化处理与限流策略
- 数据库乐观锁与悲观锁选择
- 常见问题问答(Q&A)
- 性能优化与注意事项
为什么PHP API需要并发控制?
在微服务架构盛行的今天,PHP作为后端开发语言,经常需要处理高并发请求,想象一个电商秒杀场景:当1000个用户同时请求购买最后一件商品,如果没有并发控制,数据库库存可能被扣减为负数,造成超卖。

并发问题本质:多个请求同时读写共享资源(数据库、文件、Redis等),导致数据不一致,PHP的每个请求默认是独立进程,但共享资源需要同步机制。
并发控制的核心原理与常见问题
核心概念
- 临界区:需要互斥访问的代码块,如“检查库存-扣减库存”
- 原子操作:不可中断的操作序列,如Redis的
INCR - 锁粒度:锁保护资源的范围(行锁>表锁>全局锁)
常见问题
- 脏读:读取未提交的数据
- 不可重复读:同一事务两次读取结果不同
- 幻读:事务内看到其他事务新增的数据
基于文件锁的轻量级方案
适用于单机部署场景,使用PHP的flock()函数:
<?php
$lockFile = '/tmp/api_lock_'.md5($resourceId);
$fp = fopen($lockFile, 'w');
if (flock($fp, LOCK_EX)) { // 获取独占锁
// 临界区:检查库存 -> 扣减库存
$stock = queryStock();
if ($stock > 0) {
updateStock($stock - 1);
}
flock($fp, LOCK_UN); // 释放锁
}
fclose($fp);
?>
优点:无需额外服务,实现简单
缺点:文件IO性能低,不支持分布式,可能死锁
Redis分布式锁的成熟实践
使用Redis原子命令实现多服务器共享锁(基于RedLock算法简化版):
<?php
class RedisLock {
private $redis;
private $lockKey;
private $token;
public function acquire($key, $ttl = 3) {
$this->lockKey = "lock:{$key}";
$this->token = uniqid();
// SET NX PX:仅当key不存在时设置,并设置过期时间(毫秒)
return $this->redis->set($this->lockKey, $this->token, ['NX', 'PX' => $ttl * 1000]);
}
public function release() {
// Lua脚本保证原子性:只有持有锁的进程才能释放
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
return $this->redis->eval($script, [$this->lockKey, $this->token], 1);
}
}
// 使用示例
$lock = new RedisLock($redis);
if ($lock->acquire('product:1')) {
try {
// 业务逻辑
} finally {
$lock->release();
}
}
?>
关键点:设置超时防止死锁,Lua脚本确保释放锁的原子性。
队列化处理与限流策略
消息队列削峰
将并发请求转为串行处理:
- 请求入队(RabbitMQ/Kafka)
- 消费者逐条处理
- 适用于写多读少的场景
令牌桶限流
<?php
class TokenBucket {
private $redis;
private $bucketKey = 'api_token_bucket';
public function allow($cost = 1) {
// Lua脚本实现:初始化桶容量10,速率每秒1个
$script = "
local tokens = redis.call('get', KEYS[1]);
if not tokens then
redis.call('set', KEYS[1], 10);
tokens = 10;
end
if tonumber(tokens) >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1]);
return 1;
else
return 0;
end";
return $this->redis->eval($script, [$this->bucketKey], 0);
}
}
?>
数据库乐观锁与悲观锁选择
乐观锁(适合读多写少)
UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = :current_version;
PHP层检测影响行数affected_rows == 1?
悲观锁(适合写多场景)
BEGIN; SELECT * FROM products WHERE id = 1 FOR UPDATE; -- 业务处理 UPDATE products SET stock = stock - 1 WHERE id = 1; COMMIT;
选择依据:
- 乐观锁:冲突率<5%,重试成本低
- 悲观锁:冲突率高,数据库承压大
常见问题问答(Q&A)
Q1:PHP单进程为什么还需要并发控制?
A:PHP-FPM是多进程模型,多个Worker进程同时处理请求,共享资源(如MySQL、文件、Redis)需要互斥访问。
Q2:Redis锁过期但业务未完成怎么办?
A:可以设置“看门狗”机制,定期续约锁的过期时间(如Redisson的实现),或使用更短的超时配合自动重试。
Q3:文件锁和数据库锁哪个更可靠?
A:数据库锁比文件锁可靠,因为数据库自带事务和故障恢复机制,文件锁在NFS等网络文件系统上可能失效。
Q4:如何选择锁的类型?
A:单机小并发用文件锁;分布式高并发用Redis锁;金融级强一致用数据库悲观锁配合事务。
性能优化与注意事项
- 减少锁持有时间:仅将必要操作放入临界区,避免I/O操作
- 锁粒度控制:尽量使用行级锁而非表锁
- 监控与报警:捕获锁等待超时异常,记录死锁日志
- 优雅降级:锁获取失败时返回友好提示,而非直接报错
- 测试环境验证:使用ab、wrk等工具压测不同并发控制方案
PHP API的并发控制没有银弹,需要根据业务场景权衡:
- 简单场景:文件锁+数据库行锁
- 高并发抢购:Redis分布式锁+消息队列削峰
- 强一致性要求:数据库悲观锁+事务
实际生产环境中,建议采用组合策略:用限流保护系统入口,用Redis锁处理热点资源,用数据库锁兜底保证数据最终一致性,始终牢记:锁只是手段,合理的设计才是高并发系统的核心。
(全文完)