如何用PHP项目实现分布式锁?

wen java案例 1

本文目录导读:

如何用PHP项目实现分布式锁?

  1. 基于Redis的分布式锁(最常用)
  2. 基于数据库的分布式锁
  3. 基于ZooKeeper的分布式锁
  4. 基于etcd的分布式锁
  5. 推荐实践建议
  6. 注意事项

在PHP项目中实现分布式锁,主要有以下几种常用方法,我会从简单到复杂依次介绍:

基于Redis的分布式锁(最常用)

简单实现

class RedisDistributedLock {
    private $redis;
    private $lockKey;
    private $lockValue;
    private $ttl;
    public function __construct($redis, $lockKey, $ttl = 10) {
        $this->redis = $redis;
        $this->lockKey = $lockKey;
        $this->lockValue = uniqid('', true); // 唯一标识
        $this->ttl = $ttl;
    }
    // 获取锁
    public function acquire() {
        // SET NX EX 原子操作
        $result = $this->redis->set(
            $this->lockKey,
            $this->lockValue,
            ['NX', 'EX' => $this->ttl]
        );
        return $result === true || $result === 'OK';
    }
    // 释放锁(使用Lua脚本保证原子性)
    public function release() {
        $script = <<<LUA
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
LUA;
        $result = $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
        return $result === 1;
    }
}
// 使用示例
$lock = new RedisDistributedLock($redis, 'order_lock:123', 30);
if ($lock->acquire()) {
    try {
        // 执行临界区代码
        processOrder();
    } finally {
        $lock->release();
    }
}

RedLock算法(高可用实现)

class RedLock {
    private $redisInstances = [];
    private $quorum;
    private $retryDelay = 200; // 毫秒
    private $retryCount = 3;
    public function __construct(array $redisInstances) {
        $this->redisInstances = $redisInstances;
        $this->quorum = count($redisInstances) / 2 + 1;
    }
    public function lock($resource, $ttl = 1000) {
        $token = uniqid('', true);
        $startTime = microtime(true) * 1000;
        for ($i = 0; $i < $this->retryCount; $i++) {
            $successCount = 0;
            $elapsedTime = microtime(true) * 1000 - $startTime;
            foreach ($this->redisInstances as $instance) {
                if ($elapsedTime >= $ttl) break;
                $result = $instance->set(
                    $resource,
                    $token,
                    ['NX', 'PX' => $ttl]
                );
                if ($result) {
                    $successCount++;
                }
            }
            $elapsedTime = microtime(true) * 1000 - $startTime;
            if ($successCount >= $this->quorum && $elapsedTime < $ttl) {
                return ['token' => $token, 'resource' => $resource];
            }
            // 释放已获取的锁
            $this->unlockInternal($resource, $token);
            // 等待重试
            usleep($this->retryDelay * 1000);
        }
        return false;
    }
    public function unlock($lockData) {
        $this->unlockInternal($lockData['resource'], $lockData['token']);
    }
    private function unlockInternal($resource, $token) {
        $script = <<<LUA
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
LUA;
        foreach ($this->redisInstances as $instance) {
            $instance->eval($script, [$resource, $token], 1);
        }
    }
}

基于数据库的分布式锁

MySQL行级锁实现

class MysqlDistributedLock {
    private $pdo;
    private $lockTable = 'distributed_locks';
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    public function acquire($lockName, $timeout = 10) {
        try {
            // 使用GET_LOCK函数(MySQL 5.7+)
            $stmt = $this->pdo->prepare("SELECT GET_LOCK(:lockName, :timeout)");
            $stmt->execute([
                ':lockName' => $lockName,
                ':timeout' => $timeout
            ]);
            return $stmt->fetchColumn() === 1;
        } catch (PDOException $e) {
            return false;
        }
    }
    public function release($lockName) {
        $stmt = $this->pdo->prepare("SELECT RELEASE_LOCK(:lockName)");
        $stmt->execute([':lockName' => $lockName]);
        return $stmt->fetchColumn() === 1;
    }
}

基于数据库行锁的实现

class DatabaseRowLock {
    private $pdo;
    private $lockTable = 'locks';
    public function acquire($lockName, $ttl = 30) {
        $token = uniqid('', true);
        try {
            // 使用悲观锁
            $this->pdo->beginTransaction();
            // 查询并锁定行
            $stmt = $this->pdo->prepare(
                "SELECT * FROM {$this->lockTable} 
                 WHERE lock_name = :lockName 
                 AND (expires_at IS NULL OR expires_at < NOW())
                 FOR UPDATE"
            );
            $stmt->execute([':lockName' => $lockName]);
            if ($stmt->rowCount() > 0) {
                // 更新锁信息
                $updateStmt = $this->pdo->prepare(
                    "UPDATE {$this->lockTable} 
                     SET token = :token, 
                         expires_at = DATE_ADD(NOW(), INTERVAL :ttl SECOND),
                         locked_at = NOW()
                     WHERE lock_name = :lockName"
                );
                $updateStmt->execute([
                    ':token' => $token,
                    ':ttl' => $ttl,
                    ':lockName' => $lockName
                ]);
                $this->pdo->commit();
                return $token;
            }
            // 插入新锁
            $insertStmt = $this->pdo->prepare(
                "INSERT INTO {$this->lockTable} (lock_name, token, locked_at, expires_at)
                 VALUES (:lockName, :token, NOW(), DATE_ADD(NOW(), INTERVAL :ttl SECOND))"
            );
            $insertStmt->execute([
                ':lockName' => $lockName,
                ':token' => $token,
                ':ttl' => $ttl
            ]);
            $this->pdo->commit();
            return $token;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            return false;
        }
    }
    public function release($lockName, $token) {
        $stmt = $this->pdo->prepare(
            "DELETE FROM {$this->lockTable} 
             WHERE lock_name = :lockName AND token = :token"
        );
        $stmt->execute([
            ':lockName' => $lockName,
            ':token' => $token
        ]);
        return $stmt->rowCount() > 0;
    }
}

基于ZooKeeper的分布式锁

class ZooKeeperLock {
    private $zk;
    private $root = '/locks';
    public function __construct($hosts) {
        $this->zk = new ZooKeeper($hosts);
        // 确保根节点存在
        if (!$this->zk->exists($this->root)) {
            $this->zk->create($this->root, '', [
                'flags' => ZooKeeper::EPHEMERAL
            ]);
        }
    }
    public function acquire($lockName, $timeout = 10) {
        $lockPath = $this->root . '/' . $lockName;
        // 创建临时顺序节点
        $createdPath = $this->zk->create($lockPath . '/lock-', '', [
            'flags' => ZooKeeper::EPHEMERAL | ZooKeeper::SEQUENCE
        ]);
        $startTime = time();
        while (true) {
            // 获取所有子节点
            $children = $this->zk->getChildren($lockPath);
            sort($children);
            // 检查是否是第一个
            $nodeName = basename($createdPath);
            if ($nodeName === $children[0]) {
                return $createdPath; // 获取锁成功
            }
            // 监听前一个节点
            $prevNode = $children[array_search($nodeName, $children) - 1];
            // 检查超时
            if (time() - $startTime > $timeout) {
                // 删除创建的节点
                $this->zk->delete($createdPath);
                return false; // 获取锁超时
            }
            // 等待直到前一个节点被删除
            $this->zk->get($lockPath . '/' . $prevNode, function() {
                // 前一个节点被删除,重新检查
            });
        }
    }
    public function release($lockPath) {
        return $this->zk->delete($lockPath);
    }
}

基于etcd的分布式锁

use Etcd\Client;
class EtcdLock {
    private $client;
    private $ttl = 10;
    public function __construct($endpoints) {
        $this->client = new Client($endpoints);
    }
    public function acquire($lockName) {
        $key = '/locks/' . $lockName;
        $value = uniqid('', true);
        // 使用租约(lease)
        $lease = $this->client->grant($this->ttl);
        try {
            // 尝试创建锁
            $this->client->put($key, $value, [
                'lease' => $lease->ID,
                'prevKV' => true
            ]);
            return $value; // 获取锁成功
        } catch (\Exception $e) {
            // 锁已被占用
            $lease->revoke();
            return false;
        }
    }
    public function release($lockName, $value) {
        $key = '/locks/' . $lockName;
        // 只有持有者才能释放
        $response = $this->client->get($key);
        if ($response && $response->kvs[0]->value === $value) {
            $this->client->delete($key);
            return true;
        }
        return false;
    }
    public function refresh($lockName, $value) {
        $key = '/locks/' . $lockName;
        $response = $this->client->get($key);
        if ($response && $response->kvs[0]->value === $value) {
            $this->client->put($key, $value, [
                'lease' => $response->kvs[0]->lease
            ]);
            return true;
        }
        return false;
    }
}

推荐实践建议

选择建议

class LockFactory {
    public static function create($type, $config) {
        switch ($type) {
            case 'redis':
                return new RedisDistributedLock(
                    $config['redis'],
                    $config['lockKey'],
                    $config['ttl'] ?? 30
                );
            case 'mysql':
                return new MysqlDistributedLock($config['pdo']);
            case 'zookeeper':
                return new ZooKeeperLock($config['hosts']);
            case 'etcd':
                return new EtcdLock($config['endpoints']);
            default:
                throw new InvalidArgumentException("Unsupported lock type");
        }
    }
}

使用模版方法模式

abstract class AbstractDistributedLock {
    abstract public function acquire();
    abstract public function release();
    public function execute(callable $callback) {
        if ($this->acquire()) {
            try {
                return $callback();
            } finally {
                $this->release();
            }
        }
        throw new LockFailedException('Failed to acquire lock');
    }
}
// 使用示例
$lock = new RedisDistributedLock($redis, 'process_lock:123', 30);
$result = $lock->execute(function() {
    // 业务逻辑
    return processImportantTask();
});

注意事项

  1. 锁超时处理:设置合理的TTL,避免死锁
  2. 锁粒度:尽量精细,只锁定必要资源
  3. 重试机制:获取锁失败时的重试策略
  4. 监控告警:监控锁的获取和释放情况
  5. 降级策略:缓存降级或服务降级

对于大多数PHP项目,推荐使用Redis实现分布式锁,既简单又高效,如果需要更高的可靠性,可以考虑RedLock算法或ZooKeeper方案。

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