在Web应用开发中,操作日志是系统审计和故障排查的重要凭证。然而,如果在主业务请求的链路中同步执行数据库插入等耗时操作,往往会严重拖慢接口的响应速度。本文将结合一个真实的AOP切面场景,详细讲解如何通过Spring Boot将日志写入改为异步,并附带代码层面的深度对比与优化建议。
目录
[1. 配置专属日志线程池](#1. 配置专属日志线程池)
[2. 封装异步保存日志逻辑](#2. 封装异步保存日志逻辑)
[3. 重构AOP切面(改动后核心代码)](#3. 重构AOP切面(改动后核心代码))
一.痛点分析:改动前的"阻塞式"日志
在传统的实现方式中,开发者通常会在AOP切面的 finally 块中直接调用 Mapper 层进行日志落库。这种方式虽然逻辑简单,但在高并发或数据库网络延迟的场景下,会导致前端用户长时间等待。
改动前核心代码(同步阻塞)
java
@Aspect
@Component
public class BomisLogAspect {
@Autowired
private BoFrontMapper boFrontMapper; // 直接注入Mapper
@Around("@annotation(bomisLog)")
public Object around(ProceedingJoinPoint joinPoint, BomisLog bomisLog) throws Throwable {
// ... 省略参数解析逻辑 ...
try {
Object result = joinPoint.proceed();
return result;
} finally {
// 【致命缺陷】在主线程中同步执行DB插入,必须等待SQL执行完毕才能返回前端
Map<String, Object> map = new HashMap<>();
// ... 组装Map数据 ...
boFrontMapper.addOneInteractFlow(map);
}
}
}
问题总结: 主业务流程被日志记录强行绑定,不仅拉低了接口TPS(吞吐量),还容易因为偶发的数据库抖动导致正常业务报错。
二.破局之道:改动后的"异步非阻塞"架构
要实现"让结果赶紧给返回前端,让异步执行慢慢写日志",我们需要引入 自定义线程池 + @Async 注解 的组合拳。整体改造分为三步:配置专属线程池、封装异步Service、重构AOP切面。
1. 配置专属日志线程池
为了防止高并发下日志任务耗尽系统默认线程资源,我们需要为日志分配独立的线程池。
java
@Configuration
@EnableAsync // 【关键步骤】开启Spring异步支持
public class AsyncLogConfig {
@Bean("logExecutor")
public Executor logExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(200); // 队列容量,起到削峰填谷作用
executor.setThreadNamePrefix("async-log-"); // 方便日志排查的线程名前缀
executor.initialize();
return executor;
}
}
2. 封装异步保存日志逻辑
将数据库操作抽离到独立的 Service 中,并使用 @Async 指定刚才配置的线程池。
java
@Service
public class BomisLogService {
@Autowired
private BoFrontMapper boFrontMapper;
@Async("logExecutor") // 提交到独立的日志线程池中执行
public void saveLogAsync(Map<String, Object> map) {
try {
boFrontMapper.addOneInteractFlow(map);
} catch (Exception e) {
// 异步任务中的异常不会抛给主线程,需在此处捕获防止丢失
System.err.println("异步日志落库失败: " + e.getMessage());
}
}
}
3. 重构AOP切面(改动后核心代码)
在切面中替换原有的 Mapper 调用,改为调用异步方法。
java
@Aspect
@Component
public class BomisLogAspect {
@Autowired
private BomisLogService bomisLogService; // 替换为异步Service
@Around("@annotation(bomisLog)")
public Object around(ProceedingJoinPoint joinPoint, BomisLog bomisLog) throws Throwable {
// ... 前面的参数获取逻辑保持不变 ...
String respMsg = null;
try {
Object result = joinPoint.proceed();
if (result instanceof R) {
respMsg = JSON.toJSONString((R) result);
}
return result; // 业务执行完毕后立刻返回前端,不再等待日志
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
try {
Map<String, Object> map = new HashMap<>();
// ... 组装Map数据 ...
// 【核心改动】触发异步任务,主线程瞬间释放
bomisLogService.saveLogAsync(map);
} catch (Exception e) {
log.error("组装日志数据异常: {}", e.getMessage());
}
}
}
}
三.改动前后深度对比
说白了就是,记录接口调用日志,这个动作,本身并不是我的核心业务逻辑,属于是类似于附赠品了,再说难听点,就算记录不上日志又如何?无伤大雅。所以我们干脆调用异步方法去跑就OK了(说白了就是给你个空地,自己去玩吧),这样不会拉低核心业务逻辑的处理速度。实际就是如此。
| 对比维度 | 改动前(同步模式) | 改动后(异步模式) |
|---|---|---|
| 接口响应时间 | 业务耗时 + DB写入耗时 | 仅等于业务耗时 |
| 主线程占用 | 全程阻塞,直到日志入库完成 | 触发异步后立即释放回Tomcat线程池 |
| 系统稳定性 | 数据库慢查询会直接拖垮业务接口 | 业务与日志解耦,互不影响 |
| 异常处理机制 | 异常可能向上抛出影响业务返回值 | 异常在子线程内部消化,保证主流程顺畅 |
| 资源隔离性 | 共享Tomcat/业务线程池资源 | 拥有专属的 logExecutor 线程池 |
思考:什么是所谓的"数据库抖动"
数据库抖动(Performance Jitter)是指数据库系统在运行过程中,在相同输入负载、相同SQL语句和相同配置参数的条件下,出现性能波动或不稳定的现象。
它通常表现为:一条正常执行特别快的 SQL 语句,偶尔会变得特别慢,且这种场景往往随机发生、持续时间短、难以复现;或者响应时间忽高忽低、吞吐量断崖式下跌等"非典型故障"。
以上就是本篇文章的全部内容,喜欢的话可以留个免费的关注呦~~~