PHP 先查后更安全吗

wen PHP项目 3

本文目录导读:

PHP 先查后更安全吗

  1. 什么是“先查后更”?
  2. 为什么“先查后更”不安全?
  3. 那么,直接 UPDATE 就安全吗?
  4. 推荐的“安全”写法(原子操作)
  5. 什么时候“先查后更”是安全的?
  6. 总结建议

这是一个非常经典且重要的问题,直接给结论:“先查后更”不一定更安全,反而可能引入新的并发问题;在多数情况下,使用“条件更新”(原子操作)才是更安全的选择。

下面详细拆解这个问题,并给出正确的实践方案。

什么是“先查后更”?

这是指代码中常见的模式:

// 1. 先查询
$user = $db->query("SELECT * FROM users WHERE id = 1");
// 2. 在 PHP 中判断
if ($user['balance'] >= 100) {
    // 3. 再更新
    $db->query("UPDATE users SET balance = balance - 100 WHERE id = 1");
}

为什么“先查后更”不安全?

它最大的问题在于并发(Race Condition,竞态条件)

假设两个请求(例如用户同时点击两次“购买”)同时到达:

  1. 请求 A 查询:余额为 100。
  2. 请求 B 查询:余额为 100。
  3. 请求 A 判断:100 >= 100,执行扣款,余额变为 0。
  4. 请求 B 判断:100 >= 100,执行扣款,余额变为 -100。

这种情况下,“先查”的结果已经过期了,导致数据不一致。


直接 UPDATE 就安全吗?

也不一定,如果你直接写 UPDATE users SET balance = balance - 100 WHERE id = 1,在没有条件判断的情况下,虽然不会产生负数(取决于你是否有 CHECK 约束),但无法防止逻辑上的超卖


推荐的“安全”写法(原子操作)

核心思路:把“判断”和“更新”合并成一个 SQL 语句,让数据库来保证原子性。

方案 A:条件更新(推荐,最常用)

// 意图:扣款 100,但要求当前余额必须 >= 100
$affectedRows = $db->execute(
    "UPDATE users 
     SET balance = balance - 100 
     WHERE id = ? AND balance >= 100",
    [1]
);
// 关键判断:受影响的行数
if ($affectedRows > 0) {
    // 更新成功,说明余额充足,扣款完成
} else {
    // 更新失败,说明余额不足(或者用户不存在)
}

原理:数据库在执行 UPDATE 时,会对匹配的行加锁。WHERE balance >= 100 这个条件在锁内判断,是绝对准确的,如果条件不满足,影响行数为 0,你就知道不能扣款。

方案 B:使用事务 + 排他锁(仅当需要先查询复杂逻辑时)

如果逻辑比较复杂,要先读取 A 表数据,计算后写入 B 表”,且计算过程无法用纯 SQL 表达,那么必须用事务:

$pdo->beginTransaction();
try {
    // 1. 使用 SELECT ... FOR UPDATE 锁定该行(重要!)
    $stmt = $pdo->query("SELECT balance FROM users WHERE id = 1 FOR UPDATE");
    $balance = $stmt->fetchColumn();
    // 2. 在 PHP 中做业务判断
    if ($balance < 100) {
        throw new Exception("余额不足");
    }
    // 3. 执行更新
    $pdo->exec("UPDATE users SET balance = balance - 100 WHERE id = 1");
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
}

原理FOR UPDATE 会锁定这一行数据,在事务提交前,其他请求的 SELECT ... FOR UPDATEUPDATE 会被阻塞,直到第一个事务结束,这样就保证了“查”和“更”之间没有缝隙。


什么时候“先查后更”是安全的?

只有在非并发、低压力、单用户数据允许最终一致的场景下(比如后台手动修改配置),先查后更才够用,对于涉及金额、库存这类关键数据,一定要避免。


总结建议

  1. 简单条件(如库存扣减)直接用 UPDATE ... WHERE condition + 检查 rowCount(),这是性能最好、最安全的方式。
  2. 复杂业务逻辑(如多重条件判断)使用事务 + SELECT ... FOR UPDATE
  3. 绝对不要:在非事务环境下,先 SELECT 出来判断,再 UPDATE

如果你使用的是 LaravelThinkPHP,它们也提供了对应的原子更新方法(如 incrementwhere 条件结合),实战中应优先使用这些框架封装好的原子操作。

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