找回密码
立即注册
搜索
发回帖 发新帖

5179

积分

0

好友

673

主题
发表于 昨天 21:17 | 查看: 13| 回复: 0

线上有个接口,原本 80ms 返回,改完 Spring Event 之后,偶发直接飙到 3 秒。开发同事第一反应是:“异步了啊,怎么还慢了?” 我看了下代码,问题就在那句 publishEvent()。

很多人第一次用 Spring Event,会觉得这个东西特别香。业务代码里不想塞一堆通知逻辑?丢个 Event。订单创建后要发短信、记日志、更新积分?丢个 Event。看起来代码干净了,耦合也降低了。但这里有个坑:Spring Event 默认不是异步。

你以为把代码扔出去让别人慢慢处理了,实际上它可能还在你的请求线程里同步执行。这类问题线上特别容易出现,因为测试环境数据少,接口调用频率低,根本看不出来。

我之前排一个订单接口慢的问题,链路大概这样:

POST /order/create

耗时:
Controller         20ms
保存订单           50ms
发送事件           5ms
监听库存事件       900ms
发送通知事件       1200ms
返回响应           100ms

最后发现所谓“异步通知”,全部在主线程里面跑。

Spring Event 默认机制其实很简单。

发布事件:

@Service
public class OrderService {

    private final ApplicationEventPublisher publisher;

    public OrderService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void createOrder(Order order) {
        orderRepository.save(order);
        publisher.publishEvent(new OrderCreatedEvent(order.getId()));
    }
}

监听:

@Component
public class OrderEventListener {

    @EventListener
    public void handle(OrderCreatedEvent event) {
        // 发短信
        smsService.send(event.getOrderId());

        // 更新积分
        pointService.add(event.getOrderId());
    }
}

很多人看到这里,会下意识认为:“事件发布了,监听方法应该另外执行吧?”不是。默认情况下,publishEvent() 会直接调用监听器。调用链类似:

Controller线程
       |
       |
publishEvent()
       |
       |
OrderEventListener.handle()
       |
       |
短信接口
积分服务
数据库操作
       |
       |
返回Controller

所以如果监听里面有慢操作,接口一样被拖住。

这个问题我一般第一眼会看 trace。比如 SkyWalking 里面看到:

order-create
  ApplicationEventPublisher.publishEvent
       2ms

  OrderCreatedListener.handle
       1850ms

       SmsClient.send
       900ms

       PointService.update
       950ms

基本就定位了。

那是不是 Spring Event 不能用?也不是。它适合干一些轻量级通知,比如:

  • 更新本地缓存
  • 记录操作日志
  • 发一些非核心埋点
  • 同一个应用内部解耦

这些没问题。

但下面这些,我基本不会直接塞 Spring Event:

  • 发短信
  • 发邮件
  • 调第三方接口
  • 大量数据计算
  • 文件处理
  • MQ 同步
  • 数据同步任务

因为这些事情有两个特点:第一,耗时不可控。今天第三方接口 200ms,明天可能 5 秒。第二,失败处理麻烦。事件监听里面异常了怎么办?默认情况下,如果监听抛异常,发布事件的线程也会受到影响。比如:

@EventListener
public void handle(OrderCreatedEvent event) {
    remoteApi.call();
    throw new RuntimeException("send failed");
}

这个异常可能直接影响你的业务流程。订单保存成功了,但是后面的通知失败,到底算成功还是失败?这时候很多系统开始补各种 try-catch,最后代码变成这样:

@EventListener
public void handle(OrderCreatedEvent event) {
    try {
        smsService.send(event.getOrderId());
    } catch (Exception e) {
        log.error("短信发送失败,id={}", event.getOrderId(), e);
        retryQueue.add(event);
    }
}

看到这里其实已经开始复杂了。事件通知慢慢变成了一个消息系统。那为什么不用 MQ?因为 Spring Event 和 MQ 解决的问题不一样。

Spring Event:

一个 JVM 内部
        |
        |
事件发布
        |
        |
监听处理

MQ:

订单服务
    |
    |
消息队列
    |
    |
短信服务
积分服务
库存服务

MQ 多了一层,但换来了:

  • 服务隔离
  • 消息重试
  • 消费能力控制
  • 服务故障隔离

当然,如果只是想让 Spring Event 真正异步,也可以配置线程池。比如:

@Configuration
@EnableAsync
public class EventConfig {

    @Bean("eventExecutor")
    public Executor eventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(12);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("event-worker-");
        executor.initialize();
        return executor;
    }
}

监听:

@Component
public class OrderEventListener {

    @Async("eventExecutor")
    @EventListener
    public void handle(OrderCreatedEvent event) {
        smsService.send(event.getOrderId());
        cacheService.refresh(event.getOrderId());
    }
}

这样事件监听会进入线程池。但是这里还有一个坑。

异步之后,事务关系变了。比如:

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    publisher.publishEvent(new OrderCreatedEvent(order.getId()));
}

监听:

@Async
@EventListener
public void handle(OrderCreatedEvent event) {
    Order order = orderRepository.findById(event.getOrderId());
}

这个时候监听线程已经不是原来的事务线程。所以可能出现:

主线程:

insert订单
事务未提交
publishEvent

异步线程:

查询订单
查不到

这类问题我见过不少。代码看起来没问题,测试也偶尔正常,上线压力一大就开始出奇怪问题。

如果你的事件必须等事务提交之后执行,应该考虑:

@Component
public class OrderListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommit(OrderCreatedEvent event) {
        cacheService.refresh(event.getOrderId());
    }
}

这个意思是:事务成功提交之后,再执行监听。如果事务回滚,事件不会触发。这个对于更新缓存、发送通知这种场景更安全。

不过还有一点要注意。Spring Event 没有消息队列那些能力。比如:服务重启了,事件还没执行,没了。线程池满了,任务拒绝,没了。所以别把它当 MQ 用。

我见过一些项目,把所有事情都包装成 Event。最后业务代码是干净了:

orderService.create();

但是后面藏了一堆:

OrderCreatedEvent
    |
    +-- 更新库存
    +-- 发短信
    +-- 计算佣金
    +-- 同步ES
    +-- 推送消息
    +-- 生成报表

新人接手的时候,根本不知道一次创建订单会触发多少事情。这种“解耦”,有时候只是把复杂度藏起来。

我自己用 Spring Event,一般守几个边界:

  • 简单通知,用。
  • 跨服务调用,不用。
  • 耗时任务,不用。
  • 必须可靠执行,不用。
  • 需要重试补偿,不用。

线上排查方便,比代码看起来优雅更重要。

Event 本身没有问题,问题在于很多人把它当成万能胶。刚开始觉得哪里都能粘,等系统复杂起来,才发现有些地方不是粘上就完了,后面拆的时候才真的疼。这类踩坑记录在云栈社区里也经常被翻出来讨论。




上一篇:SoC有AXI总线了,安全密钥分发为何还要专门协议?
下一篇:面试官:Kafka 如何保证消息不丢失?三端配置与 ISR 机制详解
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-27 05:40 , Processed in 0.079815 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表