本文目录导读:

在PHP项目中处理并发访问问题,主要关注数据一致性和资源竞争,PHP本身是单线程的(每个请求独立进程/线程),但高并发时多个请求同时操作同一资源(如数据库、文件、内存)就会出问题。
以下是PHP处理并发的常见策略,按使用场景推荐:
数据库层面:MySQL 行级锁 / 乐观锁
这是最常用、最可靠的方式,适合绝大多数业务(如减库存、转账)。
✅ 方案A:悲观锁(SELECT ... FOR UPDATE)
- 适用场景:并发冲突概率高,数据一致性要求极高(如金融交易)。
- 原理:事务中,读取数据时直接锁定该行,其他事务必须等待。
- 注意:必须保证
SELECT走索引,否则锁表。
// 开启事务
$db->beginTransaction();
try {
// 锁定该行,其他请求必须等待
$stmt = $db->prepare("SELECT stock FROM products WHERE id = ? FOR UPDATE");
$stmt->execute([$productId]);
$row = $stmt->fetch();
if ($row['stock'] <= 0) throw new Exception("库存不足");
// 执行更新
$db->exec("UPDATE products SET stock = stock - 1 WHERE id = $productId");
$db->commit();
} catch (Exception $e) {
$db->rollBack();
}
✅ 方案B:乐观锁(CAS + 版本号)
- 适用场景:读多写少,冲突概率低(如文章点赞、用户资料更新)。
- 原理:更新时检查数据版本号(或条件)是否与读取时一致。
$version = 1; // 从数据库读取的当前版本号
$affected = $db->exec(
"UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = $productId AND version = $version"
);
if ($affected === 0) {
// 版本冲突,说明有其他请求已修改,重试或报错
}
应用层面互斥:文件锁 / Redis 锁
当操作不是单一SQL,而是复杂逻辑(如扣库存后发消息、更新缓存)时,需要应用层锁。
✅ 方案A:PHP 文件锁(flock)
- 适用场景:简单、无外部依赖的小型项目,锁单个文件。
- 缺点:文件锁跨服务器无效(多机部署不适用)。
$fp = fopen('/tmp/order.lock', 'r');
if (flock($fp, LOCK_EX)) { // 阻塞等待
// 执行并发敏感操作
flock($fp, LOCK_UN);
}
fclose($fp);
✅ 方案B:Redis 分布式锁(高并发首选 - 推荐)
- 适用场景:多服务器、高并发、高性能要求。
- 原理:利用 Redis 原子性
SET NX EX,设置一个key作为锁,成功则持有锁。 - 实现:使用
RedLock算法或简单锁(用set key value NX EX 10)。
// 加锁:setnx + expire(Redis 2.6后推荐直接用 set ex nx)
$lockKey = "lock:product:{$productId}";
$token = uniqid(); // 用于释放锁时验证持有者
$locked = $redis->set($lockKey, $token, ['nx', 'ex' => 10]);
if ($locked) {
try {
// 执行并发敏感操作(减库存等)
} finally {
// 释放锁:只释放自己持有的锁(防止误删别人的锁)
if ($redis->get($lockKey) === $token) {
$redis->del($lockKey);
}
}
} else {
// 获取锁失败,重试或报错
}
消息队列:削峰填谷
- 适用场景:秒杀、批量订单等瞬时流量巨大,但不需要实时返回结果。
- 原理:将请求放入队列(如RabbitMQ、Redis List),后端单线程/限流消费。
- 优点:保护数据库,避免瞬间压力。
// 接收请求后,不直接操作DB,而是写入消息队列
$redis->lPush('order_queue', json_encode(['user_id' => $uid, 'product_id' => $pid]));
// 后台独立进程消费(如 Workerman / think-queue)
while ($data = $redis->brPop('order_queue', 5)) {
// 单线程执行数据库操作,天然无并发问题
}
原子操作:Redis / 数据库计数器
无需锁,利用数据库/Redis的原子操作完成简单增减。
- Redis原子增减:
DECRINCR(适合点赞量、库存总量统计) - MySQL原子更新:
UPDATE ... SET count = count - 1 WHERE count > 0
// 库存扣减(原子性,无需锁)
$result = $db->exec("UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0");
if ($result === 0) {
// 库存不足
}
前端/网关限流 + 重试机制
- 限流:Nginx
limit_req、Redis 漏桶/令牌桶、API 层面RateLimiter。 - 重试:用户点击后按钮置灰+loading,请求失败自动重试(幂等性)。
- 去重令牌:每次请求带唯一ID(如UUID),Redis检查是否已处理(防重复下单)。
总结选择建议
| 场景 | 推荐方案 |
|---|---|
| 数据库操作(减库存) | MySQL行锁(悲观/乐观)或 原子Update |
| 复杂业务逻辑(订单+发券+短信) | Redis分布式锁 |
| 瞬时大流量(秒杀) | 消息队列 + 前端限流 + 异步处理 |
| 跨服务器 / 云原生 | Redis分布式锁(RedLock) |
| 简单计数器(点赞/浏览) | Redis INCR 或 MySQL UPDATE SET count=count+1 |
最后忠告:不要依赖 session 或 $_SESSION 来处理并发锁(PHP session默认文件锁,但会阻塞同用户后续请求),也不要使用 sleep 或 usleep 来“等待”,那只会浪费资源。
最简洁可靠的金科玉律:能用数据库原子操作解决的,绝不加锁;能加乐观锁的,绝不用悲观锁;必须用锁时,优先考虑Redis分布式锁。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播