PHP项目怎么处理并发访问问题?

wen PHP项目 9

本文目录导读:

PHP项目怎么处理并发访问问题?

  1. 数据库层面:MySQL 行级锁 / 乐观锁
  2. 应用层面互斥:文件锁 / Redis 锁
  3. 消息队列:削峰填谷
  4. 原子操作:Redis / 数据库计数器
  5. 前端/网关限流 + 重试机制
  6. 总结选择建议

在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原子增减DECR INCR(适合点赞量、库存总量统计)
  • 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默认文件锁,但会阻塞同用户后续请求),也不要使用 sleepusleep 来“等待”,那只会浪费资源。

最简洁可靠的金科玉律能用数据库原子操作解决的,绝不加锁;能加乐观锁的,绝不用悲观锁;必须用锁时,优先考虑Redis分布式锁。

抱歉,评论功能暂时关闭!