比 MQ 更轻的异步方案:Spring 内置的这个隐藏功能,很多人还不知道

6 阅读4分钟

鹏磊我之前有段时间特别迷 MQ,啥异步场景都往上怼,RabbitMQ、RocketMQ 搞得很花,有一次就为了做个下单后发短信加积分的需求,硬是在内部系统里引了一套 RabbitMQ 进来

被同事笑话说这不叫杀鸡用牛刀,这叫杀鸡用核武器

后来才想起来,Spring 本身就内置了一套事件驱动机制,轻量得很,专治同 JVM 内部的异步解耦场景,白瞎我那几天折腾

问题出在哪,先看看这段代码

你写过这种东西吗:

public void createOrder(OrderDTO dto) {
    // 保存订单
    orderService.save(dto);
    // 发短信
    smsService.sendOrderSuccessSms(dto.getPhone());
    // 加积分
    pointsService.addPoints(dto.getUserId(), 100);
    // 推送站内信
    notificationService.push(dto.getUserId(), "订单创建成功");
    // 记操作日志
    logService.record("下单成功", dto.getOrderId());
}

鹏磊见过不少人写,咱也写过,一个"创建订单"的方法,里头塞了短信、积分、通知、日志,五件事挤一块儿

核心问题是,短信发没发、积分加没加,跟订单创建成不成功其实没啥关系,你把这些非核心逻辑全硬编码进来,它们就和主流程死死绑在一块了

哪天产品说再加个功能,比如下单后更新用户等级,你就得打开这个方法,再加一行,方法越来越长,越来越臃肿,谁维护谁骂娘

如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供。

Spring 事件机制是咋回事

Spring 从很早就有 ApplicationEvent 这套东西,原理是观察者模式(Observer Pattern),说白了就是发布订阅

发布方扔一个事件出去,不管谁在监听、有几个人在监听,它发完就走,不等,不管

监听方自己注册感兴趣的事件,事件来了自己处理,和发布方互不认识,互不干扰

整套机制跑在 JVM 内部,没有网络开销,没有消息队列中间件,比 MQ 省资源,延迟更低,部署也没额外的组件依赖,适合同服务内部的异步解耦这种场景

三步搞定,核心就这些

先定义事件

// 继承 ApplicationEvent,把要传递的业务数据带上
public class OrderCreatedEvent extends ApplicationEvent {
    private final String orderId;
    private final String phone;
    private final Long userId;

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

    // getter 略
}

就是个普通 Java 类,继承 ApplicationEvent,把业务数据搁里头,没啥复杂的

主流程发布事件

@Service
@RequiredArgsConstructor
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;

    public void createOrder(OrderDTO dto) {
        // 只干核心事:保存订单
        orderRepository.save(buildOrder(dto));

        // 发完就走,后续谁处理不管
        eventPublisher.publishEvent(
            new OrderCreatedEvent(this, dto.getOrderId(), dto.getPhone(), dto.getUserId())
        );
    }
}

注入 ApplicationEventPublisher,一句 publishEvent 了事,主流程干净利落,没有任何短信积分的影子

监听器各干各的

@Component
public class OrderEventListener {

    // @Async 让监听器异步执行,不加默认同步
    @EventListener
    @Async
    public void sendSms(OrderCreatedEvent event) {
        smsService.sendOrderSuccessSms(event.getPhone());
    }

    @EventListener
    @Async
    public void addPoints(OrderCreatedEvent event) {
        pointsService.addPoints(event.getUserId(), 100);
    }

    @EventListener
    @Async
    public void recordLog(OrderCreatedEvent event) {
        logService.record("下单成功", event.getOrderId());
    }
}

每个监听方法管一件事,互不干扰,Spring 自动根据事件类型路由过来

@Async 这个注解注意下,不加的话监听是同步执行的,发布方会等监听方跑完才往下走,这就跟没异步一样了,想真正解耦得加上,同时启动类加个 @EnableAsync 开关:

@SpringBootApplication
@EnableAsync
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

新增功能,核心代码一行不用动

这才是鹏磊最喜欢这套方案的地方

哪天产品说,下单后还要给用户推一条站内信,你怎么搞?直接在监听器里加个方法:

@EventListener
@Async
public void sendNotification(OrderCreatedEvent event) {
    notificationService.push(event.getUserId(), "您的订单已提交成功");
}

OrderService 里的 createOrder 方法,一个字都不用动

以前那种写法,你得打开核心方法,加一行,提交,走 code review,万一改出问题还得背锅,现在你根本不需要碰那块代码,稳得很

啥时候用 Spring 事件,啥时候上 MQ

这俩不是替代关系,别搞混了

我鹏磊的判断是这样的:

用 Spring 事件:同一个服务内部的异步解耦,不需要跨服务通信,不需要消息持久化,不需要断了还能重投,对消息丢失不敏感的场景

发个短信失败了大不了记个日志,用 Spring 事件够了,零中间件,零配置,上手五分钟

上 MQ:需要跨服务消费,需要消息不能丢,需要消费方单独扩容,需要延时消息或者死信队列这些高级功能,那就老老实实引 MQ,别省这点事

两个工具用途不一样,别因为 MQ 听起来高端就啥都往上怼,也别因为 Spring 事件轻量就觉得它能全部替代 MQ,各有各的地盘,用对了才省心

鹏磊总结一下这套东西的核心价值:主流程只干主流程的事,旁路逻辑全交给监听器,新增功能加监听器,删功能删监听器,核心代码稳如老狗,Spring 里头这种内置的好东西其实挺多的,白嫖就完了