3. 常见异常类型详解:超时异常、业务异常、系统异常、网络异常

异常分类这件事,说实话不算难。但很多团队踩坑就踩在“分不清”上。

我见过一个项目,把所有异常都扔进同一个 catch 里打日志,结果线上出了问题,排查了整整两天才找到根因——其实就是个简单的超时。后来我每带一个项目,第一件事就是让团队把异常分类先定清楚。

今天咱们就聊聊最常见的四类异常:超时异常、业务异常、系统异常、网络异常。这四种几乎涵盖了 90% 的线上问题。

3.1 超时异常

超时,说白了就是“我等得不耐烦了”。调用方设定了一个等待上限,被调用方在规定时间内没回应,那就抛个异常出来。

核心判断依据:请求在预期时间内未收到响应,而非响应内容错误。

常见场景

  • 数据库连接池耗尽,查询卡死
  • 外部 API 响应慢(比如支付通道、短信网关)
  • 分布式 RPC 调用链路超时
  • 线程池任务排队过长,任务被丢弃或超时

我个人习惯在超时异常里加个超时时间戳字段,方便定位是哪一步慢了。你想想看,有时候不是整个请求慢,而是某一段调用出了问题。没有时间戳,你根本不知道哪个环节超了。

import requests
from requests.exceptions import Timeout

try:
    resp = requests.get('https://api.example.com/data', timeout=3)
except Timeout:
    # 我一般会在这里打一个详细的告警,带上当前系统时间戳
    log.error('调用外部API超时,timeout=3s')
    raise TimeoutError('调用外部API超时')
避坑指南:我曾经设置过 timeout=0.5 秒,结果线上高峰期频繁超时。后来把超时时间调到 2 秒,再配合熔断和重试,才稳定下来。不要拍脑袋设超时,先做压测。

3.2 业务异常

业务异常不是系统故障,而是逻辑上的不合规。比如余额不足、库存为负、用户没有权限——这些都不是程序写错了,而是业务规则被违反了。

关键区别:业务异常是有“约定”的,属于预期内的异常。它不该导致系统 crash,而是应该返回明确的业务错误码。

常见场景

  • 下单时库存不足
  • 用户重复提交(幂等校验)
  • 参数校验失败(比如年龄为负数)
  • 流程状态不允许(如已取消的订单不允许再次支付)

我建议把业务异常单独定义成一个类,比如 BusinessException,里面带 errorCode 和 errorMessage。这样上层处理时,可以统一转成标准 API 响应,而不是跟系统异常混在一起。

class BusinessException(Exception):
    def __init__(self, error_code: int, message: str):
        self.error_code = error_code
        self.message = message
        super().__init__(f'[{error_code}] {message}')

# 使用示例
raise BusinessException(1001, '库存不足,当前可用库存为0')
注意:不要把敏感信息(比如数据库结构、内部 IP)放在业务异常的消息里,容易造成信息泄露。曾经有团队在报“订单创建失败”时直接把 SQL 语句带上了,后果很严重。

3.3 系统异常

系统异常是 “真的出大事了”。内存溢出、空指针、线程死锁、磁盘写满……这些往往意味着代码 bug 或资源耗尽。

处理原则:系统异常应该立即告警,并且优先熔断或降级,避免雪崩。不要尝试重试,因为大概率重试也没用。

常见场景

  • OutOfMemoryError(内存泄漏)
  • NullPointerException / IndexOutOfBoundsException
  • 数据库连接池满,新的请求无法获取连接
  • 文件系统只读或磁盘空间不足

嗯,这里要注意,系统异常的日志一定要打全堆栈信息。我记得有一次线上 OOM,就是因为堆栈被日志框架截断了,排查时只看到“OutOfMemoryError”,但根本不知道是哪个对象撑爆的。后来统一配置了 -XX:+HeapDumpOnOutOfMemoryError 才解决。

try:
    # 可能抛出系统异常的操作
    data = [1] * 10**9  # 模拟大对象分配
except MemoryError as e:
    # 一定要打完整堆栈,并且立刻告警
    log.critical('系统内存不足', exc_info=True)
    # 这里可以做熔断,比如停止接受新请求
    raise SystemException('内存资源耗尽', cause=e)
个人经验:系统异常最好跟业务异常分开报警级别。业务异常可以只记 info 日志 + 错误码返回;系统异常必须发短信/钉钉/电话告警,并且标记为 P0 级别。

3.4 网络异常

网络异常是最“无厘头”的一类。你代码写得再漂亮,网络一扔包、DNS 解析失败、SSL 握手超时,照样挂了。

难点在于:网络异常的复现成本很高,往往与环境、机房、运营商强相关。所以检测网络异常时,超时 + 重试 + 熔断是标配。

常见场景

  • DNS 解析失败
  • TCP 连接被重置(connection reset by peer)
  • SSL/TLS 证书过期或握手失败
  • 防火墙拦截导致包丢失
  • 服务端主动断开连接(比如健康检查失败后关闭连接)

我曾经遇到过一个非常隐蔽的问题:某个第三方服务每周五下午都会断连几十秒,原因是他们自己的运维在做网络割接。我们没做熔断,结果整个调用链全部超时,最终影响了线上用户。从那之后,我在网络相关的调用上都加了滑动窗口熔断逻辑。

import socket
import ssl

def fetch_https(url):
    try:
        ctx = ssl.create_default_context()
        with socket.create_connection(('api.example.com', 443), timeout=5) as sock:
            with ctx.wrap_socket(sock, server_hostname='api.example.com') as ssock:
                ssock.sendall(b'GET /data HTTP/1.1\r\nHost: ...\r\n\r\n')
                response = ssock.recv(4096)
    except (socket.timeout, ssl.SSLError, ConnectionResetError) as e:
        # 网络异常统一处理,建议先记录网络抖动次数
        log.warning(f'网络异常: {type(e).__name__} {e}')
        raise NetworkException('网络连接失败', cause=e)

四种异常对比

为了让你更直观地对比,我整理了一张表。实际开发中,我经常用这张表来教新同学区分异常。

异常类型触发原因典型特征处理方式
超时异常响应时间超过阈值无返回 / 超时标志重试(带退避)、熔断、超时升级
业务异常业务规则不符明确错误码、错误消息返回标准错误响应,不重试
系统异常资源耗竭 / 代码 bug堆栈异常、无法恢复立即告警、熔断、人工介入
网络异常网络不可达、连接重置主机/端口不可达、连接断开重试 + 限流 + 熔断

四种异常的知识结构图

下面这张图把四种异常的核心特征和检测关注点串起来了。你可以把它作为团队内部培训的参考。

常见异常类型分类与检测关注点 异常分类 超时异常 响应超阈值 → 重试+熔断 业务异常 规则不满足 → 返回错误码 系统异常 资源/代码bug → 告警+熔断 网络异常 不可达/重置 → 重试+限流 提示:四种异常的处理方式不同,实际检测时需区分报警级别

好了,以上就是四种常见异常类型的详细讲解。我个人做异常检测时,始终遵循一个原则:先分类、再定级、最后处理。不要把系统异常当成业务异常来处理,也不要把网络异常当成超时异常来重试——分类对了,后面的检测和告警才能精准。