05.错误处理规范

28 阅读11分钟

错误处理规范

1.什么是错误

因为某些因素,流程没有按照预期正常执行完,这些因素就称之为错误

1731476542364.png

2.代码说明错误处理

介绍

下面用了一段转账的代码,来说明进行了异常处理的代码和没有进行异常处理的代码的区别

asdasd20241113133927.png

结论

由此可以看的出来,一段健壮的代码,百分之70都是关于错误处理的,由此可见错误处理的重要

错误处理是最基础最重要的技术能力,错误处理是我们工作的日常内容

如果一个人的代码关于错误处理的逻辑很少,那这个代码基本也就不怎么样

3.错误分类/自定义异常类

问题说明

很多初级开发者习惯直接抛出RuntimeException或Exception,这会导致上层无法精准捕获。例如,你可能想捕获余额不足进行重试,结果却捕获了空指针异常

介绍

Error属于程序无法处理的错误,我们的目标更多是针对Exception做些文章

而RuntimeException是Exception中重要的一个子类,我们对于异常做的种种处理都是基于RuntimeException而来

这个分类指的是从业务上对RuntimeException进行分类

一般来说:业务异常指的是既定流程的非预期分支,系统异常指的是代码或环境的真正故障

asdasd1113134451.png

bbbbb.png

建立分层的异常体系是解耦的关键。建议定义一个全局的BaseException,然后派生出BaseBusinessException(针对业务逻辑)和BaseSystemException(针对系统故障)。这种分类使得全局异常处理器可以轻松应用不同的策略(日志,告警,重试)

日志策略

介绍

日志的价值在于信噪比,而业务异常和系统异常的日志处理是不同的

业务异常

如密码错误,库存不足是用户行为的结果,是系统功能的一部分。如果有100万用户登录,有1w人输错密码,如果你用error级别日志,就会产生1w条error级别的日志,这会淹没真正的系统异常。因此,业务异常使用warn甚至info打印日志更好

系统异常

如NPE,SQL失败,是代码bug或环境问题,必须使用error级别,以触发日志收集系统的红线告警

示例

1769679860889.png

告警处理

介绍

狼来了的故事在运维中天天上演。如果运维手机每天收到100条短信,其中99条是验证码校验失败,那么第100条数据库连接池满的真正告警一定会被忽略

解决方案
  • 策略:监控系统应配置为仅当日志出现error级别时触发事实通知
  • 趋势监控:对于业务异常,不发短信,而是记录到时序数据库监控趋势。如果登陆失败的曲线在1分钟内从100飙升到10000,这可能意味着撞库攻击,这种基于统计阈值的告警才是针对业务异常的正确处理方式

堆栈处理

介绍

在Java中,new Exception()的开销主要在于fillInStackTrace(),它需要遍历当前线程的栈帧来生成详细的调用链,对于系统异常和业务异常应该分别进行优化

解决方案
  • 业务异常:开发者非常清楚代码流程是如何走到这一步的,不需要知道这行代码是经过了多少层Spring AOP代理才调用的,不应该走默认的new Exception()逻辑,可以通过覆写fillInStackTrace()返回this,可以消除这部分开销。在高并发场景下,这能带来约10-50倍的异常实例化性能提升
  • 系统异常:开发者并不清楚如何调用的,必须要完整保留调用链,需要走默认的new Exception()逻辑

错误码隔离

介绍

错误码有两个消费者:用户/前端和开发/运维

解决方案
  • 业务错误码:比如BIZ_4001,给前端用的,前端看到这个码,知道要弹窗提示余额不足并引导充值。它应该是稳定的,语义化的
  • 系统traceId:给研发用的,当用户反馈系统出错了,由于安全原因不能把NPE at line 42这种错误信息展示给用户,而是展示一个随机生成的traceId。用户报出这个id,运维去日志系统里一搜,立刻定位到那条error日志和完整堆栈

客户端感知的透明度与安全

介绍

分离系统异常与业务异常后有利于客户管感知和安全

解决方案
  • 透明度:业务异常的消息通常是经过产品经理设计的(如:您选购的商品太火爆了),应该原样透传给前端展示
  • 安全性:系统异常的消息包含了数据库表结构信息或其它敏感信息。如果直接透传,黑客可能能利用上。因此,系统异常必须在全局异常处理器中被整容,统一返回系统繁忙或未知错误

监控指标的维度区分

介绍

在Prometheus等监控系统中,异常不应该只是一条平坦的曲线

建议在埋点时为异常指标增加exception_type标签

通过维度的区分,真正实现了技术归技术,业务归业务

解决方案
  • counter_exception_total{type="business",code="BALANCE_NOT_ENOUGH"}:这类异常指标暴涨,应该找运营或产品,分析是不是活动规则有问题
  • counter_exception_total{type="system",class="NullPointerException"}:这类指标暴涨,必须立刻叫醒技术负责人,安排Hotfix

4.其他异常处理的最佳实践

外部依赖的防腐层处理

介绍

当你的服务调用第三方接口(如支付网关,阿里云OSS)时,第三方可能会抛出各种千奇百怪的异常(SocketTimeout,UnknownHost,SdkServiceException)

解决方案

最佳实践是不要让这些底层技术异常直接穿透到你的Controller层。应该在集成层捕获这些异常,并进行翻译

  • 如果是因为网络超时:翻译为SystemRetryableException(系统异常,提示可重试)
  • 如果是因为第三方明确返回参数错误:翻译为BusinessValidationException(业务异常)

这样做的好处是,你的核心业务逻辑层只依赖自己的异常体系,不依赖具体的第三方SDK,实现了完美的解耦

示例
public void makePayment (Order order) {
    try{
        paymentSdk.pay(order);
    } catch () {
        // 翻译为系统连接异常,可能触发重试
        throw new RemoteServiceConnectException("支付服务不可用",e);
    } catch (SdkRefusalException e) {
        // 翻译为业务异常,订单置为失败
        throw new PaymentDeclinedException("支付被拒绝:" + e.getReason());
    }
}

5.错误的严重程度

1731476853567.png

6.错误的处理方式

介绍

1731476934933.png

优劣势

1731477024003.png

推荐用啥

1731477216374.png

7.异常码/错误码

HTTP状态码的问题

RESTful风格曾倡导使用HTTP标准状态码(400,401,403,404,500)来表示结果,但在复杂的业务场景下,这种粗颗粒度表达存在致命缺陷:

  • 业务语义缺失:HTTP400统称Bad Request,但无法区分余额不足,库存超卖,优惠券不满足门槛还是账号被风控。前端无法根据400做差异化表达和引导
  • 微服务定责模糊:当网关返回500时,是网关挂了吗?还是订单服务挂了?还是订单服务调用的数据库挂了?HTTP状态码无法透传故障源
  • 多语言与文案僵化:HTTP协议标准不包含业务文案的多语言处理机制

核心结论:HTTP状态码用于标识网络与协议层面的通信结果,而错误码用于标识业务逻辑的处理结果。两者是正交关系,必须解耦

介绍

业界并没有统一的标准,翻看腾讯、谷歌、阿里、亚马逊以及FaceBook等网站,也没有相似之处

这里我们按照Java开发手册(嵩山版)中给出的异常码规定方式。我们参考阿里巴巴开发手册如何规定异常码,给出如下方案

核心价值

  • 研发视角(可观察性):错误码是监控服务的核心指标。A类错误突增代表运营活动火爆或遭攻击,B类错误徒增代表代码上线除了bug,C类错误码代表依赖的云服务不稳定
  • 前端视角(交互控制):错误码是UI交互的指挥棒。10001弹窗提示,10002跳转登录,10003开启倒计时重试
  • 客服/运营视角(沟通语言):用户截图错误码C0412,客服一查知识库便知是银行支付通道拥堵,能立即给出标准话术,无需升级给技术排查

设计目标

1731477491340.png

核心组成

  • Type:1位,代表错误来源
  • Domain:2位,代表服务/限界上下文/业务线,帮助快速定位那个产品那个系统那个服务出事情了
  • Case:3位或4位,代表具体错误编号,标识具体的业务场景

详解三部分

Type
  • A:用户端错误
    • 定义:用户发起的请求有问题,系统逻辑无误。如:参数必填项为空、格式错误、未登录、余额不足、版本过低
    • 策略:不报警(或仅在监控大盘展示趋势)。这是正常业务逻辑的一部分
    • 处理:前端提示
  • B:系统执行错误
    • 定义:用户请求正确,但系统内部崩了。如:NullPointerException,数组越界,数据库死锁,连接池耗尽,配置加载失败
    • 策略:P0/P1级核心报警。这意味着代码质量或基础设施故障,研发必须立刻介入
  • C:第三方调用错误
    • 定义:系统本身错误,但依赖下游(RPC服务)或外部API(微信支付,阿里云短信)报错或超时
    • 策略:关注级报警。通常结合熔断与降级机制处理。运维人员需要确认是网络抖动还是服务商故障
Domain
  • 00:通用/基础架构
  • 01:用户中心
  • 02:商品中心
  • 03:订单中心
  • 04:交易/支付
  • 05:优惠券/营销

扩容建议:如果微服务超过99个,可扩展为3位,或复用数字(大部门下设子段)

Case
  • 001~099:该服务内的通用错误(如:用户中心-通用参数错误)
  • 100~999:具体场景错误(如:用户中心-手机号注册-格式错误)

完整示例和解析

1769687147580.png

为什么需要配置中心

错误码被统一管理后,如果需要更新,需要重新编译,打包,等等。很麻烦

建议用配置中心统一管理并实现快速更新,这样就能实现便于修改

响应体设计

介绍

在企业级实战中,推荐使用Always 200策略(即HTTP Status始终为200,业务成功与否看Body里的code)

原因
  1. 网关友好:Nginx,WAF,CDN等基础设施对4xx/5xx有默认的拦截或限流策略。如果业务大量返回404,可能会误触网关的防御机制
  2. 前端友好:前端框架处理HTTP错误和处理业务逻辑错误是两套机制。Always 200让前端只需要在response.data中统一拦截
  3. 穿透性:在微服务调用链中,HTTP200更容易携带完整的JSON错误体层层透传回最上层,而HTTP错误码容易被中间层框架吞没或改写
标准的响应体

一个优秀的响应体,应兼顾机器可读与人可读,并严格区分用户看和开发看的信息

{
	// 业务是否成功
    "success": false,
    // 状态码
    "code": "B03102",
    // 面向用户提示信息
    "userMsg":"订单创建失败,系统拥堵,请稍后重试",
    // 面向开发者,生产环境通过配置开关屏蔽此字段,或进行脱敏,防止出现安全问题
    "devMsg":"错误信息,帮助开发排查问题",
    // 面向前端,比如跳转到登录页,立刻重试,5秒后重试
    "solution":"RETRY",
    // traceId,无论成功或失败都必须返回,帮助排查问题
    "traceId":"xxxxx"
}

治理策略

介绍

随着业务迭代,错误码会爆炸式增长,甚至出现同一个错误定义了两个码或含义不明的码。必须建立治理机制

错误码中心

1769688659632.png

监控与告警策略

1769688686003.png