PHP 怎么无锁变更

wen PHP项目 1

本文目录导读:

PHP 怎么无锁变更

  1. 乐观锁(CAS - Compare and Swap)
  2. 数据库原子操作(适合数值型)
  3. Redis 分布式锁(跨进程)
  4. 条件更新(SQL WHERE 条件)
  5. 完整示例:乐观锁重试机制
  6. 数据库特定优化
  7. 注意事项

在 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";

注意事项

  1. 选择场景

    • 修改频率低 → 乐观锁
    • 数据库单节点 → 原子操作
    • 多实例部署 → Redis 分布式锁
  2. 性能考虑

    • 乐观锁在高竞争下会导致大量重试
    • 原子操作用途有限(只支持数值运算)
  3. 特殊情况

    • 更新前需要检查状态 → 条件更新
    • 需要保证数据一致性 → 事务 + 锁
  4. 避免陷阱

    • 版本号最好用整数而非时间戳
    • 重试时注意指数退避
    • 数据库更新和版本检查必须在同一条 SQL 中

推荐优先使用乐观锁,因为它性能好且不需要额外组件,大多数业务场景都适用。

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