Webman中需手动实现悲观锁或乐观锁:悲观锁须关闭PDO自动提交、显式事务包裹SELECT FOR UPDATE并确保索引命中;乐观锁必须单条SQL校验version并检查rowCount(),Redis分布式锁适用于多Worker场景。

Webman 中处理并发资源竞争,不能直接套用 Laravel 的 lockForUpdate() 或 Eloquent 乐观锁机制——它没有内置的模型锁方法,也不自动管理事务生命周期。必须手动控制 PDO 事务、SQL 锁语句和版本校验逻辑。
Webman 怎么用 SELECT FOR UPDATE 实现悲观锁
Webman 基于 Swoole,PDO 默认是长连接且自动提交开启(PDO::ATTR_AUTOCOMMIT => true),这会导致 SELECT ... FOR UPDATE 立即释放锁,完全失效。
- 必须显式关闭自动提交:
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, false) - 整个“查-算-改”流程必须包裹在
$pdo->beginTransaction()和$pdo->commit()之间 - WHERE 条件必须命中索引,否则可能升级为表锁或间隙锁;例如
WHERE id = ?要求id是主键或有索引 - 执行
SELECT ... FOR UPDATE后,若其他请求同时尝试相同语句,会阻塞直到前一个事务结束;超时由 MySQL 的innodb_lock_wait_timeout控制(默认 50 秒) - 避免死锁:所有服务端对同一组记录加锁时,必须保持一致的查询顺序(如总是先锁 user_id 小的,再锁大的)
示例片段:
$pdo = Db::getPdo();
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT balance, version FROM users WHERE id = ? FOR UPDATE");
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
<pre class="brush:php;toolbar:false;">if ($row['balance'] prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
$stmt->execute([$amount, $userId]);
$pdo->commit();} catch (Exception $e) { $pdo->rollback(); throw $e; }
Webman 手动实现乐观锁的正确姿势
乐观锁在 Webman 中更轻量,但极易因“读-改-写”拆成两步而失效——比如先 $user->version++ 再 $user->save(),两个并发请求会读到相同 version,都写入 version + 1,导致丢失一次更新。
- 必须把版本校验和更新压进**单条 SQL**:
UPDATE table SET col = ?, version = version + 1 WHERE id = ? AND version = ? - 严格检查
$stmt->rowCount():返回 0 表示更新失败(别人已抢先修改),此时应重试或返回错误,不能忽略 - 不要依赖 ORM 自动填充 version 字段;Webman 无 Eloquent,需自己拼 SQL 或封装通用 updateWithVersion() 方法
- 时间戳字段(如
updated_at)可替代version,但要注意 MySQL 的NOW()精度(秒级)可能引发误判,高并发下建议用微秒或自增整数版 version
关键判断逻辑:
$stmt = $pdo->prepare("UPDATE users SET balance = balance - ?, version = version + 1 WHERE id = ? AND version = ?");
$stmt->execute([$amount, $userId, $expectedVersion]);
if ($stmt->rowCount() === 0) {
// 并发冲突:数据已被他人修改
throw new OptimisticLockException('version mismatch');
}
Webman 里 sharedLock() 和 lockForUpdate() 没有对应方法
Webman 的 Db 组件不提供 sharedLock() 或 lockForUpdate() 这类链式调用。所谓“共享锁”在 MySQL 中是 SELECT ... LOCK IN SHARE MODE,但它**不防丢失更新**——两个事务都能读到同一行并各自更新,后提交者会覆盖前者。这不是业务需要的并发控制。
- 真正要防覆盖写,只能用
SELECT ... FOR UPDATE(排他锁)或乐观锁 - 别试图用
LOCK IN SHARE MODE替代,它只适合“读完就走、不改数据”的场景,比如生成报表前确保数据不被删除 - 如果真要用共享锁,得手写 SQL:
$pdo->query("SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE"),但仍需事务包裹,且意义有限
Redis 分布式锁是 Webman 更现实的选择
Webman 常部署多 Worker 进程,甚至多机器,MySQL 行锁仅限单实例内生效。跨进程/跨机器抢库存、发券等场景,必须用 Redis。
- 用
SET key value NX PX 10000实现带过期的原子加锁,value 必须唯一(如uniqid('', true)),防止误删别人锁 - 解锁必须用 Lua 脚本保证原子性:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end - Webman 可用
redis扩展直连,或封装成RedisLock类,注意设置合理超时(一般 10–30 秒),避免死锁 - 不要用
sleep轮询等锁,Swoole 下会阻塞整个协程;应结合retry+ 指数退避
复杂点在于:Redis 锁只是互斥入口,后续仍要配合数据库乐观锁或幂等设计,否则锁住后 DB 更新失败,状态就乱了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











