Skip to content

[BUG] 定时任务重复执行 #7743

Open

Description

@lushaobo

Execute the command and paste the result below.

Command: uname -a && php -v && composer info | grep hyperf && php --ri swoole

# Paste the result here.
Linux 9e1c32c7d4d8 5.15.153.1-microsoft-standard-WSL2 #1 SMP Fri Mar 29 23:14:13 UTC 2024 x86_64 Linux
PHP 8.4.14 (cli) (built: Oct 24 2025 20:05:56) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.14, Copyright (c) Zend Technologies
    with Zend OPcache v8.4.14, Copyright (c), by Zend Technologies
friendsofhyperf/lock               3.1.77  The lock component for Hyperf.
friendsofhyperf/tinker             3.1.75  The Powerful REPL for Hyperf.
hyperf/async-queue                 3.1.64  A async queue component for hyperf.
hyperf/cache                       3.1.67  A cache component for hyperf.
hyperf/code-parser                 3.1.63  A code parser component for Hyperf.
hyperf/codec                       3.1.63  A codec component for Hyperf.
hyperf/collection                  3.1.64  Hyperf Collection package which come from illuminate/collections
hyperf/command                     3.1.64  Command for hyperf
hyperf/conditionable               3.1.63  Hyperf Macroable package which come from illuminate/conditionable
hyperf/config                      3.1.63  An independent component that provides configuration container.
hyperf/context                     3.1.63  A coroutine/application context library.
hyperf/contract                    3.1.63  The contracts of Hyperf.
hyperf/coordinator                 3.1.63  Hyperf Coordinator
hyperf/coroutine                   3.1.65  Hyperf Coroutine
hyperf/crontab                     3.1.67  A crontab component for Hyperf.
hyperf/database                    3.1.68  A flexible database library.
hyperf/db-connection               3.1.66  A hyperf db connection handler for hyperf/database.
hyperf/devtool                     3.1.66  A Devtool for Hyperf.
hyperf/di                          3.1.67  A DI for Hyperf.
hyperf/dispatcher                  3.1.63  A HTTP Server for Hyperf.
hyperf/engine                      2.15.0  Coroutine engine provided by swoole.
hyperf/engine-contract             1.14.0  Contract for Coroutine Engine
hyperf/event                       3.1.63  an event manager that implements PSR-14.
hyperf/exception-handler           3.1.63  Exception handler for hyperf
hyperf/filesystem                  3.1.68  flysystem integration for hyperf
hyperf/framework                   3.1.63  A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.
hyperf/guzzle                      3.1.66  Swoole coroutine handler for guzzle
hyperf/http-message                3.1.65  microservice framework base on swoole
hyperf/http-server                 3.1.65  A HTTP Server for Hyperf.
hyperf/laminas-mime                3.0.0   Create and parse MIME messages and parts
hyperf/logger                      3.1.63  A logger component for hyperf.
hyperf/macroable                   3.1.63  Hyperf Macroable package which come from illuminate/macroable
hyperf/memory                      3.1.63  An independent component that use to operate and manage memory.
hyperf/model-listener              3.1.63  A model listener for Hyperf.
hyperf/paginator                   3.1.65  A paginator component for hyperf.
hyperf/pipeline                    3.1.63  Hyperf Macroable package which come from illuminate/pipeline
hyperf/pool                        3.1.66  An independent universal connection pool component.
hyperf/process                     3.1.63  A process component for hyperf.
hyperf/redis                       3.1.66  A redis component for hyperf.
hyperf/serializer                  3.1.63  A serializer component for Hyperf.
hyperf/server                      3.1.63  A base server library for Hyperf.
hyperf/socketio-server             3.1.63  Socket.io implementation for hyperf
hyperf/stdlib                      3.1.63  A stdlib component for Hyperf.
hyperf/stringable                  3.1.65  Hyperf Stringable package which come from illuminate/support
hyperf/support                     3.1.65  A support component for Hyperf.
hyperf/tappable                    3.1.63  Hyperf Macroable package which come from illuminate/tappable
hyperf/testing                     3.1.63  Testing for hyperf
hyperf/translation                 3.1.63  An independent translation component, forked by illuminate/translation.
hyperf/validation                  3.1.67  hyperf validation
hyperf/websocket-server            3.1.63  A websocket server library for Hyperf.

swoole

Swoole => enabled
Author => Swoole Team <team@swoole.com>
Version => 6.1.6
Built => Jan 26 2026 12:49:13
host byte order => little endian
coroutine => enabled with boost asm context
epoll => enabled
eventfd => enabled
signalfd => enabled
cpu_affinity => enabled
spinlock => enabled
rwlock => enabled
openssl => OpenSSL 3.3.5 30 Sep 2025
dtls => enabled
http2 => enabled
json => enabled
curl-native => enabled
curl-version => 8.14.1
c-ares => 1.34.6
zlib => 1.3.1
brotli => E16781312/D16781312
mutex_timedlock => enabled
pthread_barrier => enabled
coroutine_pgsql => enabled
coroutine_odbc => enabled
coroutine_sqlite => enabled

Directive => Local Value => Master Value
swoole.enable_library => On => On
swoole.enable_fiber_mock => Off => Off
swoole.enable_preemptive_scheduler => Off => Off
swoole.display_errors => On => On
swoole.use_shortname => Off => Off
swoole.unixsock_buffer_size => 8388608 => 8388608

Description:

通过日志,发现执行时机不对,有一些重复执行,达不到预期每5秒执行一次的需求

Steps To Reproduce:

日志内容
Image

任务配置
Image

任务类
Image

Activity

  1. lushaobo commented on Apr 23, 2026

    @lushaobo
    Author

    换成每10秒执行一次,发现有一定机率,会隔一秒再执行

    定时配置

    Image

    日志

    Image
  2. limingxinleo commented on Apr 25, 2026

    @limingxinleo
    Member

    这个不好查,建议你直接加个锁就行了

  3. lushaobo commented on Apr 29, 2026

    @lushaobo
    Author

    好的,不过还是希望官方能重视下这个问题。 另外, 我这个是在 wsl+docker的环境下使用的,不知有无影响。

  4. suyar commented on May 11, 2026

    @suyar
    Contributor

    @lushaobo 如果能稳定复现,可以在本地vendor代码里调试,很多场景只有在特定条件下才出现

  5. lushaobo commented on May 11, 2026

    @lushaobo
    Author

    OK,我抽个时间再看看,到时有结果了再发出来。

  6. drttcrh commented on May 13, 2026

    @drttcrh

    秒级定时器一直都有这个问题,目前只能自己加个redis锁限制

  7. tw2066 commented on Sep 12, 2026

    @tw2066
    Contributor
  8. tw2066 commented on Sep 13, 2026

    @tw2066
    Contributor

    根因分析

    Parser::parse() 要求基准时间落在整分钟的第 0 秒,而 CrontabManager::parse() 传的是 time()(当前任意秒)。基准偏移多少秒,这一分钟解析出的全部触发点就整体偏移多少秒。

    1. 触发点是「相对偏移」,不是「绝对槽位」

    src/crontab/src/Parser.php:63

    $result[] = Carbon::createFromTimestamp($startTime + $second, $timezone);

    */5 * * * * * 这条规则的语义是「每分钟的第 0、5、10…55 秒」,但代码算的是「基准 + 0、基准 + 5、基准 + 10…」。两者只在 $startTime 恰好落在第 0 秒时才等价。

    实测(规则 */10):

    基准 12:00:00 → 12:00:00  12:00:10  12:00:20  12:00:30  12:00:40  12:00:50   ← 正确
    基准 12:01:09 → 12:01:09  12:01:19  12:01:29  12:01:39  12:01:49  12:01:59   ← 整体偏移 9 秒
    

    偏移后的序列会与上一分钟的正常序列在交界处交错,产生异常间隔。

    2. 这个契约一直存在,只是只在测试里被遵守

    ParserTest 里每一个测试都手动对齐了基准:

    $startTime = Carbon::createFromTimestamp(1561052867, $this->timezone)->startOfMinute();
    $result = $parser->parse($crontabString, $startTime->getTimestamp(), $this->timezone);

    而生产路径 src/crontab/src/CrontabManager.php:42 传的是未对齐的 time():

    $last = time();

    测试路径全部对齐、生产路径没对齐 —— 这就是这个 bug 能长期存在的原因。

    3. 现有防护挡不住

    src/crontab/src/Process/CrontabDispatcherProcess.php:122

    $minuteTimestamp = (int) (time() / 60);
    if ($this->minuteTimestamp !== 0 && $minuteTimestamp === $this->minuteTimestamp) {

    它只比较「是不是同一分钟」,从不校验秒是否为 0。12:01:00 和 12:01:09 属于同一分钟,所以偏移后的基准被直接放行。

    另外 $this->minuteTimestamp 初始值是 0,首次调用时 $this->minuteTimestamp !== 0 恒为 false,防护被整个跳过。

    4. 偏移量与实际现象的对应关系

    基准偏移多少秒,交界处就会出现多长的异常间隔:

    基准偏移 */5 交界处最小间隔 */10 交界处最小间隔
    0 秒 5 秒 ✓ 10 秒 ✓
    1 秒 4 秒 9 秒
    3 秒 2 秒 7 秒
    4 秒 1 秒 ❌ 6 秒
    9 秒 1 秒 ❌ 1 秒 ❌
    10 秒 0 秒(同一时刻重复)❌ 0 秒 ❌
    11 秒 1 秒 ❌ 1 秒 ❌

    */5 会造成异常的偏移量有 8 个,*/10 只有 4 个 —— 这正好对应「*/5 更容易复现、*/10 偶发」的观察。

    两次执行的间隔只有 1 秒。只要任务本身跑得比 1 秒快,第一次执行完锁就已经释放了,第二次照跑不误。 这不是并发冲突,是调度时刻算错了 —— 锁没有可挡的并发窗口。

    5. 修复

    $last = (int) (time() / 60) * 60;

    把基准对齐到当前分钟的起点,让 Parser 拿到的永远是「这一分钟的第 0 秒」。这样不管 dispatcher 什么时候执行到 parse(),解析出的槽位都是规则真正描述的那些,交界处的交错也就消失了。

    为什么改在 CrontabManager 而不是 Parser 内部:Parser 的「相对偏移」语义被 ParserTest::testParseSecondLevel 锁定了(第二段传 01:47:55 作为基准,期望 01:47:55, 01:48:06, 01:48:17…),改 Parser 会破坏该测试。违约方是 CrontabManager,修在这里才是正确位置。

  9. suyar commented on Sep 14, 2026

    @suyar
    Contributor

    @tw2066 如果是每7秒执行一次呢,执行8次后到56秒。那么会不会到下一分钟的07秒才又开始执行。中间有可能间隔了11秒?

  10. tw2066 commented on Sep 14, 2026

    @tw2066
    Contributor

    危险阈值随规则周期变化,*/7 只需要 3 秒延迟。

    交界处塌成 1 秒间隔(或直接重叠成 0 秒)的条件,是本分钟的最后一个槽位被偏移推到第 59 秒、或溢出到下一分钟。这个阈值取决于规则的秒槽位分布:

    规则 分钟内槽位 会造成 1 秒间隔的偏移量 最小阈值
    */5 00 05 10 … 55(12 个) 4, 6, 9, 11, 14, 16, 19, 21 … 4 秒
    */7 00 07 14 … 56(9 个) 3, 5, 10, 12, 17, 19, 24, 26 … 3 秒
    */10 00 10 20 … 50(6 个) 9, 11, 19, 21, 29 … 9 秒

    */7 的实测(第 1 轮基准带偏移、第 2 轮基准正常):

    基准偏移   第 1 轮触发点(秒)                        交界处最小间隔
    偏移 0 秒  00 07 14 21 28 35 42 49 56                  4 秒    ← 正常语义
    偏移 1 秒  01 08 15 22 29 36 43 50 57                  3 秒
    偏移 2 秒  02 09 16 23 30 37 44 51 58                  2 秒
    偏移 3 秒  03 10 17 24 31 38 45 52 59                  1 秒   ❌❌
    偏移 4 秒  04 11 18 25 32 39 46 53 00(溢出)            0 秒   ❌ 同一时刻执行两次
    偏移 5 秒  05 12 19 26 33 40 47 54 01(溢出)            1 秒   ❌❌
    

    */7 的触发阈值比 */5 还小(3 秒 vs 4 秒) —— 说明这不是「只有大延迟才出问题」,几秒的调度抖动就够了。

    附注:*/7 自身的跨分钟间隔固定为 4 秒(而非 7 秒),这是 60 % 7 != 0 带来的 cron 固有语义(Linux crontab 同理),修复前后都存在,与本次问题无关。修复后它的节奏是完全确定的 7 7 7 7 7 7 7 7 4 循环。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions