博客 / 詳情

返回

Spring監聽器(ApplicationEvent):比MQ更輕的異步神器!

引言:當咖啡店遭遇程序員

“顧客擠爆櫃枱時,優秀的店長不會催促咖啡師加速,而是啓動一套科學的協作機制——
就像Spring事件驅動,用發佈-訂閲模式讓系統像頂級咖啡團隊般優雅應對洪峯流量”


一、咖啡店裏的監聽器:3位靈魂角色

真實戰場還原(每秒1000訂單的咖啡店):

graph LR 顧客["🔥 顧客喊單(事件發佈者)"] --> 訂單事件["📦 OrderEvent(事件對象)"] 訂單事件 --> 咖啡師["☕ 咖啡師(監聽器1)"] 訂單事件 --> 收銀員["💰 收銀員(監聽器2)"] 訂單事件 --> 甜點師["🍰 甜點師(監聽器3)"]

1. 事件定義:咖啡店的「訂單小票」

public class OrderEvent extends ApplicationEvent {
    // final修飾的訂單ID:就像咖啡師絕不塗改的訂單小票
    private final String orderId;  
    
    // 創建時間:記錄訂單誕生時刻(線程安全不可變)
    private final LocalDateTime createTime = LocalDateTime.now(); 

    // 無setter:防止多線程併發篡改訂單
}

2. 事件發佈:店長的「廣播系統」

@Service
public class OrderService {
    // 店長的麥克風(構造器注入更優雅)
    private final ApplicationEventPublisher eventPublisher; 

    public void createOrder(Order order) {
        // 核心業務:生成訂單(咖啡店接單)
        eventPublisher.publishEvent(new OrderEvent(this, order.getId())); // 📢 廣播訂單
    }
}

3. 事件監聽:咖啡團隊的「技能響應」

@Component
public class CoffeeMakerListener {
    @EventListener 
    @Order(1) // 優先級:先做咖啡再推薦甜點
    public void makeCoffee(OrderEvent event) {
        // 專注做咖啡,不關心誰結賬
        log.info("咖啡師:開始製作訂單{}的拿鐵...", event.getOrderId());
    }
}

二、扛住億級流量的3把利器

🔥 場景1:冷啓動緩存預加載(防雪崩)

@Component
public class CachePreloader {
    // 在Spring容器"開店準備完成"時觸發
    @EventListener(ContextRefreshedEvent.class) 
    public void initCache() {
        // 異步加載省時30%(實測數據)
        CompletableFuture.runAsync(() -> {
            provinceService.loadProvincesToCache(); 
            productService.preloadHotProducts();
        });
    }
}

💡 場景2:事務成功後的緩存清理(保一致性)

// 只在數據庫提交成功後執行(避免髒清理)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void cleanCache(OrderUpdateEvent event) {
    // 異步清理:不阻塞結賬隊伍
    redisTemplate.executeAsync(new RedisCallback<>() {
        @Override
        public Void doInRedis(RedisConnection connection) {
            connection.del(("order:" + event.getId()).getBytes());
            return null;
        }
    });
}

🚀 場景3:無侵入式功能擴展

改造前(臃腫的收銀台)

public void pay() {
    paymentService.pay();   // 核心支付
    auditService.log();     // 審計代碼入侵
    riskService.check();    // 風控代碼耦合
    marketingService.addPoints(); // 新增需求污染核心
}

事件驅動改造後

// 純淨支付核心(專注收錢)
public void pay(Long orderId) {
    paymentService.process(orderId);
    eventPublisher.publishEvent(new PaymentSuccessEvent(orderId)); // 📢 廣播支付成功
}

// 新增積分模塊(無需修改支付代碼)
@Component
public class PointListener {
    @EventListener
    public void addPoints(PaymentSuccessEvent event) {
        // 積分服務獨立演進
        pointService.award(event.getOrderId(), 100); 
    }
}

三、血淚教訓:3個深夜加班事故

🚫 事故1:多線程篡改事件(訂單混亂)

// 錯誤!事件必須是隻讀的
@EventListener
public void handle(OrderEvent event) {
    event.setStatus("MODIFIED"); // ⚠️ 多線程併發修改引發訂單錯亂
}

正確做法:事件類設計為final字段 + 無setter

🚫 事故2:異步事件丟失(顧客投訴)

@SpringBootApplication
@EnableAsync // 必須顯式開啓異步
public class Application {
    @Bean("eventExecutor") 
    public Executor taskExecutor() {
        // 關鍵參數:拒絕策略用CallerRunsPolicy(避免丟單)
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

// 指定線程池執行
@Async("eventExecutor") 
@EventListener
public void asyncHandle(OrderEvent event) {...}

🚫 事故3:事件循環調用(咖啡師卡死)

// 錯誤:在事件處理中發佈新事件
@EventListener
public void handleA(EventA a) {
    publisher.publishEvent(new EventB()); 
}

@EventListener
public void handleB(EventB b) {
    publisher.publishEvent(new EventA()); // ♻️ 死循環!
}

四、關鍵抉擇:監聽器 vs MQ 架構對壘

維度 Spring監聽器 MQ消息隊列
適用場景 單機事務協作 ✅ 跨服務通信 ✅
可靠性 進程宕機事件消失 ❌ 持久化/重試 ✅
吞吐量 內存級傳輸,10w+/s 🚀 受網絡限制,1w/s ⚠️
開發效率 免搭建MQ,註解即用 ✅ 需部署中間件 ❌
數據一致性 本地事務保障 ✅ 需分佈式事務 ⚠️

黃金決策樹

  • 同JVM事務操作 → Spring監聽器(開發效率王炸)
  • 跨服務最終一致 → RocketMQ(可靠性擔當)

五、性能調優:監聽器的渦輪增壓

  1. 異步噴射

    @Async // 方法級異步(線程池加速)
    @EventListener
    public void asyncProcess(LogEvent event) {...}
    
  2. 條件過濾(減少無效處理):

    // 只處理VIP客户的訂單
    @EventListener(condition = "#event.user.level == 'VIP'")
    public void handleVipOrder(OrderEvent event) {...}
    
  3. 批量處理(Spring 4.2+特性):

    // 一次性處理整批訂單(提升數據庫IO效率)
    @EventListener
    public void batchProcess(List<OrderEvent> events) {
        orderDao.batchInsert(events.stream().map(OrderConverter::toEntity).toList());
    }
    

六、最佳實踐:5條生存法則

  1. 單一職責原則
    一個監聽器只做一件事:如 PaymentListener 只處理支付,CouponListener 只發券

  2. 事件輕量化
    禁止在事件中攜帶 HttpSession 等重型對象(建議只傳ID)

  3. 異常隔離艙
    異步事件必須獨立捕獲異常:

    @Async
    @EventListener
    public void handle(Event event) {
        try { 
            businessLogic(); 
        } catch (Exception e) { 
            // 記錄日誌 + 告警(防止雪崩)
            log.error("事件處理失敗: {}", event, e); 
            alarmManager.notify(e);
        }
    }
    
  4. 版本兼容設計
    事件類預留版本字段:

    public class OrderEvent {
        private final String version = "1.0"; // 未來可擴展
    }
    
  5. 監控三件套

    // 監控處理時長/失敗率/QPS
    @Around("@annotation(org.springframework.context.event.EventListener)")
    public Object monitor(ProceedingJoinPoint pjp) {
        Timer.Sample sample = Timer.start();
        try {
            return pjp.proceed();
        } finally {
            sample.stop(Metrics.timer("event.process.time"));
        }
    }
    

結語:事件驅動的藝術

優秀架構的本質不是預測所有需求,而是擁抱變化。
通過Spring事件監聽器,我們將系統拆解為可插拔的樂高模塊

  • 新增功能時 → 添加監聽器(無需修改核心代碼)
  • 流量暴增時 → 開啓異步(無需重構架構)

這恰如經營咖啡店的真諦:
“不是僱傭更快的咖啡師,而是設計永不擁堵的協作機制”

程序員彩蛋
下回當你為需求變更焦頭爛額時,不妨問問自己:
“我的代碼,像一家應對自如的咖啡店嗎?”


技術選型建議:萬級QPS以內首選Spring事件,超越則上MQ

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.