Skip to content

弹性 AOP:@Retry / @CircuitBreaker / @Timed

外部 HTTP 调用、数据库操作等场景天生不稳定。ACE Framework 提供三个弹性切面宏,无需修改业务逻辑即可叠加重试、熔断和计时能力。

@Retry 自动重试

@Retry[n] 在方法抛出异常时自动重试,最多重试 n 次(不含首次调用)。若所有尝试均失败,最后一次异常原样抛出给调用方。

cangjie
package demo.service

import ace.framework.*

@Service
class PaymentClient {

    // 最多重试 3 次,共尝试 4 次
    @Retry[3]
    func charge(orderId: String, amount: Float64): Bool {
        // 调用支付网关,可能因网络抖动失败
        return paymentGateway.charge(orderId, amount)
    }
}

重试行为

调用次数结果框架行为
第 1 次(原始调用)异常等待后重试
第 2–4 次(重试 1–3)异常继续重试 / 最终抛出
任意次成功正常返回立即返回,停止重试

重试间隔

当前版本采用立即重试(无退避延迟)。若需指数退避,请在方法体内手动 Thread.sleep,或等待后续 @Retry[n, backoff: "exponential"] 支持。

@CircuitBreaker 熔断器

@CircuitBreaker[n] 在连续 n 次失败后触发熔断。熔断期间直接抛出 503 错误,不执行方法体,从而保护下游服务和本机资源。经过冷却期后自动进入半开状态,尝试恢复。

cangjie
@Service
class InventoryClient {

    // 连续 5 次失败后熔断
    @CircuitBreaker[5]
    func queryStock(skuId: String): Int64 {
        return inventoryApi.query(skuId)
    }
}

熔断器状态机

  正常调用


  [CLOSED] ──连续失败 n 次──► [OPEN] ──冷却期到──► [HALF_OPEN]
     ▲                                                   │
     └──────────── 调用成功 ─────────────────────────────┘

                                    调用失败 → 重回 [OPEN]
状态行为
CLOSED(正常)正常执行方法,统计失败次数
OPEN(熔断)直接抛出 503,不调用方法体
HALF_OPEN(半开)放行一次试探调用,成功则关闭,失败则重新熔断

@Timed 精确计时

@Timed 记录方法的调用次数、平均耗时和最大耗时,通过 aceTimedStats("方法名") 读取统计数据。

cangjie
@Service
class SearchService {

    @Timed
    func search(keyword: String): Array<String> {
        return searchEngine.query(keyword)
    }
}
cangjie
// 读取统计数据:(count, avgMs, maxMs)
let (count, avgMs, maxMs) = aceTimedStats("search")
println("调用次数: ${count}, 平均耗时: ${avgMs}ms, 最大耗时: ${maxMs}ms")

aceTimedStats API

参数说明
"methodName"方法名(字符串字面量),与被 @Timed 标注的方法名一致

返回值类型为 (Int64, Float64, Float64),分别对应 (调用次数, 平均耗时ms, 最大耗时ms)

组合使用:重试 + 熔断 + 计时

三个宏可叠加,执行顺序为 @Timed@CircuitBreaker@Retry → 方法体(由外到内)。

cangjie
package demo.service

import ace.framework.*

@Service
class ExternalHttpService {

    @Timed
    @CircuitBreaker[5]   // 连续 5 次失败后熔断
    @Retry[2]            // 每次请求失败最多重试 2 次
    func callUpstream(endpoint: String): String {
        let resp = httpClient.get(endpoint)
        if (resp.statusCode >= 500) {
            throw Exception("上游服务错误: ${resp.statusCode}")
        }
        return resp.body
    }
}
cangjie
// Controller 中使用
@Controller["/api"]
class ProxyController {
    @Inject
    var svc!: ExternalHttpService

    @Get["/proxy"]
    func proxy(): String {
        try {
            return svc.callUpstream("https://upstream.example.com/data")
        } catch (e: CircuitBreakerOpenException) {
            // 熔断期间的降级响应
            return """{"code":503,"msg":"服务暂时不可用,请稍后重试"}"""
        }
    }
}

组合执行逻辑说明

  1. @Timed 最外层,无论成功失败都记录耗时。
  2. @CircuitBreaker 若处于 OPEN 状态,直接抛出 CircuitBreakerOpenException@Retry不会重试熔断异常(避免无效重试加剧雪崩)。
  3. @Retry 只重试方法体本身抛出的业务异常。

注意事项

@Retry 与幂等性

重试意味着方法可能被多次执行。务必确保被 @Retry 标注的方法是幂等的(如 GET 查询、带幂等键的写操作),避免重复扣款、重复入库等副作用。

@CircuitBreaker 不跨实例共享

熔断器状态存储在单进程内存中,多实例部署时各节点独立计数。若需集群级熔断,请在业务层结合分布式计数器(Redis)实现,或使用服务网格(Envoy/Istio)的熔断能力。

监控集成

aceTimedStats 数据可在 /actuator/metrics 端点聚合输出(需引入 ace-actuator 模块)。生产环境建议将 @Timed 数据推送到 Prometheus,配合 Grafana 可视化。

基于 Apache-2.0 许可证发布