本文目录导读:

在 PHP 中实现“无锁变更”(通常指数据库层面的无锁更新),主要有以下几种方案:
乐观锁(CAS - Compare and Swap)
最常用且推荐的方式:
// 方案1:使用版本号
$userId = 1;
$newBalance = 500;
$version = 3; // 从查询中获取
$sql = "UPDATE user_accounts
SET balance = ?, version = version + 1
WHERE user_id = ? AND version = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$newBalance, $userId, $version]);
// 检查是否更新成功
if ($stmt->rowCount() === 0) {
// 版本不匹配,说明其他请求已修改
// 重试或返回冲突
}
// 方案2:使用时间戳
$sql = "UPDATE user_accounts
SET balance = ?, updated_at = NOW()
WHERE user_id = ? AND updated_at = ?";
数据库原子操作(适合数值型)
// 使用数据库自带的原子操作
// 不需要先查询,直接更新
$sql = "UPDATE user_accounts
SET balance = balance + ?
WHERE user_id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([100, $userId]);
// 原子更新,不需要锁
Redis 分布式锁(跨进程)
// 使用 Redis 实现分布式锁
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lockKey = "user:{$userId}:lock";
$token = uniqid();
// 获取锁(使用 SET NX 原子操作)
if ($redis->set($lockKey, $token, ['NX', 'EX' => 10])) {
try {
// 执行更新操作
updateDatabase($userId);
} finally {
// 释放锁(使用 Lua 脚本保证原子性)
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
$redis->eval($script, [$lockKey, $token], 1);
}
}
条件更新(SQL WHERE 条件)
// 利用 SQL 条件表达式实现无锁
$sql = "UPDATE orders
SET status = 'paid', updated_at = NOW()
WHERE id = ? AND status = 'pending'";
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
if ($stmt->rowCount() > 0) {
// 更新成功,说明状态符合预期
}
完整示例:乐观锁重试机制
class OptimisticLockUpdater {
private $pdo;
private $maxRetries = 3;
public function updateWithRetry($userId, callable $operation) {
for ($i = 0; $i < $this->maxRetries; $i++) {
// 1. 读取当前版本
$data = $this->pdo->query(
"SELECT balance, version FROM user_accounts WHERE user_id = {$userId}"
)->fetch(PDO::FETCH_ASSOC);
$currentVersion = $data['version'];
// 2. 执行业务逻辑
$newBalance = $operation($data['balance']);
// 3. 尝试更新(条件是版本号匹配)
$stmt = $this->pdo->prepare(
"UPDATE user_accounts
SET balance = ?, version = version + 1
WHERE user_id = ? AND version = ?"
);
$stmt->execute([$newBalance, $userId, $currentVersion]);
//4. 检查是否成功
if ($stmt->rowCount() > 0) {
return $newBalance; // 成功
}
// 5. 失败则稍后重试
usleep(100000); // 100ms
}
throw new Exception("更新失败,达到最大重试次数");
}
}
数据库特定优化
// MySQL 使用 FOR UPDATE 忽略(其实这是悲观锁)
// 但可以在事务中配合条件使用
// PostgreSQL 的 SKIP LOCKED(避免等待)
$sql = "SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED";
注意事项
-
选择场景:
- 修改频率低 → 乐观锁
- 数据库单节点 → 原子操作
- 多实例部署 → Redis 分布式锁
-
性能考虑:
- 乐观锁在高竞争下会导致大量重试
- 原子操作用途有限(只支持数值运算)
-
特殊情况:
- 更新前需要检查状态 → 条件更新
- 需要保证数据一致性 → 事务 + 锁
-
避免陷阱:
- 版本号最好用整数而非时间戳
- 重试时注意指数退避
- 数据库更新和版本检查必须在同一条 SQL 中
推荐优先使用乐观锁,因为它性能好且不需要额外组件,大多数业务场景都适用。