本文目录导读:

我来为您详细讲解PHP定时任务的分布式锁实现方案。
为什么需要分布式锁
在分布式环境中,多个服务器节点可能同时执行同一个定时任务,导致:
- 数据重复处理
- 资源竞争
- 任务重复执行
主流实现方案
Redis分布式锁(最常用)
<?php
class RedisDistributedLock
{
private $redis;
private $lockKey;
private $lockValue;
private $expireTime;
public function __construct($redis, $lockKey, $expireTime = 10)
{
$this->redis = $redis;
$this->lockKey = $lockKey;
$this->expireTime = $expireTime;
$this->lockValue = uniqid('lock_', true) . '_' . mt_rand();
}
/**
* 获取锁(非阻塞)
*/
public function acquire()
{
// 使用SET NX EX原子操作
$result = $this->redis->set(
$this->lockKey,
$this->lockValue,
['NX', 'EX' => $this->expireTime]
);
return $result === true;
}
/**
* 获取锁(阻塞重试)
*/
public function acquireWithRetry($maxRetry = 10, $retryDelay = 0.2)
{
$attempt = 0;
while ($attempt < $maxRetry) {
if ($this->acquire()) {
return true;
}
$attempt++;
usleep($retryDelay * 1000000);
}
return false;
}
/**
* 释放锁(使用Lua脚本保证原子性)
*/
public function release()
{
$luaScript = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return $this->redis->eval(
$luaScript,
[$this->lockKey, $this->lockValue],
1
) == 1;
}
/**
* 获取锁值(用于调试)
*/
public function getLockValue()
{
return $this->lockValue;
}
}
// 使用示例
class CronTask
{
private $lock;
public function execute()
{
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$this->lock = new RedisDistributedLock(
$redis,
'cron:task:sync_data',
60 // 锁超时时间
);
if (!$this->lock->acquire()) {
echo "任务正在其他节点执行,跳过...\n";
return false;
}
try {
// 执行具体任务
$this->doTask();
} finally {
// 释放锁
$this->lock->release();
}
return true;
}
private function doTask()
{
// 具体的业务逻辑
echo "执行同步数据任务...\n";
sleep(5);
}
}
使用Redlock算法(高可靠性)
<?php
class RedLock
{
private $redisNodes = [];
private $quorum;
private $retryCount = 3;
private $retryDelay = 200; // 毫秒
public function __construct(array $redisNodes)
{
$this->redisNodes = $redisNodes;
$this->quorum = intval(count($redisNodes) / 2) + 1;
}
/**
* 获取锁
*/
public function lock($resource, $ttl)
{
$token = uniqid('redlock_', true);
$startTime = microtime(true) * 1000;
$successCount = 0;
$locks = [];
// 尝试在所有节点获取锁
foreach ($this->redisNodes as $node) {
$result = $this->tryLockNode($node, $resource, $token, $ttl);
if ($result) {
$successCount++;
$locks[] = $node;
}
}
// 计算总耗时
$elapsedTime = microtime(true) * 1000 - $startTime;
// 检查是否获取到大多数节点的锁且耗时小于TTL
if ($successCount >= $this->quorum && $elapsedTime < $ttl) {
$this->locks = $locks;
$this->resource = $resource;
$this->token = $token;
return true;
}
// 获取失败,释放已获取的锁
$this->unlock($resource, $token);
return false;
}
/**
* 释放锁
*/
public function unlock($resource, $token)
{
foreach ($this->redisNodes as $node) {
$this->unlockNode($node, $resource, $token);
}
}
private function tryLockNode($node, $resource, $token, $ttl)
{
try {
$redis = new Redis();
$redis->connect($node['host'], $node['port']);
return $redis->set(
$resource,
$token,
['NX', 'PX' => $ttl]
);
} catch (Exception $e) {
return false;
}
}
private function unlockNode($node, $resource, $token)
{
$luaScript = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
try {
$redis = new Redis();
$redis->connect($node['host'], $node['port']);
$redis->eval($luaScript, [$resource, $token], 1);
} catch (Exception $e) {
// 记录日志
}
}
}
数据库分布式锁
<?php
class DatabaseDistributedLock
{
private $pdo;
private $lockTable = 'cron_locks';
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->initTable();
}
/**
* 初始化锁表
*/
private function initTable()
{
$sql = "CREATE TABLE IF NOT EXISTS {$this->lockTable} (
lock_key VARCHAR(255) PRIMARY KEY,
lock_token VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)";
$this->pdo->exec($sql);
}
/**
* 获取锁
*/
public function acquire($lockKey, $expireSeconds = 60)
{
$token = uniqid('db_lock_', true);
$expiresAt = date('Y-m-d H:i:s', time() + $expireSeconds);
try {
// 尝试插入锁记录
$stmt = $this->pdo->prepare(
"INSERT INTO {$this->lockTable} (lock_key, lock_token, expires_at)
VALUES (:key, :token, :expires)"
);
$stmt->execute([
':key' => $lockKey,
':token' => $token,
':expires' => $expiresAt
]);
return $token;
} catch (PDOException $e) {
// 锁已存在,检查是否过期
if ($e->getCode() == '23000') { // 唯一约束冲突
return $this->handleExistingLock($lockKey, $expireSeconds);
}
throw $e;
}
}
/**
* 处理已存在的锁
*/
private function handleExistingLock($lockKey, $expireSeconds)
{
// 尝试删除过期锁
$stmt = $this->pdo->prepare(
"DELETE FROM {$this->lockTable}
WHERE lock_key = :key AND expires_at < NOW()"
);
$stmt->execute([':key' => $lockKey]);
// 再次尝试获取锁
return $this->acquire($lockKey, $expireSeconds);
}
/**
* 释放锁
*/
public function release($lockKey, $token)
{
$stmt = $this->pdo->prepare(
"DELETE FROM {$this->lockTable}
WHERE lock_key = :key AND lock_token = :token"
);
$stmt->execute([
':key' => $lockKey,
':token' => $token
]);
return $stmt->rowCount() > 0;
}
}
基于文件锁(单机方案)
<?php
class FileDistributedLock
{
private $lockDir;
private $lockFile;
public function __construct($lockName, $lockDir = '/tmp/cron_locks')
{
$this->lockDir = $lockDir;
$this->lockFile = $lockDir . '/' . md5($lockName) . '.lock';
if (!is_dir($lockDir)) {
mkdir($lockDir, 0777, true);
}
}
/**
* 获取锁
*/
public function acquire($timeout = 10)
{
$startTime = time();
while (true) {
// 尝试创建锁文件
$handle = @fopen($this->lockFile, 'x');
if ($handle) {
fwrite($handle, json_encode([
'pid' => getmypid(),
'time' => date('Y-m-d H:i:s'),
'host' => gethostname()
]));
fclose($handle);
return true;
}
// 检查锁是否过期
if (file_exists($this->lockFile)) {
$fileTime = filemtime($this->lockFile);
if (time() - $fileTime > $timeout) {
// 锁过期,强制移除
@unlink($this->lockFile);
continue;
}
}
// 等待重试
if (time() - $startTime > $timeout) {
return false;
}
usleep(500000); // 0.5秒
}
}
/**
* 释放锁
*/
public function release()
{
if (file_exists($this->lockFile)) {
@unlink($this->lockFile);
return true;
}
return false;
}
}
完整的生产级实现
<?php
/**
* 生产级定时任务管理器
*/
class CronTaskManager
{
private $lock;
private $taskName;
private $config;
private $logger;
public function __construct($taskName, $config = [])
{
$this->taskName = $taskName;
$this->config = array_merge([
'lock_type' => 'redis', // redis/database/file
'lock_expire' => 300, // 锁过期时间(秒)
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'password' => null,
'db' => 0
],
'database' => [
'dsn' => 'mysql:host=localhost;dbname=test',
'username' => 'root',
'password' => 'root'
],
'log_path' => '/var/log/cron_tasks/'
], $config);
$this->initLock();
$this->initLogger();
}
/**
* 初始化锁
*/
private function initLock()
{
$lockKey = "cron:task:{$this->taskName}";
switch ($this->config['lock_type']) {
case 'redis':
$redis = new Redis();
$redis->connect(
$this->config['redis']['host'],
$this->config['redis']['port']
);
if ($this->config['redis']['password']) {
$redis->auth($this->config['redis']['password']);
}
$redis->select($this->config['redis']['db']);
$this->lock = new RedisDistributedLock(
$redis,
$lockKey,
$this->config['lock_expire']
);
break;
case 'database':
$pdo = new PDO(
$this->config['database']['dsn'],
$this->config['database']['username'],
$this->config['database']['password']
);
$this->lock = new DatabaseDistributedLock($pdo);
break;
case 'file':
$this->lock = new FileDistributedLock(
$this->taskName,
$this->config['log_path'] . 'locks/'
);
break;
}
}
/**
* 初始化日志
*/
private function initLogger()
{
$logDir = $this->config['log_path'] . date('Y/m');
if (!is_dir($logDir)) {
mkdir($logDir, 0777, true);
}
$this->logger = $logDir . '/' . date('d') . '.log';
}
/**
* 记录日志
*/
private function log($message, $type = 'INFO')
{
$logMessage = sprintf(
"[%s] [%s] [%s] %s\n",
date('Y-m-d H:i:s'),
$type,
$this->taskName,
$message
);
error_log($logMessage, 3, $this->logger);
}
/**
* 执行任务
*/
public function run(callable $task)
{
if (!$this->acquireLock()) {
$this->log("任务被拒绝,已有其他节点在执行");
return false;
}
$startTime = microtime(true);
$this->log("任务开始执行");
try {
// 执行任务
$result = $task();
$execTime = round(microtime(true) - $startTime, 3);
$this->log("任务执行成功,耗时: {$execTime}秒", 'SUCCESS');
return $result;
} catch (Exception $e) {
$this->log("任务执行失败: " . $e->getMessage() .
"\n" . $e->getTraceAsString(), 'ERROR');
return false;
} finally {
// 确保释放锁
$this->releaseLock();
}
}
/**
* 获取锁
*/
private function acquireLock()
{
try {
if ($this->lock instanceof RedisDistributedLock) {
return $this->lock->acquireWithRetry(5, 0.2);
} elseif ($this->lock instanceof DatabaseDistributedLock) {
return $this->lock->acquire(
"cron:task:{$this->taskName}",
$this->config['lock_expire']
) ? true : false;
} elseif ($this->lock instanceof FileDistributedLock) {
return $this->lock->acquire($this->config['lock_expire']);
}
return false;
} catch (Exception $e) {
$this->log("获取锁失败: " . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* 释放锁
*/
private function releaseLock()
{
try {
if ($this->lock instanceof RedisDistributedLock) {
return $this->lock->release();
} elseif ($this->lock instanceof DatabaseDistributedLock) {
return $this->lock->release(
"cron:task:{$this->taskName}",
$this->lock->getLockToken()
);
} elseif ($this->lock instanceof FileDistributedLock) {
return $this->lock->release();
}
} catch (Exception $e) {
$this->log("释放锁失败: " . $e->getMessage(), 'ERROR');
}
return false;
}
}
// 使用示例
$taskManager = new CronTaskManager('sync_user_data', [
'lock_type' => 'redis',
'lock_expire' => 300,
'redis' => [
'host' => '127.0.0.1',
'port' => 6379
]
]);
$result = $taskManager->run(function() {
// 执行具体的定时任务逻辑
$userService = new UserService();
return $userService->syncUsers();
});
最佳实践建议
锁的续期处理
class RenewLockWorker
{
private $redis;
private $lockKey;
private $lockValue;
private $ttl;
private $renewInterval;
public function start($lockKey, $lockValue, $ttl = 60, $renewInterval = 30)
{
// 启动守护线程定时续期
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->lockKey = $lockKey;
$this->lockValue = $lockValue;
$this->ttl = $ttl;
$this->renewInterval = $renewInterval;
$this->renewLoop();
}
private function renewLoop()
{
while (true) {
usleep($this->renewInterval * 1000000);
$luaScript = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
LUA;
$this->redis->eval(
$luaScript,
[$this->lockKey, $this->lockValue, $this->ttl * 1000],
1
);
// 检查是否需要停止
if (pcntl_signal_dispatch()) {
break;
}
}
}
}
锁的监控和告警
class LockMonitor
{
private $redis;
public function checkLockHealth($lockKey)
{
$lockInfo = $this->redis->get($lockKey);
if (!$lockInfo) {
$this->sendAlert("锁不存在: {$lockKey}");
return false;
}
$ttl = $this->redis->ttl($lockKey);
if ($ttl < 10) {
$this->sendAlert("锁即将过期: {$lockKey}, TTL: {$ttl}秒");
}
return true;
}
private function sendAlert($message)
{
// 发送邮件、短信或企业微信通知
$alertService = new AlertService();
$alertService->send($message);
}
}
任务执行记录
class TaskExecutionLog
{
private $pdo;
public function logStart($taskName, $host, $pid)
{
$stmt = $this->pdo->prepare(
"INSERT INTO task_executions
(task_name, host, pid, status, start_time)
VALUES (?, ?, ?, 'running', NOW())"
);
$stmt->execute([$taskName, $host, $pid]);
return $this->pdo->lastInsertId();
}
public function logEnd($executionId, $status, $message = '')
{
$stmt = $this->pdo->prepare(
"UPDATE task_executions
SET status = ?, message = ?, end_time = NOW()
WHERE id = ?"
);
return $stmt->execute([$status, $message, $executionId]);
}
}
注意事项
- 锁超时设置:根据任务执行时间合理设置,避免任务未完成锁就过期
- 原子性保证:获取锁和释放锁的操作必须是原子性的
- 异常处理:确保在任务异常时也能正确释放锁
- 监控告警:及时发现锁的异常情况
- 兼容性:考虑各种分布式环境的兼容性
这套方案可以根据具体需求选择合适的实现方式,建议生产环境使用Redis方案,并配合完善的任务监控机制。