跳转到主要内容
要为包添加定期同步或 Heartbeat,应将处理逻辑的提供与向调度的注册分开。设计时不要让仅仅安装包就增加使用方应用的定时任务,而是让使用者可以选择是否启用、执行频率以及在多台服务器上的行为。 本页基于 Laravel Framework v13.39.0 的官方源码,将任务调度的基本操作应用于包开发。关于服务提供者的加载,请参阅包自动发现的内部结构;关于配置的发布,请参阅包配置的合并与缓存。

在 Schedule 解析后注册

在常规的 Laravel 应用中,FoundationServiceProvider 会将 Schedule::class 注册为 singleton。其创建过程会调用 Console Kernel 的 resolveConsoleSchedule(),并应用应用程序的时区和调度器所用的缓存存储。 包应扩展这个共享的 Schedule,而不是创建自己的 new Schedule() 了事。下面的服务提供者假定 Acme\Courier\ 通过 Composer 的 PSR-4 对应到 src/,并且包中有 config/courier.php。
callAfterResolving() 在尚未解析时会等待之后的解析;如果已经解析,则会获取实例并立即调用回调。因此,即使其他服务提供者先使用了 Schedule,也能完成注册。详情请参阅服务解析后钩子。 runningInConsole() 并不用于判断特定的 Artisan 命令。本例将通过 HTTP 使用 Schedule 的场景排除在外。另外,注册钩子的服务提供者本身不要设为延迟服务提供者。因为如果在需要之前 boot() 没有运行,钩子也不会被注册。
事件的注册与任务的执行是两回事。加载此服务提供者并不会立即执行 Heartbeat。仅注册也不会启动 cron 或 schedule:work。

由使用者显式启用

包中的 config/courier.php 默认设为禁用。
在使用方应用中发布或创建配置,并将 schedule.enabled 改为 true。本例在注册时读取这些值。时区默认为 UTC;如果选择其他时区,还需要考虑夏令时导致的重复执行或漏执行。 由于配置是浅合并的,注册器中也要为每个键指定默认值。这样即使部分覆盖导致 schedule 数组中的其他键缺失,也能以基本配置完成注册。

防止向同一个 Schedule 重复注册

下面的 src/CourierSchedule.php 注册一个记录一条日志的最小 Heartbeat。借助 WeakMap,该注册器只会向同一个 Schedule 对象添加一次。
由于该注册器是 singleton,即使从服务提供者的钩子中再次获取,也会共享已注册判定。另一方面,仍然可以向新的 Schedule 注册。不要使用整个进程范围的静态”已注册”标志,以免连新容器或测试中的 Schedule 也被排除。 Schedule::call()、exec()、job() 会将事件添加到数组中。它们并不是查找同名事件并进行替换或去重的 API。
withoutOverlapping() 并不能修正重复注册。如果同一任务被注册了两次,在前一次执行结束并释放锁后,后一个事件也可能被执行。onOneServer() 也不是去除重复注册的功能。上面的 WeakMap 只能防止该注册器自身的重复。如果使用者在 routes/console.php 中添加了相同的任务,应禁用包的自动注册,让注册职责只由一处负责。

事件名称也关系到锁的兼容性

name() 是 description() 的别名,但它对锁名称的影响因事件类型而异。以下是默认的 mutex 名称。 对于 CallbackEvent,要在 withoutOverlapping() 或 onOneServer() 之前设置非空名称。如果未设置名称,这些方法会抛出异常。 job() 通常以任务类名等作为初始名称。但是,当以不同的参数或对象注册同一个任务时,应为每个对象显式指定名称。仅改变参数并不会让初始名称自动变成不同的名称。
SyncCatalogJob 是包所提供的任务类示例。任务的实现方法请参阅队列与任务。 更改名称、命令或 cron 表达式时,根据事件类型,锁的标识也会随之改变。在新旧版本进程并存的部署中,即使是相同的处理也可能获取不同的锁。不要把它当作单纯的显示文字变更,而应将任务名称作为稳定的公开规范来管理。

区分 onOneServer 与 withoutOverlapping

  • onOneServer() 用于选出在该执行时刻负责执行的服务器。经确认,CacheSchedulingMutex 会在事件的 mutex 名称后加上时分,并以 1 小时的有效期保存。它与随任务结束而释放的防重叠锁是不同的。
  • withoutOverlapping() 用于防止在前一次执行仍在进行时发生重叠。指定的有效期单位为分钟,默认为 1,440 分钟。有效期过短时,锁会在处理结束前失效。
  • 当 job() 将任务投递到队列时,调度事件的完成并不等于任务处理的完成。worker 端的重复控制需要另行设计,例如任务端的唯一任务或 WithoutOverlapping 中间件等。
对于启用 onOneServer() 的使用者,应告知其必须满足官方文档指定的 database、memcached、dynamodb、redis 共享缓存配置。锁并不保证业务处理的 exactly-once,处理本身也需要能够承受重复执行。

繁重的依赖在执行时解析

上述 Heartbeat 中的 LoggerInterface,会在事件执行时由 CallbackEvent 使用容器的 call() 进行解析。这种结构在注册钩子时既不会输出日志,也不会创建客户端。 不在构造函数中放置外部连接或业务处理的设计,在 Artisan 命令的依赖解析中同样重要。 此外,call() 和 job() 属于 CallbackEvent,因此不能使用 runInBackground()。耗时的处理应委托给队列,或根据需要选择 command() 的后台执行。

不要篡改整个应用的配置

Schedule::useCache() 会更改 Schedule 所持有的 mutex 对象的存储。它并不是只针对包事件的配置,也会影响共享同一 mutex 的其他事件。不要在包的 boot() 中无条件调用它,而应将其视为由使用方应用选择的配置。 同样,包也不应擅自更改 Schedule::alwaysOnOneServer()、调度器的暂停以及维护模式期间的执行策略。

配置变更后要重新创建 Schedule

示例中的 enabled、cron 表达式和时区都是注册时的配置。对于已创建的 Schedule,仅更改 config() 的值并不会添加或删除事件,也不会重新读取配置。WeakMap 的判定也不是用于重新配置的。 在使用配置缓存的部署中,应重建缓存,并在加载新配置的进程中确认 schedule:list。对于长时间运行的调度器,以及正在执行不足一分钟任务的 schedule:run,也要确认运维上不会残留旧代码和旧配置。关于子分钟任务在部署时的中断,请参阅任务调度。

将注册与执行分开测试

下面是使用包的 Composer 自动加载和 PHPUnit 的 tests/CourierScheduleTest.php 示例。将 mutex 替换为测试替身,在不使用外部缓存或真实 cron 的情况下确认注册结果。
该测试是注册器的单元测试。服务提供者的自动发现、配置合并以及与 Console Kernel 的集成,需要另行通过 Orchestra Testbench 或验证用应用进行确认。 不要为了通过注册测试而用 schedule:run 执行使用方应用的全部任务。将主体测试与在受控环境中进行的调度器集成测试分开,可以避免无关定时任务带来的副作用。

相关页面

任务调度

确认执行频率、cron 的配置、委托给队列以及部署时的运维。

包的服务解析后钩子

确认对未解析和已解析服务的应用,以及重新注册时的注意事项。

参考的一手资料

最后修改于 2026年11月1日