本文已收录在Github关注我,紧跟本系列专栏文章,咱们下篇再续!

  • 🚀 魔都架构师 | 全网30W技术追随者
  • 🔧 大厂分布式系统/数据中台实战专家
  • 🏆 主导交易系统百万级流量调优 & 车联网平台架构
  • 🧠 AIGC应用开发先行者 | 区块链落地实践者
  • 🌍 以技术驱动创新,我们的征途是改变世界!
  • 👉 实战干货:编程严选网

1 fail-fast(快速失败):出错要趁早,别犹豫!

1.1 定义

快速失败。设计一个模块(可以是一个函数,一个类甚至一个服务)时,若有某种条件会导致模块无法正常运行,就该让模块立刻终止(可return或抛RuntimeException)。

1.2 好处

  • 及早终止,防止错误被带到下游,导致下游服务崩溃或产生脏数据
  • 便于排查,fail-fast一般都有类似断言机制判定某种条件,在断言处终止可方便定位问题,而若错误被带到下游,难定位问题根源

1.3 实战

1.3.1 微服务启动健康检查

场景: 一个订单服务依赖用户服务和商品服务。

Fail-Fast实践: 订单服务启动时,会尝试Ping一下用户服务和商品服务的健康检查接口或检查必要的数据库连接、消息队列连接是否正常。若任一关键依赖在启动时不可用,订单服务会选择启动失败并退出。

落地价值: 在k8s容器编排环境中,启动失败的Pod会被自动重启或标记为不健康,流量不会导入,避免服务“带病上岗”,保证了整体系统的稳定性。运维人员也能立刻收到告警,知道是订单服务启动依赖出了问题。

1.3.2 API接口参数校验

场景: 一个用户注册接口,需要接收用户名、密码、邮箱等参数。
Fail-Fast实践: 在接口的最前端(比如Controller层或API Gateway层),对传入的参数进行严格校验。例如,用户名是否为空、长度是否符合要求、邮箱格式是否正确、密码强度是否达标等。一旦发现任何一个校验不通过,立即返回明确的错误码和错误信息给调用方(比如 HTTP 400 Bad Request),根本不会进入后续的业务逻辑处理。

// 伪代码示例 - Spring Boot Controller
@PostMapping("/users/register")
public ResponseEntity<Object> registerUser(@Valid @RequestBody UserRegistrationDto registrationDto) {
    // 如果@Valid校验失败,Spring会自动抛出MethodArgumentNotValidException
    // 全局异常处理器会捕获这个异常,并返回一个HTTP 400响应
    // 这就是一种Fail-Fast的体现,在业务逻辑执行前就失败了

    // 假设我们还需要检查用户名是否已存在,这通常需要查询数据库
    if (userService.isUsernameTaken(registrationDto.getUsername())) {
        // 明确地快速失败,而不是继续尝试创建
        return ResponseEntity.badRequest().body("Username already taken.");
    }
    
    userService.createUser(registrationDto);
    return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully.");
}

落地价值: 避免了无效数据进入核心业务逻辑,减少了不必要的计算和数据库操作,提升了系统效率和数据质量。同时,清晰的错误提示也方便了前端或调用方进行调试。

1.3.3 关键配置加载

场景: 服务启动时需要加载一些关键配置,如第三方服务的API Key或加密算法的密钥。

Fail-Fast实践: 在服务初始化阶段,如果这些关键配置缺失或格式不正确,服务应该立即抛出致命错误并停止启动。

落地价值: 防止服务在缺少必要“武器”的情况下运行,导致后续所有依赖这些配置的功能全部失效,产生大量运行时错误。

一句话总结:“我不行,别指望我,赶紧处理我这个环节的问题!”

2 fail-safe(安全失败):留得青山在,不怕没柴烧!

2.1 定义

与fail-fast相反,若模块遇到某种错误,不该让程序失败,而是采取某种降级策略,尽量往下走。

餐厅假设主菜“清蒸澳洲大龙虾”没问题,但配菜里的“特定产地有机小番茄”今天没货了。

这时候,主厨可能会想: “嗯,主菜是核心,不能少。小番茄只是点缀,影响不大。我可以用另一种品质类似的小番茄替代,或者干脆这道菜就不加小番茄了,但主菜的味道和品质必须保证。”

这就是 Fail-Safe 的思想。

2.2 适用场景

主模块内的分支流程,通常用在系统的非核心功能或辅助流程。如果这些模块出错了,我们不希望它“一粒老鼠屎坏了一锅粥”,把整个主流程都给搞垮了。

Feed流是核心,广告是非核心。广告服务挂了,Feed流也挂了,用户肯定不答应。这时候,广告模块就必须Fail-Safe。

2.3 实战

2.3.1 Feed流

Feed流是核心,广告是非核心。广告服务挂了,Feed流也挂了,用户肯定不答应。这时候,广告模块就必须Fail-Safe。

调用广告服务时,用 try-catch 块包起来。若出现异常(如连接超时、服务返回错误码),就在 catch 块里记录一条日志,然后跳过广告插入的逻辑,继续返回正常的Feed流。用户可能只是少看了一个广告,但主功能不受影响。

有接口主要返回feed流,但有个分支逻辑是调用广告服务插入广告类型的feed,如果广告服务不可用,我的整体逻辑是不能挂的,因为不插入广告也没关系,但正常feed流还是需要返回。

所以对广告服务的调用要能降级,可try-catch住,若调用异常就不插入广告。不能因为一个分支逻辑而把主要逻辑整挂了。

2.3.2 用户个性化推荐降级

电商首页的商品推荐,优先展示“猜你喜欢”的个性化推荐商品。

Fail-Safe实践:调用个性化推荐服务时,设置合理的超时时间。如果推荐服务超时或返回错误,系统不会卡死或报错,而是会优雅地降级到显示“热门商品”、“新品上架”或默认的通用推荐列表。

// 伪代码示例
public List<Product> getHomepageRecommendations(User user) {
    List<Product> recommendations;
    try {
        // 设置超时调用个性化推荐服务
        recommendations = personalizedRecService.getRecommendations(user.getId(), 500); // 500ms超时
    } catch (TimeoutException | RecommendationServiceException e) {
        log.warn("Personalized recommendation service failed or timed out for user {}. Falling back to general recommendations.", user.getId(), e);
        // 降级策略:获取热门商品
        recommendations = generalRecService.getHotProducts();
    }

    if (recommendations == null || recommendations.isEmpty()) {
        // 再次降级:如果热门商品也没有,返回一个预设的默认列表
        recommendations = defaultRecService.getDefaultProducts();
    }
    return recommendations;
}

落地价值:保证了核心页面的可用性。用户体验有所下降(看不到最精准的推荐),但基本功能(浏览商品)得以保留。这比整个页面打不开要好太多。

2.3.3 非核心功能的数据统计

  • 场景: 用户完成某个操作后,需要异步发送一个事件到数据分析系统,用于行为统计。
  • Fail-Safe实践: 发送这个统计事件的操作可以放在 try-catch 中。如果消息队列满了,或者数据分析系统接口暂时不可用,捕获异常后,可以选择性地将事件暂存到本地磁盘稍后重试,或者干脆就丢弃这次事件(如果业务允许丢失少量统计数据),但绝不能影响用户当前操作的成功完成。
  • 落地价值: 核心业务流程顺畅,用户操作不受影响。数据统计的短暂失败可以容忍,或者通过后续补偿机制来弥补。

2.3.4 加载用户头像或可选的附加信息

  • 场景: 显示用户列表,每个用户有昵称(核心)和头像(非核心)。
  • Fail-Safe实践: 若某用户的头像URL加载失败或图片损坏,系统应显示一个默认的占位头像,而非让整个用户列表都加载不出来或在那位置显示一个刺眼红叉。
  • 落地价值: 提升了页面的健壮性和视觉体验的平滑性。

Fail-Safe 的精髓在于“容错”和“降级”,确保主要矛盾得到解决,次要问题不干扰大局。

3 Fail-Fast vs Fail-Safe

Q:到底该用谁?

A:情况!它们不是互斥的,而是根据场景和模块的重要性来决定的“组合拳”:

  • 对核心流程、关键依赖、写操作、事务性操作、启动检查:倾向于 Fail-Fast

    • 如支付流程中,扣款服务失败,你总不能告诉用户“支付成功”吧?须Fail-Fast,回滚操作,明确告知用户失败
    • 服务启动时连不上数据库,跑起来也是废柴,不如Fail-Fast
  • 对非核心流程、辅助功能、读操作(且有替代方案)、可降级的功能:倾向于 Fail-Safe

    • 如广告、个性化推荐、用户头像、日志记录等。这些功能出问题,用户体验会打点折扣,但核心功能还能用。

甚至,Fail-Fast 和 Fail-Safe 可在一个流程的不同阶段并存:

想象一个复杂的订单创建流程:

  1. 参数校验(Fail-Fast): 用户ID、商品ID、数量等参数是否合法?不合法直接拒绝。
  2. 库存检查(Fail-Fast): 商品库存是否足够?不足就明确告知用户无法下单。
  3. 价格计算(Fail-Fast): 价格服务是否正常?能否正确计算出订单金额?不行就中断。
  4. 优惠券应用(Fail-Safe): 尝试调用优惠券服务应用优惠。如果优惠券服务超时或不可用,可以降级为不使用优惠券(或者给个默认的小折扣),但订单依然可以继续创建(需要业务决策是否允许)。
  5. 订单持久化(Fail-Fast): 将订单信息写入数据库。如果写入失败,整个订单创建失败,需要回滚。
  6. 发送订单创建成功短信/邮件通知(Fail-Safe): 订单已经成功创建了。此时如果短信服务或邮件服务暂时故障,记录下来,后续重试,不应该影响订单创建的最终成功状态。

4 总结

Fail-Fast(快速失败): 像个严格的门卫,不符合条件,门儿都没有!目的是尽早暴露问题,防止问题蔓延,方便定位。适用于系统关键路径和依赖。

Fail-Safe(安全失败): 像个经验丰富的老管家,家里某个小摆件坏了,他会想办法不让它影响整体的宴会进行。目的是在部分功能异常时,保证核心功能可用,通过降级、容错来提升用户体验和系统韧性。适用于非核心、可降级的功能。

理解并灵活运用这两个原则,能让我们的分布式系统更加健壮、可靠,也能让我们在面对线上问题时更加从容。

本文由博客一文多发平台 OpenWrite 发布!

JavaEdge
374 声望417 粉丝