如何实现异步写入日志?一文详解

在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 语句,偶尔会变得特别慢,且这种场景往往随机发生、持续时间短、难以复现;或者响应时间忽高忽低、吞吐量断崖式下跌等"非典型故障"。

以上就是本篇文章的全部内容,喜欢的话可以留个免费的关注呦~~~

相关推荐
better_liang1 天前
每日Java面试场景题知识点之-JUC并发编程核心原理与实战
java·线程池·并发编程·juc·aqs·reentrantlock·concurrenthashmap
阿维的博客日记4 天前
线程任务执行报错后,线程会不会挂掉,Java线程池
java·线程池
阿昌喜欢吃黄桃6 天前
如果线程池中线程异常后:销毁还是复用?
java·线程·线程池·多线程·juc
是码龙不是码农8 天前
ThreadPoolExecutor 7 个核心参数详解
java·线程池·threadpool
阿昌喜欢吃黄桃9 天前
并发线程工具类分享
java·线程池·多线程·并发·juc
W230357657313 天前
Linux C++ 基于 timerfd + epoll 实现高性能定时器队列(完整源码 + 超详细解析)
linux·开发语言·c++·线程池
工程师00719 天前
.NET 线程池 工作线程 扩容 + 空闲 + 回收 原理
c#·线程池·扩容·回收·空闲
青山师20 天前
线程池深度解析:从生产者-消费者模型到工业级调优实践
java·面试题·线程池·多线程·java面试
阿冰冰呀1 个月前
互联网大厂Java求职面试实录:谢飞机的“水货”之路
java·mybatis·dubbo·springboot·线程池·多线程·hashmap