1. 异常与日志体系:后端稳定性的基石
第一次线上事故让我记忆犹新——凌晨三点被报警电话惊醒,发现核心服务不可用却无从排查。那次教训让我明白:完善的异常与日志体系不是可选项,而是后端服务的生命线。这套体系就像飞机的黑匣子,平时默默记录,关键时刻却能救命。
现代后端系统面临三大稳定性挑战:复杂业务逻辑中的隐蔽错误、高并发下的性能瓶颈、分布式环境中的不可靠因素。良好的异常处理能主动防御前两类问题,而健全的日志体系则是解决第三类问题的钥匙。两者配合形成防御-诊断闭环,这正是我们称其为"第一道防线"的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 异常处理体系设计精要
2.1 异常分类与分级策略
异常处理的首要原则是:不是所有错误都值得同等级别的关注。我将异常划分为四个等级:
- 致命异常(FATAL):导致服务完全不可用,如数据库连接池耗尽。需要立即人工干预,触发电话告警。
- 严重异常(ERROR):影响核心业务流程但服务仍部分可用,如支付失败。触发企业IM告警,30分钟内需响应。
- 普通异常(WARN):需要关注但可自动恢复的问题,如缓存穿透。记录日志并纳入日常巡检。
- 调试信息(INFO/DEBUG):用于问题定位的辅助信息,如方法入参出参。
在Java中,我推荐使用自定义异常类来实现分级:
java复制public class BusinessException extends RuntimeException {
private ErrorLevel level; // 异常等级枚举
private String errorCode; // 业务错误码
// 构造方法省略...
}
2.2 全局异常处理最佳实践
Spring Boot的@ControllerAdvice是构建统一异常处理器的利器,但要注意这些细节:
- 响应标准化:所有异常返回统一的JSON结构:
json复制{
"success": false,
"code": "PAYMENT_FAILED",
"message": "支付金额超过限额",
"timestamp": 1672531200000
}
-
异常转换:将底层异常转化为业务异常,避免暴露系统细节。例如将SQLException转换为"系统繁忙,请稍后重试"。
-
上下文保留:在转换异常时,务必保留原始异常链(cause chain),这对排查根因至关重要。
关键技巧:为每个异常分配唯一错误码,建立错误码字典。这样前端可以根据错误码展示定制化提示,而日志系统可以通过错误码快速归类问题。
3. 日志体系构建实战
3.1 日志框架选型与配置
当前主流选择是SLF4J + Logback组合。以下是我的生产级配置要点:
- 滚动策略:按日期和大小双维度滚动,避免单个日志文件过大
xml复制<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
- 异步日志:使用AsyncAppender提升性能,但要注意队列大小和丢弃策略
xml复制<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE" />
</appender>
- 敏感信息过滤:添加自定义过滤器脱敏手机号、身份证号等
java复制public class SensitiveDataFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
String maskedMessage = event.getMessage()
.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
((LoggingEvent)event).setMessage(maskedMessage);
return FilterReply.NEUTRAL;
}
}
3.2 结构化日志与追踪体系
现代日志系统的核心是结构化日志和全链路追踪。我采用以下方案:
- JSON格式输出:便于ELK等系统解析
xml复制<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"order-service","env":"${spring.profiles.active}"}</customFields>
</encoder>
- MDC(Mapped Diagnostic Context):在全链路中传递追踪ID
java复制// 在过滤器或拦截器中设置追踪ID
MDC.put("traceId", UUID.randomUUID().toString());
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
- 业务染色日志:为关键业务(如订单创建)添加特殊标记,便于单独分析
4. 异常监控与智能告警
4.1 指标采集与可视化
我通常组合使用以下工具搭建监控体系:
| 工具类型 | 推荐方案 | 采集频率 | 关键指标 |
|---|---|---|---|
| 时序数据库 | Prometheus | 15s | 异常次数、异常类型分布、P99延迟 |
| 日志分析 | ELK Stack | 实时 | 错误日志聚类、异常模式识别 |
| 全链路追踪 | SkyWalking/Jaeger | 采样 | 异常传播路径、跨服务错误关联 |
| 业务指标 | 自定义埋点+InfluxDB | 按需 | 关键业务流程异常率、转化漏斗 |
4.2 智能告警规则设计
避免告警风暴的关键是分层分级策略:
-
基础层:系统指标异常
- 规则示例:
最近5分钟FATAL日志次数 > 3 - 动作:触发电话告警
- 规则示例:
-
业务层:核心流程异常
- 规则示例:
支付成功率同比昨日下降20% - 动作:企业IM通知+自动创建工单
- 规则示例:
-
预测层:异常趋势预警
- 规则示例:
数据库连接池使用率连续1小时>80% - 动作:邮件周知+自动扩容评估
- 规则示例:
血泪教训:曾经因为未设置磁盘空间告警,导致日志写满磁盘引发服务崩溃。现在我的必检清单包含:磁盘空间、线程池状态、DB连接池、GC频率。
5. 典型异常场景处理实录
5.1 并发场景下的异常处理
场景:秒杀活动中超卖问题
解决方案:
- 乐观锁+重试机制
java复制@Transactional
public void deductStock(Long itemId, int num) {
int retryTimes = 0;
while(retryTimes < MAX_RETRY) {
Item item = itemDao.selectForUpdate(itemId);
if (item.getStock() >= num) {
item.setStock(item.getStock() - num);
if (itemDao.updateWithVersion(item) > 0) {
return; // 更新成功
}
} else {
throw new BusinessException("库存不足", STOCK_NOT_ENOUGH);
}
retryTimes++;
Thread.sleep(50); // 指数退避更佳
}
throw new BusinessException("系统繁忙,请重试", SYSTEM_BUSY);
}
- 本地缓存+预扣减
java复制// 使用Guava的LoadingCache
private LoadingCache<Long, AtomicInteger> localStockCache =
CacheBuilder.newBuilder()
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicInteger>() {
@Override
public AtomicInteger load(Long itemId) {
return new AtomicInteger(queryDBStock(itemId));
}
});
public boolean tryDeduct(Long itemId, int num) {
AtomicInteger stock = localStockCache.get(itemId);
while (true) {
int current = stock.get();
if (current < num) return false;
if (stock.compareAndSet(current, current - num)) {
asyncUpdateDB(itemId, num); // 异步更新数据库
return true;
}
}
}
5.2 分布式事务异常处理
场景:订单创建后扣减库存失败
解决方案:SAGA模式+补偿机制
- 定义正向操作和补偿操作
java复制public interface SagaAction {
void execute();
void compensate();
}
public class DeductStockAction implements SagaAction {
@Override
public void execute() {
if (!stockService.deduct(itemId, amount)) {
throw new SagaException("扣减库存失败");
}
}
@Override
public void compensate() {
stockService.add(itemId, amount); // 补偿增加库存
}
}
- 实现SAGA协调器
java复制public class SagaCoordinator {
private List<SagaAction> actions = new ArrayList<>();
public void addAction(SagaAction action) {
actions.add(action);
}
public void execute() {
LinkedList<SagaAction> compensated = new LinkedList<>();
try {
for (SagaAction action : actions) {
action.execute();
compensated.push(action);
}
} catch (Exception e) {
for (SagaAction action : compensated) {
try {
action.compensate();
} catch (Exception ex) {
log.error("补偿操作失败", ex);
// 记录补偿失败,需要人工干预
alertService.notifyManualCompensation(action);
}
}
throw e;
}
}
}
6. 日志分析进阶技巧
6.1 异常根因分析
当面对海量日志时,我常用以下方法快速定位问题:
-
时间线分析法:在ELK中按时间轴排列相关服务的日志,观察异常传播路径
-
异常指纹比对:计算日志内容的相似度哈希,聚类相似异常
python复制# 使用SimHash算法生成日志指纹
def simhash(log_text):
tokens = jieba.cut(log_text)
vec = [0] * 64
for token in tokens:
hash_val = bin(hash(token))[-64:] # 取64位hash
for i in range(64):
vec[i] += 1 if hash_val[i] == '1' else -1
return ''.join(['1' if x > 0 else '0' for x in vec])
- 调用链追踪:结合TraceID重建完整请求上下文
6.2 日志容量规划
为避免日志撑爆磁盘,需要科学计算日志保留策略:
code复制所需存储空间 = 单条日志平均大小 × 每秒日志量 × 86400 × 保留天数 × 压缩比
示例计算:
- 单条日志:2KB
- QPS:1000
- 保留30天
- Snappy压缩比:0.25
code复制2KB × 1000 × 86400 × 30 × 0.25 ≈ 1.2TB
建议采用分层存储策略:
- 热数据(7天):高性能SSD
- 温数据(30天):标准HDD
- 冷数据(1年):对象存储(如S3)
7. 生产环境血泪教训
-
日志异步写入的陷阱:曾经因为异步队列满导致日志丢失,关键故障无法追溯。现在我的配置铁律:
- 队列大小不超过内存的1%
- 设置合理的discardingThreshold(建议20%)
- 关键日志(如支付成功)同步写入
-
过度日志化的代价:某次将DEBUG日志误开上生产,导致:
- 磁盘IOPS飙升至极限
- 日志收集系统过载
- 实际业务请求被阻塞
现在严格执行日志级别动态调整机制:
java复制// 通过配置中心动态调整日志级别 @RefreshScope @Configuration public class LogLevelConfig { @Value("${log.level.root:INFO}") public void setRootLevel(String level) { ((ch.qos.logback.classic.Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)) .setLevel(Level.valueOf(level)); } } -
异常处理的反模式:
- 捕获异常后不做任何处理(空的catch块)
- 过度宽泛的异常捕获(catch Throwable)
- 在循环内创建异常实例(导致GC压力)
我的异常处理检查清单:
- [ ] 每个catch块至少记录日志
- [ ] 只捕获预期中的异常类型
- [ ] 避免在性能热点路径抛出异常
- [ ] 自定义异常保持轻量(不包含复杂业务对象)
