队列延迟推送(如Redis ZSET)才是可靠选择;扫表在高并发、大数据量下易卡死,而Redis延迟队列通过时间戳score实现精确触发,需注意时区、过期处理、QPS压测及持久化兜底。

ThinkPHP定时发布靠扫表还是队列?先说结论
扫表(Crontab + 查询数据库)是简单项目能用的底线方案,但并发高、文章量大时会卡死;队列延迟推送(如 Redis Delayed Queue + think-worker)才是可靠选择——不是“更高级”,而是避免 status 字段被大量未发布文章拖垮查询性能。
为什么 crontab 扫 article 表容易出问题
常见错误现象:凌晨三点服务器负载飙升,SELECT * FROM article WHERE status = 0 AND publish_time 慢查询暴增,甚至锁表。
- 没加复合索引:
INDEX(status, publish_time)必须有,否则全表扫描 - 扫表频率不合理:每分钟扫一次,但实际只需秒级精度,反而增加 DB 压力
- 没做分页或 LIMIT:一次更新几百条,事务长、锁时间久,影响写入
- 没处理异常中断:脚本崩了,漏掉的发布时间就永远卡住,没人发现
示例命令别直接写死路径:*/5 * * * * /usr/bin/php /var/www/thinkphp/artisan schedule:run >> /dev/null 2>&1,必须配合 think:schedule 和自定义 Command,且该 Command 内部要用 chunkById 分批更新,不能 get() 全量。
用 Redis 实现真正延迟发布的实操要点
ThinkPHP 8.0+ 推荐走 think-queue + Redis 的 ZSET 延迟队列,本质是把「发布时间」转成 UNIX 时间戳作为 score 插入有序集合。
立即学习“PHP免费学习笔记(深入)”;
- 发布文章时,不改
status,而是往zadd delayed_publish 1717027200 article_123插入(1717027200 是发布时间戳) - 独立守护进程(如
php think queue:work --delay=1)每秒执行一次zrangebyscore delayed_publish -inf [now_timestamp] WITHSCORES LIMIT 0 100 - 取出后立刻
zrem,再异步调用ArticleService::publish($id),失败进重试队列,不阻塞后续 - 注意 Redis 连接复用:别在每次 job 里 new Redis 实例,用容器绑定的单例
别依赖 delay 参数走 AMQP 或数据库驱动——它们不支持精确到秒的延迟,Redis ZSET 是目前最稳的。
上线前必须验证的三个边界场景
很多项目上线后才发现:定时发布在某些条件下根本不动。
- 服务器时区和 PHP 时区不一致:
date_default_timezone_set('Asia/Shanghai')必须显式设,不能靠系统 - 文章发布时间早于当前时间但
status还是草稿:说明扫表逻辑或队列消费逻辑漏掉了“过期立即触发”分支 - 同一时间大量文章到期:Redis 队列消费者扛不住,得提前压测
zrangebyscore+zrem组合的 QPS 上限,必要时横向扩 worker 进程
最易被忽略的是:Redis 持久化策略(RDB/AOF)若关闭,服务器重启后所有延迟任务丢失——必须搭配 zcard 监控 + 启动时补偿扫描兜底。











