Spring异步体系与事务一致性实战指南

文章目录

  • 前言
  • [一、 Spring 异步路径深度对比](#一、 Spring 异步路径深度对比)
  • 二、基于ApplicationEventMulticaster事件机制异步
    • [2.1 基于ApplicationEventMulticaster事件机制异步的代码实现](#2.1 基于ApplicationEventMulticaster事件机制异步的代码实现)
  • [三、 基于 @Async 注解的代理执行机制](#三、 基于 @Async 注解的代理执行机制)
    • [3.1 注解 @Async 的实现原理](#3.1 注解 @Async 的实现原理)
    • [3.2 使用@Async 注解 实现方法级别异步处理](#3.2 使用@Async 注解 实现方法级别异步处理)
    • [3.3 @Async + @EventListener 经典异步事件驱动组合](#3.3 @Async + @EventListener 经典异步事件驱动组合)
  • [四、 TransactionalEventListener解决异步事务一致性](#四、 TransactionalEventListener解决异步事务一致性)

前言

在Java业务开发中,异步处理是提升系统吞吐量的核心手段。Spring框架提供了丰富的工具栈(如 @Async、ApplicationEvent ),帮助开发者实现异步处理。但在分布式或高并发环境下,若处理不当,常会引发"异步抢跑 "事件等严重一致性问题。
异步抢跑:当事件提交时,主线程事务尚未提交,而消费者已经获取到事件进行处理,由于主线程事务未提交,由于事务的隔离性,但是异步消费线程无法从数据库查询到提交事件的信息


一、 Spring 异步路径深度对比

​ 实现异步化处理主要有两条技术路线,其底层原理与适用场景有着本质区别:

实现方式 全局事件异步(Multicaster) 注解驱动代理(@Async)
实现原理 修改ApplicationEventMulticaster的执行策略 基于 AOP(动态代理) 拦截方法调用
控制粒度 粗(全站所有事件一律走异步分发) 细(可针对具体某个方法、某个监听器开启异步)
事务感知 无感知(事件发出后立即入队,无法等待事务提交) 强感知 (@TransactionalEventListener实现事务阶段绑定)
线程隔离 通常全站共用一个线程池,容易互相干扰 可通过 @Async("线程池名称") 为不同业务分配独立池
灵活性 一对一(一个类只能实现一个接口,不同类型的业务都在这个接口处理) 一对多(一个类里可以写无数个监听方法)
过滤逻辑 当一个接口存在多个业务逻辑时,需大量if/else进行判断 支持SpEL表达式过滤

二、基于ApplicationEventMulticaster事件机制异步

SpringJDK 原有的观察者模式(EventListenerEventObject )基础上进行了扩展,提供了基于ApplicationListenerApplicationEvent 的消息发布事件。而ApplicationEventMulticasterSpring 事件机制的核心组件 ,它负责接收发布的事件(ApplicationEvent ),并将事件发布给所有监听该事件的处理器(ApplicationEvent),你可以把它理解成:

  • 事件发布者(**Service):只负责将 "ApplicationEvent" 发送给广播器;
  • 广播器(ApplicationEventMulticaster):负责 "分发事件" 给所有对应的监听器;
  • 监听器(ApplicationListener):只负责 "处理事件"。

SimpleApplicationEventMulticasterApplicationEventMulticaster的实现类,其实现异步的核心思想在于当广播器找到对应的监听器时,将监听器处理这个事件的逻辑放到了线程池中进行处理,以此完成异步处理,核心代码如下:

java 复制代码
   @Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		Executor executor = getTaskExecutor();
		for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
				invokeListener(listener, event);
			}
		}
	}

getTaskExecutor() 方法尝试去获取线程池,如果没有配置线程池的话,则进入 invokeListener(listener, event); 的代码逻辑,该代码逻辑则时直接调用 listener.onApplicationEvent(event); 此时就没有使用到线程池,也就并没有实现异步处理的效果。反过来,如果获取到了线程池,则调用 executor.execute(() -> invokeListener(listener, event)); 这段代码,将监听器处理事件的逻辑放入了线程池中进行处理,此时就实现了异步处理的效果。

由上核心代码我们得知,想要实现事件异步处理,核心在于需要给 SimpleApplicationEventMulticaster 配置线程池!接下来我们将以 JAVA 代码来实现基于Spring事件的异步事件处理。


2.1 基于ApplicationEventMulticaster事件机制异步的代码实现

既然需要线程池并将线程池设置到SimpleApplicationEventMulticaster 中才能实现异步,那么肯定需要我们定义线程池和SimpleApplicationEventMulticaster并注入Spring Bean 容器中,那么我们定义一个线程池配置类,并将它们注入到Spring Bean中。

java 复制代码
@Configuration
public class ThreadPoolTaskExecutorConfig {
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
        threadPoolTaskExecutor.setCorePoolSize(8);
        threadPoolTaskExecutor.setKeepAliveSeconds(30000);
        threadPoolTaskExecutor.setMaxPoolSize(16);
        threadPoolTaskExecutor.setQueueCapacity(100000);
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.setThreadNamePrefix("Thread-Pool-Task-");
        return threadPoolTaskExecutor;
    }

    /**
    定义SimpleApplicationEventMulticaster并设置线程池
    **/
    @DependsOn(value = "taskExecutor")
    @Bean(AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
    public SimpleApplicationEventMulticaster eventMulticaster() {
        SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new SimpleApplicationEventMulticaster();
        simpleApplicationEventMulticaster.setTaskExecutor(taskExecutor());
        return simpleApplicationEventMulticaster;
    }
}

现在已经定义好了SimpleApplicationEventMulticaster并为其设置了线程池,接下来我们来编写消息事件、事件发布者、事件监听器:

消息事件:

java 复制代码
public class MessageEvent extends ApplicationEvent {
    public MessageEvent(Object source) {
        super(source);
    }
}

事件发布者:

java 复制代码
@Autowired
private ApplicationContext applicationContext;
/**
调用publishMessage方法发送事件
**/
public void  publishMessage() {
    MessageEvent messageEvent = new MessageEvent("你好!SimpleApplicationEventMulticaster");
    System.out.println("当前发布事件线程:" + Thread.currentThread());
    //发布事件
    applicationContext.publishEvent(messageEvent);
}

事件监听器:

java 复制代码
@Component
public class MessageListener implements ApplicationListener<MessageEvent> {

    @Override
    public void onApplicationEvent(MessageEvent event) {
        Object source = event.getSource();
        System.out.println("消息监听器监听到消息:===>"+JsonUtils.writeObjectAsBeautifulJson(source));
    }
}

当事件发布者通过applicationContext.publishEvent 将事件MessageEvent 发送以后,SimpleApplicationEventMulticaster 会试图找监听事件MessageEvent 类型的监听器,然后通过executor.execute(() -> invokeListener(listener, event));进行异步调用监听器的onApplicationEvent方法。测试结果如下:

java 复制代码
当前发布事件线程:Thread[http-nio-8088-exec-1,5,main]
当前处理事件线程:Thread[Thread-Pool-Task-1,5,main]
消息监听器监听到消息:===>"你好!SimpleApplicationEventMulticaster"

可以看到事件发布线程与事件处理线程是两个不同的线程,监听器也成功收到消息,说明基于ApplicationEventMulticaster的异步处理已经成功实现!


三、 基于 @Async 注解的代理执行机制

相比于全局配置,使用 @Async 注解是 Spring 提供的更为现代、灵活的异步实现方案。它通过 AOP(面向切面编程)技术,将原本同步执行的方法拦截并交给线程池处理。


3.1 注解 @Async 的实现原理

@Async 的核心在于 AsyncAnnotationBeanPostProcessor 。在 Spring 容器启动时,它会扫描标注了 @Async 的类或方法,并为其生成一个 代理对象(Proxy)

  • 调用链拦截 :当外部方法调用该代理对象的方法时,Spring 拦截器会从指定的 TaskExecutor中获取线程。
  • 上下文切换 :拦截器将任务封装为 CallableRunnable 提交给线程池执行,而调用方(主线程)会立即获得返回值(通常为 void 或 Future)并继续向下执行。

注意(关键坑点):由于其基于 AOP 代理,同一个类内部的方法互调(Internal Call)会导致异步失效。因为内部调用绕过了代理对象,直接通过 this 指针调用原始方法。

@Async 的生命周期大体如下:

  • 注册阶段@EnableAsync 导入了 ProxyAsyncConfiguration ,它向容器注册了 AsyncAnnotationBeanPostProcessor
  • 创建阶段BeanPostProcessor 在初始化 Bean 时,通过 AsyncAnnotationAdvisor 里的 Pointcut 识别出带有 @Async 的类或方法。
  • 运行阶段 :生成的代理对象拦截调用,由 AnnotationAsyncExecutionInterceptor 将任务丢进指定的线程池。

3.2 使用@Async 注解 实现方法级别异步处理

要使用 @Async ,首先需要在配置类上开启 @EnableAsync 并在容器中注入线程池。

java 复制代码
    @Configuration
    @EnableAsync 开启异步驱动总开关
    public class AsyncConfig {
    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
            ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
            threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
            threadPoolTaskExecutor.setCorePoolSize(8);
            threadPoolTaskExecutor.setKeepAliveSeconds(30000);
            threadPoolTaskExecutor.setMaxPoolSize(16);
            threadPoolTaskExecutor.setQueueCapacity(100000);
            threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            threadPoolTaskExecutor.setThreadNamePrefix("Thread-Pool-Task-");
            return threadPoolTaskExecutor;
            }
     }

在具体的监听器或 Service 方法中使用注解Aysnc即可完成异步:

java 复制代码
@Component
public class MessageService {
    //在方法上指定使用哪个线程池
    @Async("taskExecutor")
    public void onMessage(String message) {
        // 异步逻辑
        log.info("当前异步线程: {}, 收到消息: {}", Thread.currentThread().getName(), message);
    }
}

3.3 @Async + @EventListener 经典异步事件驱动组合

@Async + @EventListener 是 Spring 中异步事件驱动 的经典组合,核心价值是解耦业务逻辑 + 异步执行非核心操作,既能提升接口响应速度,又能将 "主业务" 和 "附属业务" 拆分,从而到功能职责单一。

既然 @Async 本身就能实现异步,为啥还要多此一举加 @EventListener ?本质上,@Async 只解决 "怎么异步执行" 的问题,而 @EventListener 解决 "异步逻辑和主业务怎么解耦" 的问题;两者组合是 "异步执行 + 发布 - 订阅解耦" 的黄金搭配,比单独用 @Async 优势大得多!

实现方式 核心能力 耦合度 扩展性 维护成本
单独用 @Async 仅异步执行方法 高(主业务直接调用异步方法) 差(改异步逻辑要动主业务代码)
@Async + @EventListener 异步执行 + 发布 - 订阅解耦 低(主业务只发事件,不关心谁处理) 强(新增 / 删除异步逻辑不改动主业务)

我们以 "创建订单成功后,发送提示短信" 这个场景来说,如果仅使用 @Async 来实现异步,代码案例如下:

java 复制代码
@Service
public class OrderService {
    //注入短信服务(主业务和短信业务强耦合)
    @Autowired
    private SmsService smsService;

    @Transactional
    public void createOrder(String phone, String productId) {
        // 核心逻辑:创建订单
        Long orderId = 1001L;
        String orderNo = "ORDER_" + System.currentTimeMillis();
        System.out.println("主线程:创建订单 " + orderNo);
        // 直接调用异步方法(主业务硬编码依赖短信服务)
        smsService.sendOrderSms(phone, orderNo);
        System.out.println("主线程:订单接口返回");
    }
}

@Service
public class SmsService {
    // 仅用@Async实现异步
    @Async("taskExecutor")
    public void sendOrderSms(String phone, String orderNo) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("异步线程:给" + phone + "发送订单短信,订单号" + orderNo);
    }
}

单独用 @Async 的核心问题

  1. 强耦合 :订单服务必须知道 "短信服务" 的存在,还得注入它、调用它的方法;如果后续要加 "推送 APP 消息",必须修改 OrderService 的代码,违背 "开闭原则";
  2. 职责混乱:订单服务的核心职责是 "创建订单",却要关心 "发短信" 这个附属逻辑,代码越写越臃肿;
  3. 难以扩展 :如果要新增 "订单创建后加积分""订单创建后记录日志",需要在 createOrder 里逐个调用 pointService.addPoint()logService.saveLog(),主业务代码会被各种附属逻辑淹没;
  4. 异常风险 :虽然 @Async 是异步,但如果注入的 SmsService 为空(比如配置错了),主线程会直接抛空指针,订单都创建不了。

如果我们采用 @Async + @EventListener来实现一下业务场景,代码如下:

java 复制代码
//定义事件(只承载数据,无业务逻辑)
@Getter
public class OrderCreatedEvent extends ApplicationEvent {
    private final String phone;
    private final String orderNo;

    public OrderCreatedEvent(Object source, String phone, String orderNo) {
        super(source);
        this.phone = phone;
        this.orderNo = orderNo;
    }
}

// 订单服务(只发布事件,不关心谁处理)
@Service
public class OrderService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Transactional
    public void createOrder(String phone, String productId) {
        // 核心逻辑:创建订单
        Long orderId = 1001L;
        String orderNo = "ORDER_" + System.currentTimeMillis();
        System.out.println("主线程:创建订单 " + orderNo);
        // 发布事件(只传数据,不调用任何服务)
        eventPublisher.publishEvent(new OrderCreatedEvent(this, phone, orderNo));
        System.out.println("主线程:订单接口返回");
    }
}

//短信监听器(异步处理,和主业务完全解耦)
@Component
public class SmsListener {
    @Async("taskExecutor")
    @EventListener(OrderCreatedEvent.class) //设置监听器监听的事件类型
    public void sendSms(OrderCreatedEvent event) {
        System.out.println("异步线程:给" + event.getPhone() + "发送订单短信,订单号" + event.getOrderNo());
    }
}

组合使用的核心优势(对比单独用 @Async):

一. 彻底解耦(最核心)

  • 主业务(订单创建)和附属业务(发短信)完全隔离:订单服务只负责 "发布事件",不知道也不关心谁会处理这个事件;
  • 新增 / 删除附属逻辑,无需修改主业务代码 :比如要加 "积分增加",只需要新增一个 PointListener 监听 OrderCreatedEvent 即可;要停用 "短信发送",直接删掉 SmsListener 就行,订单服务完全无感知。

二. 职责单一(符合设计原则)

  • 订单服务:只做 "创建订单 + 发布事件",核心职责清晰;
  • 监听器:每个监听器只做自己该做的事,代码模块化,易维护;
  • 事件类:只承载数据(phone、orderNo),是 "数据传输载体",无业务逻辑。

三. 容错性更强

  • 即使所有监听器都出问题(比如短信监听器抛异常),主线程的订单创建逻辑不受任何影响
  • 即使没有任何监听器(比如忘了写),订单服务也能正常运行(只是没人处理事件而已),不会像单独用 @Async 那样因为注入失败导致主线程报错。

四. 扩展性极致灵活

  • 一个事件可以被多个监听器监听:订单创建事件可以同时触发 "发短信、推 APP、加积分、记日志",所有逻辑并行异步执行;
  • 可以动态注册 / 移除监听器:甚至在运行时决定哪些监听器生效(比如测试环境不发短信),主业务代码无需改动。

四、 TransactionalEventListener解决异步事务一致性

当主线程存在事务和事件时,由于事务是在方法执行之后才提交,而事件分发出去后可能立即被监听器进行对应的业务处理。则现在就可能存在一种情况:主线程将事件分发出去时,事务还未提交,而监听器拿到事件之后,就开始进行业务处理。由于事务的隔离性,会导致监听器处理业务时,无法查询到还未提交事务的事件数据,这种情况我们一般称为"异步抢跑"。并且如果事务提交失败,而事件又已经被分发出去,此时的事件也会监听器进行消费,这其实是错误的逻辑!

为了解决上述问题,Spring提供了 @TransactionalEventLisener 注解,@TransactionalEventListener 是 Spring 专门为 "事务和事件驱动结合" 设计的注解,核心解决的是「异步 / 同步事件执行时机与数据库事务提交状态不匹配」的问题。@TransactionalEventListener 解决的核心问题是让事件监听器的执行逻辑与数据库事务的生命周期绑定,确保监听器在事务的指定阶段(如提交后、回滚后)执行,避免出现 "事务未提交就执行监听器导致查不到数据" 或 "事务回滚了但监听器仍执行" 的问题。

  • (一)它通过绑定事务生命周期,让监听器只在事务的指定阶段执行,核心是 2 个关键能力:

@TransactionalEventListener 新增了 phase 属性,支持 4 个事务阶段,精准控制执行时机:

phase 值 执行时机 解决的问题
AFTER_COMMIT 事务提交成功后执行 解决 "事务未提交查不到数据" 的问题(你的场景)
BEFORE_COMMIT 事务提交前执行 适合 "提交前做最后校验 / 补充数据"
AFTER_ROLLBACK 事务回滚后执行 适合 "回滚后记录日志 / 告警"
AFTER_COMPLETION 事务完成后执行(提交 / 回滚都执行) 适合 "无论成败都做的收尾操作"
java 复制代码
// 只在事务提交后发消息,回滚则不执行
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEvent(OrderCreatedEvent event) {
    smsService.sendSms("订单创建成功"); // 只有事务提交才会发
}
// 回滚后记录告警
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleRollback(OrderCreatedEvent event) {
    log.error("订单{}创建失败,事务回滚", event.getOrderId());
}
  • (二)无事务时的兜底执行(属性:fallbackExecution)

默认情况下,如果发布事件的方法没有事务 (比如没加 @Transactional),@TransactionalEventListener 不会执行。通过 fallbackExecution = true 可以兜底执行。

java 复制代码
// 无事务时也执行(兜底)
@TransactionalEventListener(
    phase = TransactionPhase.AFTER_COMMIT,
    fallbackExecution = true // 核心:无事务时也执行
)
public void handleEvent(OrderCreatedEvent event) {
    // 有事务 → 提交后执行;无事务 → 立即执行
}
  • @TransactionalEventListener 和普通 @EventListener 的核心区别
特性 @EventListener @TransactionalEventListener
事务绑定 无,发布后立即执行 绑定事务生命周期,指定阶段执行
解决核心问题 仅解耦,无法处理事务一致性 解耦 + 保证事务一致性
无事务时执行 正常执行 默认不执行,需配置 fallbackExecution=true
你的原问题 无法解决(查不到数据) 完美解决(提交后执行)
相关推荐
鸽鸽程序猿1 小时前
【JavaEE】【SpringAI】聊天模型
java·java-ee
韭菜张师傅1 小时前
Ceph环境完全重置指南:彻底清理集群环境
java·网络·ceph
SunnyDays10111 小时前
使用 Java 实现 Word 文档水印自动化(全面指南)
java·添加水印·word文档
敲代码的小王!1 小时前
prompt开发游戏-哄哄模拟器
java·游戏·ai·prompt
学编程就要猛1 小时前
JavaEE:多线程初阶
java·开发语言·jvm
筱顾大牛1 小时前
缓存更新策略
java·redis·缓存
sheji34161 小时前
【开题答辩全过程】以 慧医疗网上医院管理系统为例,包含答辩的问题和答案
java
子一!!1 小时前
JavaEE初阶第一课时==计算机与系统讨论==
java·java-ee
小二·1 小时前
Go 语言系统编程与云原生开发实战(第37篇)
java·云原生·golang
yxc_inspire1 小时前
大二 Java 后端学习记录:集合框架(List/Queue/Map/Set)+ 泛型 + 迭代器
java·开发语言