news 2026/10/7 9:41:04

Hyperf Retry 组件实战:注解式重试、策略组合与熔断防雪崩指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hyperf Retry 组件实战:注解式重试、策略组合与熔断防雪崩指南
  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

导读

Hyperf 是高性能的协程框架,而网络通信天然不稳定,微服务场景下调用失败在所难免。hyperf/retry 组件为 Hyperf 提供了从“注解式重试”到“多策略可插拔组合”的一整套容错方案:既能用#[Retry]一行注解完成重试,也能通过自定义注解、组合策略(次数限制、异常分类、退避间隔、预算限流、超时、熔断、降级)适配任意业务场景,还能用 Fluent 链式调用编写过程式重试逻辑。读完本文,你将掌握 retry 组件的全部配置参数、策略语义,以及其底层 AOP 切面与令牌桶预算的实现原理,能够在自己的 Hyperf 服务中安全、可控地引入重试机制。

为什么重试必须“受控”

分布式系统依赖网络通信,而网络本质上不稳定,因此良好的容错设计必不可少。但无差别重试非常危险:当通信出现问题时,如果每个请求都被重试一次,相当于系统 IO 负载瞬间提升 100%,极易触发雪崩(Avalanche)。同时重试还要考虑错误原因——如果是重试无法解决的问题,重试只会浪费资源;如果被重试的接口不幂等,还可能造成数据不一致等更严重的后果。

hyperf/retry 组件正是为解决这些问题而设计:它提供一整套丰富的重试机制,用策略(Policy)组合的方式覆盖不同场景的差异化需求,并内置预算(Budget)机制从全局层面限制重试对系统造成的额外负载。

安装

composer require hyperf/retry

安装完成后,组件会通过 ConfigProvider 自动注册注解收集器与 AOP 切面,无需额外配置即可使用#[Retry]注解。

Hello World:一行注解开启重试

在需要重试的方法上添加#[Retry]注解即可:

/** * Retry method ketika terjadi exception */ #[Retry] public function foo() { // 发起远程调用 }

默认的重试策略足以覆盖日常大多数重试需求,并且不会因为过度重试而引发雪崩——这正是默认策略中内置了BudgetRetryPolicy(预算策略)的原因。

从源码实现看,#[Retry]的能力由 RetryAnnotationAspect 这个 AOP 切面提供:该切面监听AbstractRetry注解(见 AbstractRetry.php),在切面process()中先把注解上的所有策略实例化并组合成一个HybridRetryPolicy,随后进入canRetry → process → beforeRetry → 循环的重试循环(源码中以attempt:/end:标签实现 goto 循环),直到策略判定不再重试为止。也就是说,注解只是声明,真正的重试逻辑全部由策略驱动。

深入自定义:从 0 构建专属重试注解

组件通过组合多个重试策略实现可插拔(pluggability):每个策略只关注重试流程中的一个侧面——是否重试的判断、重试间隔、结果处理等。通过调整注解中使用的策略,你可以配置出适配任意场景的重试行为。

官方强烈建议按具体业务需求构建自己的“别名注解”。下面演示如何创建一个最大尝试次数为 3的新注解。

说明:默认的#[Retry]注解本身就可以通过#[Retry(maxAttempts=3)]控制最大重试次数,这里仅为演示目的,假定该参数不存在。

第一步:继承 AbstractRetry

首先创建新的注解类,继承\Hyperf\Retry\Annotation\AbstractRetry:

<?php declare(strict_types=1); namespace App\Annotation; use Attribute; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { }

注意:源码中注解基类的实际命名空间是Hyperf\Retry\Annotation\AbstractRetry(见 AbstractRetry.php)。AbstractRetry本身继承自Hyperf\Di\Annotation\AbstractAnnotation,其collectMethod()会把注解收集到 DI 的AnnotationCollector中,供 AOP 切面在运行时读取。

第二步:限制重试次数

按需覆写$policies属性。要限制重试次数,需要用到MaxAttemptsRetryPolicy,它有一个参数$maxAttempts(最大尝试次数上限):

<?php declare(strict_types=1); namespace App\Annotation; use Attribute; use Hyperf\Retry\Policy\MaxAttemptsRetryPolicy; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { public $policies = [ MaxAttemptsRetryPolicy::class, ]; public $maxAttempts = 3; }

现在#[MyRetry]会让任何方法最多循环执行 3 次。从 MaxAttemptsRetryPolicy 的实现看:start()时把上下文attempt置为 1,beforeRetry()每次自增attempt,canRetry()仅在attempt < maxAttempts时返回 true,否则置retryExhausted并终止重试。

第三步:加入异常分类策略

我们还需要ClassifierRetryPolicy来控制哪类错误才值得重试。加入后,默认它只会对抛出的Throwable进行重试:

<?php declare(strict_types=1); namespace App\Annotation; use Attribute; use Hyperf\Retry\Policy\ClassifierRetryPolicy; use Hyperf\Retry\Policy\MaxAttemptsRetryPolicy; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { public $policies = [ MaxAttemptsRetryPolicy::class, ClassifierRetryPolicy::class, ]; public $maxAttempts = 3; }

继续打磨:限定超时异常 + 可变间隔退避

你可以持续细化这个注解直到满足自定义需求。例如:只重试用户自定义的TimeoutException,并使用可变间隔(backoff)策略,重试前至少睡眠 100 毫秒:

<?php declare(strict_types=1); namespace App\Annotation; use Attribute; use Hyperf\Retry\Policy\ClassifierRetryPolicy; use Hyperf\Retry\Policy\MaxAttemptsRetryPolicy; use Hyperf\Retry\Policy\SleepRetryPolicy; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { public $policies = [ MaxAttemptsRetryPolicy::class, ClassifierRetryPolicy::class, SleepRetryPolicy::class, ]; public $maxAttempts = 3; public $base = 100; public $strategy = \Hyperf\Retry\BackoffStrategy::class; public $retryThrowables = [\App\Exception\TimeoutException::class]; }

只要该文件能被 Hyperf 扫描到(位于app/目录、由注解扫描器加载),就可以在方法上使用#[MyRetry]来重试超时错误。

关于策略顺序:RetryAnnotationAspect::makePolicy()会按$policies数组的顺序依次实例化策略并传入注解的完整参数(make($policy, $annotation->toArray())),最终组合成HybridRetryPolicy。策略被当作“堆叠的中间件”,顺序不同,行为可能不同——例如预算策略需要放在次数策略之前,才能先做全局限流再判断次数。

默认配置详解

#[Retry]注解的完整默认属性如下(与 Annotation/Retry.php 源码中的构造函数默认值一一对应):

/** * 重试策略数组,可视为堆叠的中间件 * @var string[] */ public $policies = [ FallbackRetryPolicy::class, ClassifierRetryPolicy::class, BudgetRetryPolicy::class, MaxAttemptsRetryPolicy::class, SleepRetryPolicy::class, ]; /** * 重试间隔算法 */ public string $sleepStrategyClass = SleepStrategyInterface::class; /** * 最大尝试次数 */ public int $maxAttempts = 10; /** * Retry Budget 重试预算 * ttl: 令牌有效时长(秒) * minRetriesPerSec: 基础令牌生成速率(每秒最少可重试次数) * percentCanRetry: 按请求量的该比例生成新令牌 * * @var array|RetryBudgetInterface */ public $retryBudget = [ 'ttl' => 10, 'minRetriesPerSec' => 1, 'percentCanRetry' => 0.2, ]; /** * 每次尝试的基础时间间隔(毫秒) * 对 backoff 策略而言是第一次尝试的间隔;对 flat 策略而言是每次尝试的间隔 */ public int $base = 0; /** * 配置 Predicate(谓词),判断某个异常是否应被重试 * Predicate 返回 true 表示应重试,返回 false 表示不重试 * * @var callable|string */ public $retryOnThrowablePredicate = ''; /** * 配置 Predicate,判断某个返回值是否应被重试 * Predicate 返回 true 表示应重试,返回 false 表示不重试 * * @var callable|string */ public $retryOnResultPredicate = ''; /** * 配置被记为失败(因而需要重试)的 Throwable 类列表 * 任何匹配或继承自列表中任一类的 Throwable 都会被重试,除非被 ignoreThrowables 忽略。 * 忽略(ignore)的优先级高于重试(retry)。 * * @var array<string|\Throwable> */ public $retryThrowables = [\Throwable::class]; /** * 配置被忽略(因而不会重试)的错误类列表 * 任何匹配或继承自列表中任一类的异常都不会被重试,即使它被 retryThrowables 标记。 * * @var array<string|\Throwable> */ public $ignoreThrowables = []; /** * 所有尝试耗尽后的 fallback 回调 * * @var callable|string */ public $fallback = '';

几个默认值的要点:

  • maxAttempts = 10:默认最多尝试 10 次(首次调用 + 9 次重试)。
  • base = 0:默认不等待,立即重试;若需要退避必须显式配置base与strategy。
  • retryBudget三个参数共同决定令牌桶规模:在 RetryBudget 中,最大令牌数为maxToken = (minRetriesPerSec / percentCanRetry) * ttl。按默认值计算即(1 / 0.2) * 10 = 50个令牌,对应每秒约 5 次可重试的额度上限。
  • retryThrowables = [\Throwable::class]:默认对所有异常一律重试,若只想重试特定异常(如TimeoutException),务必覆写该属性。

可选策略详解

以下策略均可独立或组合使用,是自定义注解时的“积木”。

Max Attempts PolicyMaxAttemptsRetryPolicy

限制最大尝试次数。

参数类型描述
maxAttemptsint最大尝试次数

实现上,首次尝试时attempt记为 1,每重试一次自增 1,直到attempt >= maxAttempts时置retryExhausted = true并终止(MaxAttemptsRetryPolicy.php)。

Error Classifier PolicyClassifierRetryPolicy

通过分类器(classifier)判断某个错误是否值得重试。

参数类型描述
ignoreThrowablesarray被忽略的Throwable类名列表,优先级高于retryThrowables
retryThrowablesarray需要重试的Throwable类名列表,优先级高于retryOnThrowablePredicate
retryOnThrowablePredicatecallable通过函数判断某个Throwable是否可重试,可重试返回 true,否则 false
retryOnResultPredicatecallable通过函数判断某个返回值是否可重试,可重试返回 true,否则 false

从 ClassifierRetryPolicy 的源码看,其判定顺序是严格的:先检查ignoreThrowables(命中即不重试)→ 再检查retryThrowables(命中即重试)→ 最后才调用retryOnThrowablePredicate;retryOnResultPredicate仅当未抛出异常(lastThrowable为 null)且返回值非空时才被调用。这意味着你既可以用类名列表精确圈定异常,也可以用闭包编写任意复杂的判定逻辑。

Fallback PolicyFallbackRetryPolicy

当重试资源耗尽后,执行一个替代方法(降级兜底)。

参数类型描述
fallbackcallable兜底方法

除了is_callable能识别的普通闭包/函数外,fallback还支持class@method格式的字符串:框架会从Container(容器)中取出对应的class,再执行其method方法。这种写法在需要注入依赖的降级逻辑中非常实用。

Sleep PolicySleepRetryPolicy

提供两种重试间隔策略:固定间隔(FlatStrategy)与可变间隔(BackoffStrategy)。

参数类型描述
baseint基础睡眠时间(毫秒)
strategystring任意实现Hyperf\Retry\SleepStrategyInterface的类名,如Hyperf\Retry\BackoffStrategy

从 SleepRetryPolicy 的实现看,start()时通过容器按sleepStrategyClass实例化睡眠策略(传入base),beforeRetry()在每次重试前调用strategy->sleep()完成等待。FlatStrategy每次固定睡base毫秒;BackoffStrategy则以base为第一次间隔、随后递增(如base, base*2, base*4...),从而在连续失败时自动拉开重试节奏,降低对下游的瞬时压力。

Timeout PolicyTimeoutRetryPolicy

当总执行时间超过指定时长后,退出重试会话(超时熔断)。

参数类型描述
timeoutfloat超时时间(秒)

注意这是针对整个重试会话(多次尝试累计)的时间预算,而非单次调用的超时。

Circuit Breaker PolicyCircuitBreakerRetryPolicy

当重试失败并退出重试会话后,直接进入“熔断(fused)”状态一段时间,期间不再做任何尝试。

参数类型描述
circuitBreakerState.resetTimeoutfloat恢复所需时间(秒)

熔断状态由 CircuitBreakerState 维护,组件还提供了配套的#[CircuitBreaker]注解(见 CircuitBreaker.php)及对应测试 CircuitBreakerAnotationAspectTest.php 验证其行为。

Budget PolicyBudgetRetryPolicy

每个#[Retry]注解会对应生成一个令牌桶(token bucket):每次被注解的方法被调用,就往桶里放入一个带有过期时间(ttl)的令牌;当发生可重试错误时,必须先消耗相应数量(percentCanRetry)的令牌才能执行重试,否则不再重试(错误继续向下抛出)。

举个例子:当percentCanRetry = 0.2时,每次重试要消耗 5 个令牌(1 / 0.2 = 5)。这样当对端(peer)崩溃时,最多只会带来20%的额外重试消耗,对大多数系统而言是可接受的安全水位。

为了照顾低频方法(调用量少导致令牌不足),令牌桶还会每秒生成一批“最低额度”令牌(minRetriesPerSec),保证系统重试能力的下限稳定。

参数类型描述
retryBudget.ttlint令牌有效期(秒)
retryBudget.minRetriesPerSecint每秒保证的最低重试次数
retryBudget.percentCanRetryfloat重试次数不超过总请求量的百分比

其实现细节在 RetryBudget.php 中:构造时算出maxToken(令牌桶上限);init()首次会预生成minRetriesPerSec / percentCanRetry个令牌,之后每秒通过协程定时器Timer->tick(1, ...)再补充同数量令牌,同时剔除过期令牌与溢出令牌;consume()检查桶内令牌数是否大于等于1 / percentCanRetry,满足则一次性出队相应数量。对应测试见 RetryBudgetTest.php。

重要提醒:重试组件的令牌桶不跨 worker 共享,每个 worker 进程各自持有一个桶,因此实际可发生的总重试次数约为“单桶上限 × worker 数量”,在规划容量时需要把 worker 数乘进去。

别名注解

由于重试注解的配置项相对复杂,组件预置了以下别名注解,方便日常快速书写:

  • #[RetryThrowable]:仅重试Throwable,等同于默认的#[Retry]。
  • #[RetryFalsy]:仅当返回值与 false 宽松相等($result == false)时重试,不重试异常。
  • #[BackoffRetryThrowable]:#[RetryThrowable]的可变间隔版本,重试间隔至少 100 毫秒。
  • #[BackoffRetryFalsy]:#[RetryFalsy]的可变间隔版本,重试间隔至少 100 毫秒。

这些别名注解的源码分别位于 RetryThrowable.php、RetryFalsy.php、BackoffRetryThrowable.php 与 BackoffRetryFalsy.php,并有 RetryFalsyTest.php 等测试用例覆盖其行为。

Fluent 链式调用

除了注解方式,组件还支持用普通 PHP 函数以链式调用的方式使用重试。Retry类通过__callStatic静态代理到 FluentRetry,因此以下写法都合法。

方式一:Retry::with()手动组装策略

<?php $result = \Hyperf\Retry\Retry::with( new \Hyperf\Retry\Policy\ClassifierRetryPolicy(), // 默认重试所有 Throwable new \Hyperf\Retry\Policy\MaxAttemptsRetryPolicy(5) // 最多重试 5 次 )->call(function () { if (rand(1, 100) >= 20) { return true; } throw new Exception; });

方式二:语义化 Fluent 链

<?php $result = \Hyperf\Retry\Retry::whenReturns(false) // 当返回 false 时重试 ->max(3) // 最多 3 次 ->inSeconds(5) // 总耗时最多 5 秒 ->sleep(1) // 每次间隔 1 毫秒 ->fallback(function () { return true; }) // 兜底函数 ->call(function () { if (rand(1, 100) >= 20) { return true; } return false; });

FluentRetry 提供的完整链式方法包括:with(...$policies)、when($callable)(表达式策略)、whenReturns($value)、whenThrows($throwable = Throwable::class)、max($times)、inSeconds($seconds)(对应TimeoutRetryPolicy)、fallback($callable)、sleep($base)(固定间隔)、backoff($base)(退避间隔)以及call($callable)。call()内部与注解切面共用同一套HybridRetryPolicy循环逻辑(它会构造一个兼容的ProceedingJoinPoint),因此过程式调用与注解式调用的重试语义完全一致。若未指定任何策略就调用call(),会抛出BadMethodCallException提醒你至少指定一个策略。对应测试见 RetryTest.php 与 RetryAnnotationAspectTest.php。

总结:如何为你的服务选择重试组合

回顾组件提供的全部能力,推荐按以下思路落地:

  1. 低频、幂等、可重试的异常:直接使用默认#[Retry],内置预算策略已能防雪崩;
  2. 需要精确控制次数与间隔:自定义注解,组合MaxAttemptsRetryPolicy+ClassifierRetryPolicy+SleepRetryPolicy,并用retryThrowables限定异常范围、strategy = BackoffStrategy开启退避;
  3. 对总时长敏感:加入TimeoutRetryPolicy,给整个重试会话设置硬性时间上限;
  4. 对下游崩溃敏感:加入CircuitBreakerRetryPolicy,失败后进入熔断窗口,不再空转;
  5. 必须兜底降级:配置fallback(闭包或class@method),重试耗尽后返回替代结果;
  6. 过程式场景(非注解方法、回调内等):使用Retry::whenReturns(...)->max(...)->call(...)链式调用,语义清晰且与注解行为一致。

始终记住:重试是容错的工具而非万能药,务必同时保证被调用接口的幂等性,并结合预算与熔断机制控制对下游的冲击,才能让分布式系统在故障面前既“扛得住”又“稳得住”。

  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

相关推荐

上一篇:网盘直链下载解决方案:技术原理与实践指南
下一篇:警惕!你的数字记忆正在消失:构建个人记忆安全防线

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 9:39:43

WebBatchRequest批量探测:存活判定与并发抓取实战解析

简介&#xff1a;WebBatchRequest 是一款适合网站维护、网络监控和数据分析场景的批量探测工具&#xff0c;核心作用是快速检查大量目标地址是否存活&#xff0c;并自动抓取网页标题&#xff0c;便于用户快速了解站点状态与内容主题。资源定位偏向个人学习与网络技术研究&#…

作者头像 李华
网站建设 2026/10/7 9:38:16

YOLOv11n部署RDK X5实战:移除DFL Softmax,帧率从6飙到35FPS

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 9:36:51

一把能判自己死刑的尺叫尺,一把不能判自己死刑的叫令

一把能判自己死刑的尺叫尺&#xff0c;一把不能判自己死刑的叫令摘要本文围绕核心判词“一把能判自己死刑的尺叫尺&#xff0c;一把不能的叫令”展开&#xff0c;将其升维至人类认知史、认知方法论、软件工程与学术建制权力批判的本体论终极范式。以“尺”与“令”为朴素且刚性…

作者头像 李华