CodeIgniter 不支持原生延时队列,需借助外部机制;Redis ZSet 方案侵入小、精度高、无需额外中间件,适合中小型项目落地,而 RabbitMQ 因需 DLX+TTL 组合、运维复杂、易拖慢接口,应谨慎选用。

CodeIgniter 本身不内置延时队列或定时任务调度能力,必须靠外部机制补足——直接在 CI 中用 sleep() 或 set_time_limit() 模拟延时是不可靠的,会阻塞请求、被超时中断、重启即丢任务。
为什么不能用 CI 的 scheduler 或 cron 类做“真延时”
CI 的 Cron 类(如第三方库)只是封装了系统 cron 调用逻辑,本质仍是轮询:比如每分钟执行一次 php index.php order cancel_expired。它解决不了“订单创建后精准 30 分钟触发”这个需求,只适合容忍误差(±60 秒)且量级不大的场景。
- 每次执行都要全表扫描
t_order,没索引时WHERE status = 'unpaid' AND create_time 会变慢查询 - 集群部署下,多个实例同时跑这个命令,得加分布式锁(比如用 Redis
SET order:cancel:lock 1 NX EX 30),否则重复取消 -
create_time字段必须有单列索引或与status的联合索引,否则分页更新时LIMIT+OFFSET会越来越慢
用 Redis ZSet 实现轻量级延时队列(推荐 CI 项目接入)
这是 CI 项目最可行的“伪实时”方案:订单创建时把 order_id 写入 Redis ZSet,score 设为到期时间戳(如 time() + 1800),再起一个常驻 PHP 进程(非 Web 请求)持续 ZRANGEBYSCORE 拉取已到期 ID 并处理。
- CI 控制器里写入:用
$this->redis->zAdd('order:delay', time() + 1800, $order_id)(需先配置好 Redis 驱动) - 单独写个 CLI 命令:
php index.php tools check_delay_queue,内部循环执行:$ids = $this->redis->zRangeByScore('order:delay', '-inf', time(), ['limit' => [0, 100]]); - 拉到 ID 后,先
ZREM删除,再查 DB 确认状态是否仍是unpaid(防重复或已支付),最后调用$this->order_model->cancel($id) - 这个 CLI 进程要用
nohup php index.php tools check_delay_queue &启动,并配合supervisord管理生命周期
CI 中调用 RabbitMQ 延迟消息的硬伤与绕过方式
CI 没有原生 MQ 客户端,强行集成 RabbitMQ 的 php-amqplib 会引入大量依赖和异步复杂度;更麻烦的是 RabbitMQ 官方不支持原生延迟消息,得靠死信队列(DLX)+ TTL 组合实现,而 TTL 是队列级或消息级设置,无法为每个订单设不同超时(比如有的 30 分钟,有的 2 小时)。
- 若坚持用 RabbitMQ,只能建多个 TTL 固定的队列(如
delay_30m,delay_120m),订单创建时按规则路由过去 - CI 中发消息必须用
try/catch包裹$channel->basic_publish(),失败要落库重试,否则消息丢了就再也找不到那个订单 - 消费者不能写在 CI 的控制器里——得另起独立脚本监听,且要手动处理消息重入(比如用 DB 记录已处理
order_id,消费前先查) - 真正上线前,务必压测
basic_publish在高并发下单时的耗时,CI 的同步 HTTP 请求链路里插入 MQ 调用,可能让接口响应从 200ms 拉长到 800ms+
Redis ZSet 方案看似多了一个常驻进程,但它对 CI 项目侵入最小、无额外中间件依赖、超时精度可控到秒级,且所有逻辑仍可写在 CI 的 Model/Command 层里——这才是中小型项目该盯住的落地点。别在“看起来高级”的 RabbitMQ 上卡住上线节奏,尤其当运维连 Erlang 环境都没配好时。


















