滚动新闻 →
进化骗局(33) 南猿露西 造假至极 SaaS 定时任务加锁有哪些方法?MySQL 锁和文件锁怎么选择 笔记本电脑充电器发热严重,怎样判断属于正常发热还是故障 中共五中全会前官场震荡加剧 数十名中央委员去向引发关注 中国教育“卷”到学生家长:早7点就去学校陪读 Windows 11 管理员权限是什么?安装和修改系统时经常会遇到 MySQL TEXT 和 VARCHAR 有什么区别?存储文章内容时应该如何选择 欧阳修为什么不仅是一位文学家 陈皮与岭南饮食文化 印尼种植园惊现疑似中国火箭残骸 铝板坠落引关注 英国派遣台风战机支援沙特防御 胡塞袭击致也门军营伤亡激增 临摹古人书法究竟应该临什么 美国30所高校完成安全审查 80余项涉外科研合作被叫停 美国国会官员警告台海局势因海警船行动升级 赵薇五年后首度公开亮相 新加坡发型秀引关注 泽连斯基在挪威与美欧代表会晤,探讨对乌援助方案 如何教育孩子学会与别人合作 为什么有些视频文件特别大 川普中期选举冲刺:4亿美元投入拉票 7州7日行程 纽约提前投票启动 州检察长警示AI假讯息 台湾国安局:网络攻击激增 不排除中共介入选举 靖康之变究竟改变了什么 法国暴动背后54组织牵涉左翼势力 旧房厨房太小应该优先改哪里 北方面食文化为什么如此丰富 电竞选手遭裸检引争议 京城大师赛主办方致歉 在加拿大奥肯那根湖畔,葡萄成熟的季节刚刚好 新能源汽车为什么需要轻量化 国家数据集团成立引争议,舆论质疑垄断风险 美国房贷之外还需要考虑哪些长期住房支出 闲聊研究揭示其潜在心理益处 为什么小额长期支出也会影响家庭财富 游艇的折旧速度通常受到哪些因素影响 纽约多家华人成人日间护理中心遭联邦调查 欺诈举报奖励计划同步启动 一个演员的报价究竟是谁决定的 当一座城市成为成功的尺度 加拿大修改报税需要多长时间 古迹遭焚 法国女子痛斥:移民问题在毁掉我们国家 湖北男子创新招领方式,用身份证数字解谜寻主 美国成文宪法为何如此重要

SaaS 定时任务加锁有哪些方法?MySQL 锁和文件锁怎么选择

发布时间: 2026-10-20 23:24:01    最后更新: 2026-10-20 23:58:44    阅读:7  约10 分钟阅读     

SaaS 定时任务加锁有哪些方法?MySQL 锁和文件锁怎么选择
图片说明:示意图   图片来源:Public Domain(公有领域)
SaaS系统中的定时任务经常遇到一个看似简单、实际上很容易出问题的场景:Cron每分钟启动一次任务,而上一轮任务可能还没有结束;如果系统部署了多个PHP、Node.js或Java实例,同一个任务甚至可能被多个服务器同时执行。

因此,所谓“定时任务加锁”,实际上包含两个不同问题。一个是防止同一个调度任务被多个进程同时启动,另一个是防止多个Worker同时领取同一条业务任务。前者可以使用文件锁、数据库命名锁或Redis等机制,后者通常应该直接利用数据库中的任务状态和原子更新解决。把所有问题都理解成“加一个全局锁”,往往反而会降低系统并发能力。

对于单服务器上的PHP Cron,如果目的只是防止同一个脚本重复运行,flock()往往是最简单的方法。

$fp = fopen('/var/lock/myapp-task.lock', 'c');

if (!$fp) {
exit("Cannot open lock file\n");
}

if (!flock($fp, LOCK_EX | LOCK_NB)) {
exit("Task is already running\n");
}

try {
runTask();
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}

这里真正起作用的是操作系统提供的文件锁,而不是锁文件本身。只要多个进程能够访问同一个文件系统上的同一个锁文件,就可以进行进程之间的协调。

因此,文件锁非常适合单台服务器上的Cron防重入。例如服务器每分钟执行一次PHP脚本,如果上一轮还没有结束,下一轮直接退出,可以避免出现多个相同任务同时运行的问题。

但文件锁有一个非常重要的限制:它依赖共享的文件系统。

如果SaaS系统部署在三台服务器上,服务器A上的 /var/lock/task.lock 和服务器B上的 /var/lock/task.lock 通常是两个完全不同的文件。A获得的文件锁,B根本看不到。因此,flock()不能直接作为普通多服务器SaaS环境的分布式锁。

如果多个Worker需要协调领取数据库任务,通常没有必要首先创建一个“全局锁表”。更好的设计是利用任务表本身进行原子状态转换。

例如任务表可以包含:

id
status
locked_by
locked_at
attempts

Worker领取任务时,不应该简单执行:

SELECT id FROM tasks
WHERE status = 'pending'
LIMIT 1;

然后再执行:

UPDATE tasks
SET status = 'running'
WHERE id = ?;

因为两个Worker完全可能同时读取到同一个pending任务。

更可靠的方式是让数据库参与任务领取过程,通过事务锁或者数据库支持的原子更新机制,让“发现任务”和“占有任务”成为一个受控制的操作。

在MySQL 8.0环境下,对于多个Worker并发领取任务,可以考虑:

SELECT id
FROM tasks
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;

然后在同一个事务中把任务状态修改为running:

UPDATE tasks
SET status = 'running',
locked_by = ?,
locked_at = NOW()
WHERE id = ?;

最后提交事务。

SKIP LOCKED的价值在于,当一个Worker已经锁住某条任务记录时,其他Worker可以跳过正在被锁定的记录,而不是全部排队等待同一条任务。这种模式特别适合任务队列型SaaS系统。

不过需要特别注意,SELECT ... FOR UPDATE取得的锁属于事务锁。事务提交或回滚后,数据库锁就会释放。因此,不应该拿着数据库行锁执行一个可能持续几分钟甚至几十分钟的业务任务。

正确的思路通常是:短事务负责“领取任务”,领取完成后立即提交;真正耗时的业务处理在事务之外执行。

这也是“数据库锁”和“业务任务状态”应该分开的原因。数据库锁负责防止两个Worker在领取瞬间发生竞争,而status = running、locked_by和locked_at等字段负责记录任务已经被哪个Worker领取。

如果Worker在执行过程中突然崩溃,还可以利用locked_at判断任务是否已经超时。例如一个任务长时间保持running状态,就可以进入恢复或重试流程。

这种设计比单纯设置一个“5分钟全局锁”更加可靠,因为不同任务可以并行执行,而不是整个任务系统只有一个Worker能够工作。

如果确实需要一个跨服务器的全局互斥锁,可以考虑MySQL命名锁。例如:

SELECT GET_LOCK('saas_cron:daily_report', 10);

成功取得锁后执行任务,最后释放:

SELECT RELEASE_LOCK('saas_cron:daily_report');

MySQL命名锁与InnoDB行锁不是一回事。它不需要创建专门的锁表,因此对于某些简单的“同一个Cron只能有一个实例运行”的场景非常方便。

但MySQL命名锁也有自己的边界。它依赖MySQL连接和数据库服务,如果任务执行时间很长,数据库连接本身也需要保持;如果系统已经处于数据库压力很大的状态,用数据库承担大量跨服务协调工作也未必合适。

Redis则适合另外一种场景:SaaS已经拥有Redis基础设施,并且需要在多个应用实例之间进行快速协调。

基本的加锁操作不能写成简单的:

$redis->setex('task_lock', 300, 'locked');

然后再判断:

$redis->get('task_lock');

因为这不是可靠的“只有一个客户端可以成功获得锁”的原子操作。

更基本的模式应该使用条件写入,例如:

$token = bin2hex(random_bytes(16));

$ok = $redis->set(
'saas:lock:daily_report',
$token,
['NX', 'EX' => 300]
);

if (!$ok) {
exit("Task is already running\n");
}

NX意味着只有当Key不存在时才创建,EX则设置过期时间。这样可以避免多个Worker同时成功创建同一个锁。

但锁的释放同样不能简单写成:

$redis->del('saas:lock:daily_report');

原因是锁可能已经过期,然后被另一个Worker重新取得。如果第一个Worker此时才执行DEL,就可能把第二个Worker刚刚取得的新锁删除。

因此,实际系统通常会把唯一token写入锁值,并通过Lua脚本进行“只有锁的拥有者才能删除”的原子判断。

此外,如果任务可能运行超过锁的TTL,还需要考虑锁续租问题。否则锁过期以后,第二个Worker可能重新获得锁,而第一个Worker实际上还在执行。最终就会出现两个Worker同时执行同一个业务任务。

这也是为什么“分布式锁”从来不只是写一条Redis SET命令那么简单。

从实际SaaS架构来看,文件锁、MySQL事务锁、MySQL命名锁和Redis锁解决的是不同层面的问题。

如果只是单服务器Cron防止脚本重复启动,flock()通常已经足够,而且实现简单、故障面小。

如果多个Worker需要从MySQL任务表中领取不同任务,优先考虑数据库事务、行锁以及SKIP LOCKED等机制,让任务领取动作直接建立在业务数据之上。

如果需要跨服务器阻止某个特定任务同时运行,而且系统本身大量使用MySQL,可以考虑MySQL命名锁或基于唯一索引的原子状态转换。

如果SaaS已经使用Redis,并且需要大量跨实例协调,则可以采用带TTL和唯一token的Redis锁,但应该正确处理过期、续租、释放和故障问题。

还有一种经常被忽视的方案:对于真正复杂的SaaS任务系统,与其自己设计越来越复杂的锁机制,不如使用消息队列、任务队列或者专门的调度系统。让任务进入队列,再由Worker领取和确认,可以把重试、失败恢复、并发限制和任务状态管理从Cron脚本中分离出来。

例如Cron每分钟只负责触发调度:

Cron
↓
任务调度器
↓
Queue
↓
Worker 1
Worker 2
Worker 3
Worker 4

这样Cron不需要自己长期持有锁,也不需要扫描整张任务表寻找变化。对于规模较大的SaaS系统,这种架构通常比“每分钟扫描数据库然后给任务加一个大锁”更加容易扩展。

最终需要避免的,是把“锁”当成万能解决方案。锁只能解决资源竞争的一部分问题,不能替代幂等设计、任务状态机、失败重试和故障恢复。

对于一个真正可靠的SaaS定时任务系统,更合理的设计通常是:Cron负责触发,数据库负责保存任务状态,短事务负责原子领取任务,Worker负责实际执行,任务本身保持幂等,超时任务进入恢复机制,必要时再使用Redis或其他协调服务处理跨实例互斥。

这样即使服务器突然宕机、Worker被杀死、Cron重复启动或者多个实例同时抢任务,系统也不会因为一个简单的锁失效而把同一笔业务执行两遍。

喜欢这篇报道?

使用下面的功能,方便以后继续阅读和分享 MNewsTV

设为 Google 新闻首选来源 让 Google 新闻优先显示 MNewsTV 的最新报道 ›
★ 我的收藏 查看已经收藏的文章
关于文章收藏 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。
分享这篇报道

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP