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

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

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

相关推荐
~木雨2 天前
Java 并发编程架构全景:并发层的五域设计 —— 线程模型、线程池隔离、并发安全到问题治理(全体系汇总)
java·安全·架构·线程池·并发编程·高并发架构
琥珀色糖2 天前
Linux手搓线程池!(C和C++版)
线程池
~木雨4 天前
Java 线程池七问七答:参数、执行流程、拒绝策略到 ThreadLocal 内存泄漏,面试必背
java·面试·线程池·threadlocal·threadpool·executor
小宇宙清歡渡8 天前
SpringBoot JDK17 线程池的使用
springboot·线程池·jdk17
AI人工智能+电脑小能手15 天前
大白话说Java设计模式-37-中介者模式(源码剖析篇)
java·设计模式·线程池·中介者模式·源码分析·executor·applicationeventmulticaster
JacksonMx18 天前
CompletableFuture 生产环境踩坑详解与解决方案
spring boot·spring·线程池
lisin-lee-cooper1 个月前
简单聊聊线程池源码
java·线程池
2401_858286111 个月前
OS82.【Linux】设计线程池
java·linux·运维·服务器·开发语言·算法·线程池
小当家.1051 个月前
工具并行调用原理与实现:CompletableFuture 实战
java·agent·线程池·工具·并行
AaronJonah1 个月前
企业级线程池 traceId 闭环:上下文传递与全链路追踪实战
java·线程池·traceid跨线程传递