找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

6365

积分

1

好友

794

主题
发表于 3 天前 | 查看: 2| 回复: 0

订单状态刚更新完,接口突然 500。

日志往下一翻,不是数据库挂了,也不是 RPC 超时,而是一个八竿子打不着的"发积分"监听器炸了。

2026-08-26 14:37:21.608 ERROR order-api
payment confirm failed, orderNo=OD882731

java.lang.IllegalStateException: points rule not found
    at PointsListener.onPaid(PointsListener.java:43)
    at ...
    at SimpleApplicationEventMulticaster.invokeListener(...)

看到 SimpleApplicationEventMulticaster 这行,我基本就知道问题往哪查了。

Spring Event 又被当 MQ 用了。

这种代码我见过不少:

@Transactional
public void confirmPaid(String orderNo){
    Order order = orderRepository.lockByOrderNo(orderNo);
    order.markPaid();

    eventPublisher.publishEvent(
    new OrderPaidEvent(order.getId(), orderNo, order.getBuyerId())
    );
}

然后另外一个类里:

@Component
public class PointsListener{

@EventListener
public void onPaid(OrderPaidEvent event){
        pointsService.grant(event.buyerId(), event.orderId());
    }
}

代码看着挺舒服。

订单模块不用依赖积分模块,publish 一下,监听器自己处理,甚至还有点"事件驱动架构"的味道。

问题是,Spring Event 默认根本不是你脑子里那个 MQ。

publishEvent() 默认是同步调用。

也就是说,上面的真实调用链差不多是:

HTTP线程
  -> confirmPaid()
     -> publishEvent()
        -> PointsListener.onPaid()
           -> pointsService.grant()
     -> confirmPaid()继续执行

监听器没跑完,publishEvent() 就不会回来。

更麻烦的是,监听器抛 RuntimeException,异常可以直接沿调用栈往外冒。

而 confirmPaid() 上面正好还有一个 @Transactional。

于是就出现了很恶心的一幕:

订单支付逻辑本来没问题。

积分模块因为一条脏配置报错,订单事务跟着回滚。

业务那边看到的是"支付确认失败",你查半天支付代码,最后发现锅在一个监听器里。

这种解耦,我一般不认。

类之间确实没直接引用了,失败倒是绑得比以前还紧。

如果积分只是支付后的附加动作,我至少会先把事务边界拆开。

Spring 提供了 @TransactionalEventListener:

@Component
public class PaidAuditListener{

private final PaidAuditService auditService;

public PaidAuditListener(PaidAuditService auditService){
this.auditService = auditService;
    }

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void record(OrderPaidEvent event){
        auditService.append(event.orderId(), event.orderNo());
    }
}

这样监听器只在原事务提交成功之后触发。

订单都没提交,后面的动作也没必要跑。

但这里还有个坑。

AFTER_COMMIT 不是"开启一个新事务"。

原事务已经提交了,这时候你还想在监听器里继续写数据库,我一般不会赌当前事务资源的状态,直接把真正的写操作放进新事务:

@Service
public class PaidAuditService{

private final PaidAuditRepository repository;

public PaidAuditService(PaidAuditRepository repository){
this.repository = repository;
    }

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void append(Long orderId, String orderNo){
        repository.insert(orderId, orderNo, "PAID");
    }
}

这个地方代码多两行没什么。

事务边界看不清,线上排起来才是真的贵。

还有一种改法更常见:

@Async("bizEventExecutor")
@EventListener
public void onPaid(OrderPaidEvent event){
    pointsService.grant(event.buyerId(), event.orderId());
}

加个 @Async,接口确实不等监听器了。

但我看到这种改法,第一反应不是"好了",而是先找线程池配置。

因为你只是把坑从请求线程挪到了异步线程。

@Bean("bizEventExecutor")
public ThreadPoolTaskExecutor bizEventExecutor(){
    ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
    pool.setCorePoolSize(4);
    pool.setMaxPoolSize(12);
    pool.setQueueCapacity(300);
    pool.setThreadNamePrefix("biz-event-");
    pool.setRejectedExecutionHandler(
    new ThreadPoolExecutor.CallerRunsPolicy()
    );
    pool.initialize();
return pool;
}

线程数、队列长度、拒绝策略,一个都不能靠感觉。

不然流量一上来,队列堆满,你会看到这种日志:

TaskRejectedException:
Executor [biz-event-] did not accept task

然后问题又来了。

这个事件丢了怎么办?

应用重启了怎么办?

执行到一半机器挂了怎么办?

消费失败怎么重试?

怎么查某个订单的事件到底处理过没有?

如果这些问题你都开始认真回答,那我劝你别继续给 Spring Event 打补丁了。

你需要的八成已经是 MQ。

Spring Event 我现在用得比较克制。

同一个 JVM 内部,监听方挂了也无所谓,失败可以接受,数据不用持久化,主要目的是拆开模块依赖——这种场景用它很顺手。

比如刷新本地缓存、刷新内部状态、记录一些非关键指标。

但订单、支付、库存、退款、发券这种核心链路,只要你开始要求"不能丢、能重试、可追踪、跨实例",我不会拿 Spring Event 硬顶。

还有个细节我也比较嫌弃。

别把 JPA Entity、DO 之类的对象直接塞进 Event:

eventPublisher.publishEvent(order);

后面监听器一多,谁改了对象、谁触发了懒加载、事务什么时候结束,全搅在一起。

我更愿意发一个不可变快照:

public record OrderPaidEvent(
        Long orderId,
        String orderNo,
        Long buyerId
){}

事件里只放监听器真正需要的东西。

少一点"方便",后面就少一点玄学。

Spring Event 本身没什么问题。

真正容易出事的是,代码里写着 publishEvent(),脑子里想的却是 Kafka。

看到 Event,先问一句:这个监听器失败,能不能把当前业务一起带死?

这个问题答不清,我一般不会让它上线。




上一篇:2026年MX Linux稳居DistroWatch前三的Debian系发行版,强在哪
下一篇:Ubuntu 安装 node_exporter 实战:9100 端口、systemd 与防火墙排错
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-10-2 23:46 , Processed in 0.581317 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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