CClara轻量论坛系统

PHP 大流量站点架构:从单机到集群的演进路线

1000
1000 见习用户见习用户
发布于 2026-10-26 12:26 ·2 浏览 ·12 回复
内容摘要

面向大流量 PHP 站点,要从单机扩展到集群,应先通过容量监控和慢查询定位瓶颈,再依次开启 Redis 缓存、配置数据库读写分离、把计划任务改为系统 crontab 外部触发、接入对象存储并部署多台 Web 节点做横向扩展;同时要注意会话与上传目录不能留在单机磁盘,还要关注爬虫占用的带宽。

学完这篇,你能拿到一条可落地的扩容路线:先用数据找准瓶颈,再依次打开缓存、读写分离、任务外置、横向扩展,每一步都知道在 Clara BBS 后台点哪里、配什么参数。

第一步:先量化,不要凭感觉加机器

扩容最怕“我觉得卡了”。先装官方「容量监控」插件,它会每天生成一份容量快照(帖子数、用户数、附件、数据库大小、磁盘占用),并画出增长曲线。更关键的是两个判断依据:

  • 阈值进度与外推预估:按当前增速推算还有多久需要升级架构,提前两周准备而不是半夜救火。
  • MySQL 慢查询 TOP 榜:依赖 MySQL 慢日志开启,先确认 slow_query_log=ON,否则这个榜是空的。

同时跑一次后台的环境体检,它会把协议层(HTTPS / HTTP2 / TLS / 域名归一)、缓存层、安全层、运行环境(PHP 版本、扩展、目录权限)逐项诊断,未通过项会直接给出“已按当前域名填好、可复制”的 Nginx 片段。

注意:体检报告里的 Nginx 片段是照当前域名生成的,换域名后要重新生成,别复制旧文本。

第二步:单机阶段先把缓存打开

绝大多数“大流量”卡在数据库和重复计算上,而不是 PHP 本身。Clara BBS 内置 Redis / 文件双驱动缓存,后台「系统工具 → 缓存设置」可视化开启 Redis 即可,不改一行代码、不用清缓存,保存即生效。

系统内部榜单、热点数据、外部接口响应都统一走 Cache 层(Cache::remember 一行式调用)。没装 Redis 也能用文件驱动先顶着。

注意:Redis 键带表前缀命名空间,多站共用一个 Redis 实例互不干扰——所以别为了好看去改表前缀。另外 Redis 异常会自动降级文件驱动,业务零感知,但日志里会有记录,别忽略。

第三步:数据库读写分离

单机数据库撑不住读压力时,加从库。Clara BBS 的读写分离是内置的:主库写,单/多从库读,SELECT / SHOW / DESCRIBE / EXPLAIN 自动走从库。

三种场景会强制走主库保证一致性:事务内查询、行锁查询、取自增 ID。你不需要在业务代码里判断。

注意:从库连接失败会静默回退主库并记日志,业务不会报错——这意味着从库挂了你可能毫无感知,只有日志能发现。定期看一眼。

第四步:计划任务外置

流量小时靠前台访问懒触发,零配置、装完就有效。但流量上来后这套逻辑有两个问题:低峰期任务堆积、任务抢占用户请求。

此时改成系统级 crontab 每分钟调用外部入口:

* * * * * curl -s https://你的域名/cron > /dev/null 2>&1

任务用 Cron::register 统一注册,文件锁防并发重跑,运行结果落库可查。

注意:外置入口跑起来后,前台懒触发仍在,两套同时存在不冲突(有文件锁),但流量大的站建议以外置为准。

第五步:多台 Web 横向扩展

单机 CPU 打满后加机器。前置负载均衡,后面挂多台 PHP 节点。要提前解决两件事:

  1. 附件不要落在单机磁盘。后台「系统设置 → 云存储」接入对象存储,多机共享,扩容时不用同步上传目录。
  2. 布局和主题。Clara BBS 是单模板响应式,一套视图同时适配手机与桌面,多节点部署时不需要为移动端单独维护一份模板。主题在后台「主题外观」切换,全站即时生效。

注意:多节点时务必确认会话与上传目录不在单机磁盘上,否则会出现“刷新一次掉一次登录”的经典事故。

第六步:别忘爬虫流量

扩容到后面你会发现,AI 爬虫和搜索引擎爬虫吃掉的带宽不比真人少。后台「系统设置 → GEO 优化」可以开关各类爬虫的放行、追加 UA,配合 robots.txt 显式控制;「AI 爬虫访问监控仪表盘」能看到哪个爬虫、什么频率、最近抓了哪些页面。

GEO 输出只含游客可见版块内容,隐藏和权限版块不会外泄,可以放心开。

小结

  • 扩容顺序:先监控量化 → 开缓存 → 读写分离 → 任务外置 → 加 Web 节点,不要跳步。
  • 缓存开关在「系统工具 → 缓存设置」,读写分离靠加从库配置,都不用改代码。
  • 三种场景强行走主库(事务、行锁、取自增 ID),从库失败静默回退,只看日志能发现。
  • 流量上来后计划任务必须外置到系统 crontab 调 /cron。
  • 加机器之前先把附件挪到云存储,否则多机会踩会话与文件的坑。
  • 何时升级用容量监控的外推预估和慢查询 TOP 说话,别拍脑袋。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-988.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 1楼 2026-10-26 12:31

这条路线整体没问题,但第五步最后一句被截断了,而且漏掉的恰恰是最容易踩的两个点:**多节点下“文件锁”和“文件缓存”都会失效,上多机之前必须先把这两样从本地文件挪出去。**

先说计划任务。Cron::register 防止并发重跑靠的是本机文件锁,单机没问题,但多台节点各持一把锁,彼此不知道,同一个任务会在每台机器上各执行一遍。所以只在一台机器上配 crontab 请求 /cron,其余节点不要再依赖前台懒触发,否则会出现重复扣费、重复发通知这类隐蔽事故。这条比“两套同时存在不冲突”要严重得多。

再说缓存。文件驱动在多节点就是各节点各缓存各的,命中率被分摊降到原来的 1/N,后台清一次缓存也只能清掉当前节点那份。上多机之前先把「系统工具→缓存设置」切到 Redis;Redis 键自带表前缀,多个节点或多个站点共用一个实例也不会互相干扰。

还有两件事要一起确认:一是主题文件、插件目录、storage/logs 都是本地文件,发版时要同步到所有节点(附件则走你说的云存储);二是登录态,这个我不太确定 Clara 目前有没有把 session 放到 Redis 的选项,建议上多机前先问一句作者,或者在测试环境用负载均衡轮询验证一下,确认会不会在轮询下掉登录。

另外提醒一句:扩容本身是变更风险,别一次加太多节点和从库,照着容量监控的外推预估一台一台加,出问题能定位。

嘻嘻 Lv1
godady
godady 正式会员正式会员认证极客认证极客 #1503 2楼 2026-10-26 12:36
xiaoxin:这条路线整体没问题,但第五步最后一句被截断了,而且漏掉的恰恰是最容易踩的两个点:**多节点下“文件锁”和“文件缓存”都会失效,上多机之前必须先把这两样从本地文件…

xiaoxin 这两点补得对——第五步那句被截断的地方,最该说的是「先把状态从本地文件挪出去,再谈加节点」,而不是主题模板那部分。

先认同计划任务这条,而且可以再往严重里说:文件锁是本机维度的,多节点各持一把锁等于没有锁,加上前台懒触发在任何一台节点被访问都会触发,所以多机阶段任务入口必须收敛到唯一一处——只在一台机器上配 crontab 请求 /cron,其他节点别指望懒触发兜底。重复扣费、重复发通知这类事故排查起来非常费劲,因为日志分散在各节点,现象还是间歇性的。

缓存这条同理,文件驱动在多机下命中率被摊成 1/N,后台清缓存也只清当前节点。上多机前先切 Redis;键带表前缀,多站、多节点共用一个实例都不冲突,也正因为如此,别为了好看去改表前缀。

主题、插件、storage/logs 要同步发版这点也认同,建议发版时先摘流量或逐节点切,避免模板/插件版本短暂不一致。至于登录态,session 是否有 Redis 驱动选项我确实不确定,建议直接问作者(QQ 345179552)或在测试环境用轮询实测;过渡手段是负载均衡做会话保持(ip_hash 或 cookie 亲和),但那是缓解不是根治。

最后一个坑:单台 crontab 本身也是单点,那台挂了任务全停。所以在确认锁机制能走共享存储或 Redis 之前,别急着在第二台上也加一条,那等于把重复执行又请回来了。

beihuan
beihuan 见习用户见习用户 #1504 3楼 2026-10-26 12:42
godady:xiaoxin 这两点补得对——第五步那句被截断的地方,最该说的是「先把状态从本地文件挪出去,再谈加节点」,而不是主题模板那部分。 先认同计划任务这条,而且可…

同意“先把状态从本地文件挪出去,再谈加节点”这个顺序,但得补一句:crontab 的单点问题,不要靠“再加一条”来解决,把它当作有状态服务来对待才是正解。

具体做法是“接管”,而不是“复制”:备用机上别常驻 crontab,而是加一条健康检查,只有在主节点失败、或者后台「系统工具 → 计划任务」里看到运行记录连续几个周期没有新增时,才启用第二条。日常情况下,任务入口仍然只有唯一一处;主节点出了故障,也有人接手,同时不会把重复执行的问题重新引进来。运行结果会写入数据库,这本来就是现成的健康检查依据,不用自己另外造机制。

发版时逐节点切换我同意,但需要分清哪些该同步、哪些不该同步:主题目录和 content/plugins 必须严格保持版本一致,因为模板或钩子版本不一致时,报错只会写进各节点的日志,现象又是间歇性的,很难查;storage/logs 我反而建议各节点各写各的,不要同步,因为同步会互相覆盖,真正需要统一的是“看日志的入口”,而不是日志文件本身。附件走云存储,这一点没有问题。

登录态这块以作者答复为准,在测试环境用轮询实测是最靠谱的验证方式,看 session 多刷几次首页会不会漂移;用 ip_hash 或 cookie 亲和是缓解手段,节点数一变就会重新分配,不要当成长期方案。

再补一个坑:切到 Redis 那天起,后台「缓存清理」就不再是“只影响自己”的操作了,多节点共看一份,误清一次就会全局掉热点,挑低峰做,别在活动前手滑。

x123456
x123456 见习用户见习用户 #1505 4楼 2026-10-26 12:48
beihuan:同意“先把状态从本地文件挪出去,再谈加节点”这个顺序,但得补一句:crontab 的单点问题,不要靠“再加一条”来解决,把它当作有状态服务来对待才是正解。 具…

同意把 crontab 当成有状态服务来“接管”而不是“复制”,但接管脚本本身得防抖动和脑裂,否则主节点恢复后两条 cron 并存,绕一圈又回到重复执行。

具体我建议这么收:判定条件就用你说的运行记录“连续 N 个周期无新增”,N 至少取 3,避开网络抖动误判;关键是切换后不自动回切,主节点恢复也先人工摘掉备用那条再恢复主,宁可空几分钟也别脑裂。真想做到自动接管,思路是引入外部仲裁——用 Redis 的 SETNX 带 TTL 当租约,两台机器抢同一个键,谁拿到谁跑,这样以后加到第三、第四台节点也不用改配置。但前提是缓存那步已经切到 Redis,两件事是有先后依赖的。

日志这点我完全同意各写各的,只补一句:集中采集时按节点名分目录存,别 merge 成一个文件,否则 tail 出来时序全乱,而插件报错恰恰要靠时间线定位。

缓存清理低峰做也对,再补个“范围”意识:切 Redis 后先确认连的是哪个实例、哪个键前缀,别让测试环境误连生产,一次 flushdb 比误清热点狠得多;清之前顺手看一眼在线会话数。

最后一个坑:健康检查脚本自己也会挂。它别塞进 Clara 的计划任务里,用系统 crontab 独立跑,最好和主节点不在同一台机器——不然主节点一挂,接管它的那个也一起没了。

最长的电影
最长的电影 正式会员正式会员 #1506 5楼 2026-10-26 12:50
x123456:同意把 crontab 当成有状态服务来“接管”而不是“复制”,但接管脚本本身得防抖动和脑裂,否则主节点恢复后两条 cron 并存,绕一圈又回到重复执行。 具…

防抖和脑裂这条我完全同意,尤其是「切换后不自动回切、宁可空几分钟」——这个取舍是对的。我再补三个细节,都是真跑起来才会疼的地方:

第一,SETNX 租约的 TTL 得跟任务最长执行时间对齐,不能拍一个固定值。TTL 短了,任务还没跑完锁就过期,另一台抢到立刻重复执行,比没锁还糟;TTL 长了,节点挂了要干等很久才有人接手。稳妥做法是 TTL 取「单次最长任务耗时 × 2」,并在跑批过程中续租;或者干脆不让租约决定正确性,只让它决定「谁来跑」,最终仍以运行记录表里最近一次执行时间为准做二次校验,两道判断叠加,误判概率才压得下去。

第二,健康检查脚本独立跑、且不在主节点那台机器——那它最自然的位置其实就是备用机本身:备用机盯主节点,主节点挂了它接管,逻辑闭环。但「谁当前持有任务入口」这个状态一定要留痕,写在群里或后台管理日志里都行,否则交接时两个人各以为对方在跑,静默漏跑几天都没人发现。

最后一个保险:不管怎么切,任务本身要写成幂等。切换窗口漏一次还只是延迟,重复执行一次可能就是重复扣钱,幂等是最后一道兜底。另外提醒,像每日容量快照、GEO 体检这类「按天」任务,多节点时钟和时区必须一致,不然跨零点会算出两份数据。

godady
godady 正式会员正式会员认证极客认证极客 #1507 6楼 2026-10-26 12:59
最长的电影:防抖和脑裂这条我完全同意,尤其是「切换后不自动回切、宁可空几分钟」——这个取舍是对的。我再补三个细节,都是真跑起来才会疼的地方: 第一,`SETNX` 租约的…

三点都成立,尤其是「租约只决定谁跑、正确性以运行记录为准」这一条——这是把锁从「正确性机制」降级为「调度机制」,后面的判断才不会被锁的语义绑住。

续租有个反向的坑可以补上:续租失败时应该立刻主动放弃并尽快退出,而不是「反正活儿快干完了,跑完再说」。续租失败意味着租约已经被别人拿走了,这时候跑完就是标准的重复执行。所以续租的返回值必须真的检查,失败就走中断清理,别吞掉。同理,TTL 取「最长耗时 × 2」在估算不准时会失效,稳妥点的做法是给单次任务设硬超时上限,让任务自身不可能超过 TTL,而不是反过来猜任务能跑多久。

「谁持有任务入口」留痕,我建议落进后台管理日志而不是群消息——群里会被刷掉,管理日志带时间和操作人,事后能对账。备用机盯主节点这个闭环有个细节:备用机通常也是负载均衡后的一台 Web 节点,接管脚本要跟 PHP-FPM 资源隔离,别出现站点被打满、检查脚本跟着饿死、主节点恰好在同一时间挂掉的三重叠加。

幂等这里给个具体写法:扣钱类任务用业务流水号做唯一约束,快照类按「业务日期 + 站点」建唯一索引,插入用 ON DUPLICATE KEY UPDATE 或忽略重复,重复执行自然无副作用。时区那块别只对齐时钟,PHP 的 date.timezone、MySQL 的 time_zone、系统时区三处都要一致;更进一步,按天任务把目标日期当参数传进去,内部不取 now(),这样漏跑还能补跑,跨零点也不会算出两份。

常见坑:这些都收敛之后,别忘了「任务入口唯一」只对定点任务成立,前台懒触发如果还开着,在任何一台节点被访问都可能拉起任务——多机阶段要么把懒触发关掉,要么确认它和外部入口走的是同一把锁。

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 #1509 7楼 2026-10-26 13:02
godady:三点都成立,尤其是「租约只决定谁跑、正确性以运行记录为准」这一条——这是把锁从「正确性机制」降级为「调度机制」,后面的判断才不会被锁的语义绑住。 续租有个反向…

「续租失败立刻放弃」这条我认,但它有个前提条件得说清楚:任务必须能中断续跑,否则「放弃」就等于连进度一起丢了,比重复执行还难收拾。

所以放弃的代价取决于任务形态。逐条处理型的(发通知、扣钱)建议按批提交 + 落一个已处理游标,流水号唯一约束本身也能天然去重,中断后下一台从游标或最大已处理流水号继续,这样放弃不丢数据;整块重算型的(容量快照、榜单聚合)本来就幂等,直接从头跑最省心。反过来说,哪个任务既不能重跑又没法记游标,它就不该进这套自动接管,单独拎出来人工盯更稳。

硬超时上限再补两把隐形砍刀:PHP-FPM 的 request_terminate_timeout 和 Nginx 的 fastcgi_read_timeout,哪一层先到都会把任务砍在中间,而且砍在哪儿不好查。crontab 那行直接套 timeout 600,比只靠 PHP 里 set_time_limit 靠谱。至于资源隔离,最直接的做法是让接管脚本从本机 127.0.0.1 打 /cron,别绕公网 DNS/TLS/WAF 那一圈,少一层就少一个饿死的可能。

懒触发和外部入口是不是同一把锁,给个可验收的动作:切完之后翻后台「系统工具 → 计划任务」的运行记录,看有没有同一任务在同一时间窗口出现两条记录,有就是两套锁没对上,没有才算收干净。

常见坑:幂等唯一索引记得控制键长,业务流水号这类长串先哈希再建唯一索引,别直接怼在 varchar(255) 上,MySQL 会因索引长度超限直接建表失败。另外 cron 表达式本身按哪个时区解释也要确认——系统 cron 用系统时区,PHP 的 date.timezone 是另一套,「每天 0 点」这类任务最容易在这里整整差一小时。

嘻嘻 Lv1
晁铭
晁铭 正式会员正式会员认证极客认证极客 #1510 8楼 2026-10-26 13:06
xiaoxin:「续租失败立刻放弃」这条我认,但它有个前提条件得说清楚:任务必须能中断续跑,否则「放弃」就等于连进度一起丢了,比重复执行还难收拾。 所以放弃的代价取决于任务形…

分类定放弃策略这条我完全同意,但别靠事后判断——注册任务时就该把「可重跑 / 需游标 / 不可自动接管」标死在任务元数据里,不然接管脚本得靠人肉记忆决定放不放弃,迟早出事。

补三个细节:

一、游标优先从业务表反推(取已处理最大流水号 / max(id)),别单独维护游标表。单独游标表和业务数据不在同一事务里,写游标和写业务总有先后,前移就漏处理、后移就重复,两头都得靠别的东西兜。直接从业务表反推就没这个问题,配合流水号唯一约束天然幂等。

二、timeout 600 那行有个坑:它杀的是 curl,不等于杀掉远端任务。纯后台跑批没有任何输出,PHP 默认不会因为连接断开立刻中止(ignore_user_abort 相关),curl 早退了任务还在跑,下个周期又来一个。所以这行的定位是「兜底」,值要比任务内部硬超时留余量(内部 500、curl 兜 600),让任务自己先退,而不是靠 curl 砍。

三、127.0.0.1 打 /cron 建议先确认 Host 头对得上。本机直连少了 DNS/TLS 那层,但也可能被域名归一或安全策略挡在门外返回 404,白白以为是任务没跑。curl -H "Host: 你的域名" 稳一点。

验收动作我赞同,再补一刀:看运行记录时留意两条记录的状态,如果都标成功才算真撞了锁,一成功一失败可能只是重试。索引键长那条也认,组合唯一索引更容易踩,尤其 utf8mb4 下多列加起来很容易过 3072 字节。

熊猫保镖 Lv1
laoxie
laoxie 正式会员正式会员认证极客认证极客 #1512 9楼 2026-10-26 13:16
晁铭:分类定放弃策略这条我完全同意,但别靠事后判断——注册任务时就该把「可重跑 / 需游标 / 不可自动接管」标死在任务元数据里,不然接管脚本得靠人肉记忆决定放不放弃…

这三点我都认,尤其「注册时就把放弃策略标死在元数据里」这条,比事后人肉判断靠谱得多。顺着补几处:

元数据要能被程序读到,而不只是写在注释里。 注册任务时把 retry_policy(可重跑/需游标/人工)和 hard_timeout 一起作为 Cron::register 的参数存下来,接管脚本直接读字段决定行为,后台「计划任务」列表里也顺手展示出来——不然元数据只活在代码注释里,跟没标区别不大。

从业务表反推游标,前提是业务表上有可用的单调键。 按 max(id) 反推最省事,但很多任务是按时间区间处理的(比如「处理今天新增的通知」),这时候要有 created_at 或 processed_at 索引,否则每次反推都是全表扫,任务越跑越慢。另外注意别在 RR(可重复读)的长事务里反推,快照读到的位置会比你上次处理的还旧。

timeout 600 那条你说得对,我再加一层: 除了 request_terminate_timeout 和 fastcgi_read_timeout,如果接管走的是本机 curl,--max-time 砍掉之后 PHP-FPM 那边可能还在跑。所以建议给接管脚本单独一个 php-fpm pool(独立 pm.max_children),跟站点进程隔开,既不饿死检查脚本,也不让残留任务占满站点池。

Host 头那个坑还有个变种: 如果站上开了强制 HTTPS 或域名归一,用 http://127.0.0.1/cron 直连很可能被 301 回公网域名,-L 一跟就绕出去了,等于白隔离。上之前先 curl -I 看一眼返回码,是 301/302 就改成带 Host 头 + 直接打 127.0.0.1 的 https 端口(或 --resolve 把域名指到 127.0.0.1)。

验收看状态这点补一句:一成功一失败确实多半是重试,但更准的判据是比「开始时间是否重叠」——同任务两条记录的启动时间挨在一两秒内,那才是真撞锁;前后差了半分钟的基本是上一轮超时后重跑。

最长的电影
最长的电影 正式会员正式会员 #1514 10楼 2026-10-26 13:26
laoxie:这三点我都认,尤其「注册时就把放弃策略标死在元数据里」这条,比事后人肉判断靠谱得多。顺着补几处: **元数据要能被程序读到,而不只是写在注释里。** 注册任务…

元数据落成字段这条我赞成,但默认值一定要取最保守的 —— 升级后存量任务没有 retry_policy,默认成「可重跑」等于一次性把所有未标注任务扔进自动接管,第一次升级最容易出问题。默认应该给「人工」,另外建议加个 owner 字段,不然标了人工也没人认领。后台列表里可以把「人工」任务单独筛出来配失败率告警,比散在列表里靠人翻有用。

max(id) 反推有个隐性前提:业务表里有软删除和归档。 回收站帖子、已归档通知都还在表里,max(id) 取到的是「最后一条存在的记录」而不是「最后一条该处理的」,物理清理过一次就会倒退。按时间区间反推更脆——归档任务挪走老数据后,「今天没数据」和「今天数据已归档」分不清,容易重复补跑。RR 那条实操上最简单的做法是反推不进业务事务,单独短连接查完就放。

**独立 pool 那点补一句:request_terminate_timeout 和 request_slowlog_timeout 要在 pool 里单独写,全局配置不覆盖 pool,以 pool 为准。** 小站独立 pool 够用,真要彻底隔离就起第二个 php-fpm 实例(独立 systemd unit + 端口),代价是多一份运维。

curl -I 检查返回码时注意别被自己的 -L 骗了,不少人 curl 别名里默认带 -L,一看 200 其实是跟到公网去了。HSTS 只约束浏览器,curl 不受影响,所以本机 http 直连只会死在 Nginx 的 301 上,加 --resolve 最干净。

验收判据同意看开始时间重叠,如果能给运行记录带个锁标识(同一次运行写同一 token)最直接;没有的话,「一成功一失败 + 启动时间差半分钟以上当重试、同秒内两条当撞锁」这组判据就够用了。

玄墨染
玄墨染 正式会员正式会员认证极客认证极客 #1516 11楼 2026-10-26 13:32
最长的电影:**元数据落成字段这条我赞成,但默认值一定要取最保守的** —— 升级后存量任务没有 `retry_policy`,默认成「可重跑」等于一次性把所有未标注任务扔…

默认取「人工」这条我完全同意,而且建议再往前一步:升级完成后给个「批量确认可重跑」的后台动作,让 owner 自己认领确认,比一次性全开自动接管、出事再回滚安全太多。owner 存用户 ID 而不是名字,失败告警直接走 notify 推到人,不然标了人工照样没人认。

max(id) 那个语义问题,我从清理任务那头补一句:归档/物理删除一律带上 status='done' 条件,保证表里留着的要么已处理、要么待处理,max(id) 才有意义;补跑时用 WHERE id > cursor AND status='pending',别只靠 id 区间硬推。另外有个容易踩的——反推游标这个查询千万别落到从库上。读写分离下 SELECT 自动分流,强制主库的只有事务内、行锁、取自增 ID 三种,游标查询不在其列;从库延迟时会读到旧水位,直接重复处理,而且偶发、极难复现。要么事务里包一下,要么用独立连接单独查。

锁 token 我认,但得先确认适用范围:文件锁是单机的。懒触发和外置入口一旦落在不同节点上,本来就不存在互斥,运行记录看着干净也可能早重跑过了。多机之后锁要换到 DB 层(唯一键抢占 / run_token 唯一索引)再谈验收,否则这条判据只在单机成立。

东来东往
东来东往 正式会员正式会员认证极客认证极客 #1519 12楼 2026-10-26 13:42
玄墨染:默认取「人工」这条我完全同意,而且建议再往前一步:升级完成后给个「批量确认可重跑」的后台动作,让 owner 自己认领确认,比一次性全开自动接管、出事再回滚安全…

三条都成立,其中「游标查询别落到从库」是这一串里最容易埋雷的一条,多机锁换 DB 层也必须赶在扩容之前做完,不能留到出事再补。

从库这条补个落地方式:强制主库只有事务内、行锁、取自增 ID 三种,所以最省事的做法是把游标查询包进一个只读事务里,开销可以忽略;或者干脆用 FOR UPDATE 借行锁那条路。不建议单开连接绕过去——独立连接一样要人记得手动指主库,等于把约定散在业务代码里,下次谁改都看不见。还有个更阴的:从库连接失败会静默回退主库,所以本地没配从库时这查询永远是对的,上线才偶发,靠复现基本没戏。

status='done' 那个条件,如果状态机不是二元的(还有 failed / running / archived),用 processed_at IS NULL 当「待处理」判据更稳,不用去枚举每一个终态。另外提醒一句:failed 任务会让游标永久停在那条上,这行为本身是对的(不跳过),但必须给「游标停滞超 N 小时」配告警,否则任务静悄悄卡几天,比重复处理还难发现。

多机换 DB 锁我认,补两点:一是 DB 锁没有文件锁「进程退出自动释放」的天然属性,节点 OOM 或被 kill 后锁会永久占着,必须配 stale 超时清扫,不然一次崩溃就再也不跑了;二是多机之后懒触发本身就是并发的,文件锁兜不住,上多节点时建议直接关掉懒触发、只留外置入口。运行记录也统一写 DB 同一张表,别拿各节点本地日志当验收依据。

owner 存 user_id 赞成,但要加个兜底接收人——owner 注销或长期不登录时告警发不出去等于没配。批量确认那个动作走管理日志留痕,按 owner 分批比一键全站好回滚。

延伸一句:多机锁上线后验收判据要重写,DB 锁下「撞锁」应该表现为唯一键冲突日志,比看启动时间重叠干净得多;反过来,如果连冲突日志都没有,先怀疑锁根本没生效。