一、两种方案正面对比
方案 A:事务 + SELECT ... FOR UPDATE
DB::transaction(function () {
$user = DB::table('users')
->where('id', $id)
->lockForUpdate()
->first();
if ($user->balance >= $amount) {
DB::table('users')
->where('id', $id)
->update(['balance' => $user->balance - $amount]);
}
});
方案 B:原子 UPDATE
$affected = DB::update(
'UPDATE users
SET balance = balance - ?
WHERE id = ? AND balance >= ?',
[$amount, $id, $amount]
);
二、为什么方案 B 效率更高
1️⃣ SQL 次数:1 次 vs 2 次
| 方案 | SQL |
|---|---|
| FOR UPDATE | SELECT + UPDATE |
| 原子 UPDATE | UPDATE(1 次) |
少一次:
-
网络 IO
-
SQL 解析
-
执行计划
-
锁切换
👉 在高并发下差距非常明显
2️⃣ 锁的持有时间:极短
FOR UPDATE
-
锁从
SELECT开始 -
一直到事务提交
-
中间可能跑 PHP 逻辑、日志、异常处理
原子 UPDATE
-
锁只在 一条 UPDATE 执行期间
-
几乎是毫秒级
👉 锁时间越短,并发吞吐越高
3️⃣ 锁的范围更小、更可控
WHERE id = ? AND balance >= ?
-
主键精确命中
-
只锁 1 行
-
不产生多余间隙锁
而:
SELECT ... FOR UPDATE
-
如果条件不严谨
-
可能锁多行 / 锁范围扩大
-
更容易死锁
三、并发安全性:谁更强?
| 维度 | FOR UPDATE | 原子 UPDATE |
|---|---|---|
| 防超扣 | ✅ | ✅ |
| 防并发 | ✅ | ✅ |
| 防逻辑漏洞 | ⚠️ 依赖代码 | ✅ 数据库保证 |
| 出错概率 | 较高 | 极低 |
👉 原子 UPDATE 把风险从「代码层」下沉到「数据库层」
四、那 FOR UPDATE 什么时候才值得用?
✅ 适合 FOR UPDATE 的场景
-
必须先读,再做复杂决策
余额 + 状态 + 规则 + 多字段计算 -
要锁多行 / 多表
-
订单 + 库存 + 账户
-
且逻辑无法合并成一条 SQL
-
-
需要保证"读到的就是将要修改的"
❌ 不适合 FOR UPDATE 的场景
-
单字段扣减(余额 / 库存 / 次数)
-
能写成:
UPDATE ... WHERE 条件 -
高并发、低延迟要求
👉 这正是支付扣款场景
FINALLY:
💰 钱 / 库存 / 次数
✅ 原子 UPDATE
UPDATE ... SET x = x - ? WHERE x >= ?
📦 复杂业务状态流转
✅ transaction + FOR UPDATE