Spring Boot 中什么时候使用事件发布订阅?以文章阅读量 +1 为例
在开发 Spring Boot 项目时,我们经常会遇到这样一种场景:
主业务已经完成了,但是还需要顺便做一些"额外"的事情。
例如:
- 用户阅读文章,阅读量 +1;
- 用户注册成功,发送欢迎邮件;
- 用户下单成功,发送短信通知;
- 用户登录成功,记录登录日志;
- 用户发表评论,发送消息提醒。
很多人的第一反应就是把这些逻辑全部写到一个方法里面。
例如文章详情接口:
public ArticleVO detail(Long articleId) {
// 查询文章
ArticleVO article = articleMapper.selectById(articleId);
// 阅读量 +1
articleMapper.increaseReadCount(articleId);
return article;
}
功能能够实现,但随着业务越来越复杂,问题也开始出现。
一、为什么不要把所有逻辑写在一起?
假设以后需求不断增加:
用户阅读文章后需要:
- 阅读量 +1
- 记录阅读历史
- 更新文章热度
- 记录用户行为
- 推送推荐算法
- 给作者增加积分
那么代码很快会变成:
public ArticleVO detail(Long articleId) {
ArticleVO article = queryArticle(articleId);
increaseReadCount(articleId);
saveReadHistory(articleId);
updateHotScore(articleId);
saveBehavior(articleId);
addAuthorPoint(articleId);
return article;
}
虽然代码没有问题,但是职责已经越来越混乱。
文章详情接口既负责查询文章,又负责处理各种后续业务。
任何一个需求发生变化,都可能修改这里。
这明显违背了单一职责原则。
二、什么是事件发布订阅?
Spring 提供了一套非常轻量的事件机制。
它的思想其实非常简单:
我只负责告诉别人:"发生了一件事情。"
至于谁对这件事情感兴趣,由监听者自己决定。
例如:
文章被阅读
│
▼
发布 ArticleReadEvent
所有监听这个事件的模块都会收到通知。
例如:
ArticleReadEvent
│
┌─────────┬──────────┬──────────┐
▼ ▼ ▼ ▼
阅读量+1 阅读历史 热度计算 行为统计
这样,文章模块根本不需要知道后面还有哪些业务。
它只负责:
文章被阅读了。
三、Spring 中如何发布事件?
Spring 提供了:
ApplicationEventPublisher
发布事件:
publisher.publishEvent(new ArticleReadEvent(articleId));
然后对应业务监听事件:
@EventListener
public void handleReadEvent(ArticleReadEvent event){
increaseReadCount(event.getArticleId());
}
整个过程就是:
查询文章
│
▼
发布事件
│
▼
监听器收到事件
│
▼
阅读量 +1
四、事件机制已经异步了吗?
很多人第一次接触 Spring Event 都会误以为:
发布事件以后就是异步执行。
其实不是。
默认情况下:
publisher.publishEvent(event);
仍然是在当前线程执行。
流程其实是:
Tomcat线程
查询文章
│
▼
发布事件
│
▼
执行Listener
│
▼
阅读量+1
│
▼
返回页面
虽然代码解耦了,但是性能没有任何提升。
五、为什么还要配合线程池?
真正希望的是:
文章能够先返回。
至于阅读量什么时候增加,其实用户根本感觉不到。
所以监听器可以改成:
@Async("articleThreadPool")
@EventListener
public void handleReadEvent(ArticleReadEvent event){
increaseReadCount(event.getArticleId());
}
这样流程就变成:
Tomcat线程
查询文章
│
▼
发布事件
│
├──────────────► 线程池
│ │
▼ ▼
返回文章 阅读量+1
主线程不用等待阅读量更新完成。
页面响应速度也会更快。
六、为什么不用 new Thread()?
有同学可能会问:
为什么不用:
new Thread(() -> {
increaseReadCount(id);
}).start();
因为线程创建和销毁都有成本。
如果文章访问量很高:
10000 个请求
↓
10000 个线程
这显然是不现实的。
线程池的作用就是:
提前准备好几个线程
任务来了直接复用
不仅性能更好,也方便统一管理。
七、哪些业务适合使用事件发布订阅?
有一个很简单的判断标准:
这件事情不是主业务,但是必须完成。
例如:
✅ 用户注册
- 发送欢迎邮件
- 发放新人优惠券
- 记录注册日志
✅ 用户登录
- 更新登录时间
- 保存登录日志
- 更新在线状态
✅ 用户阅读文章
- 阅读量 +1
- 更新热度
- 保存阅读历史
✅ 用户发表评论
- 通知作者
- 更新评论数量
- 敏感词检测
这些业务都可以拆成事件。
八、哪些场景不建议使用?
如果主业务必须等待结果,就不要使用事件。
例如:
用户下单:
创建订单
↓
扣减库存
↓
支付金额
↓
返回成功
库存扣减失败,订单就不能创建成功。
这种属于核心业务流程。
应该同步执行。
否则容易造成数据不一致。
九、什么时候使用线程池?
事件并不一定非要异步。
可以简单记住下面这张表:
| 场景 | 是否建议异步 |
|---|---|
| 记录日志 | ✅ |
| 阅读量统计 | ✅ |
| 用户行为统计 | ✅ |
| 发邮件 | ✅ |
| 发短信 | ✅ |
| 更新推荐算法 | ✅ |
| 创建订单 | ❌ |
| 扣库存 | ❌ |
| 支付 | ❌ |
如果用户需要等待执行结果,就不要异步。
如果只是一些"后置处理",就非常适合交给线程池。
十、总结
Spring 的事件发布订阅,本质上是一种解耦业务的设计思想。
它让发布者不需要关心后续有哪些处理逻辑,只需要告诉系统:
某件事情发生了。
而自定义线程池,则进一步让这些后续业务脱离主线程执行,提高接口响应速度。
实际开发中,这种组合经常用于:
- 阅读量统计
- 登录日志
- 操作日志
- 消息通知
- 邮件发送
- 用户行为统计
如果以后项目中遇到"主业务完成后,还需要做很多附加操作"的场景,不妨考虑使用 Spring Event + 自定义线程池 来完成,它既能降低模块耦合,又能提升系统的可维护性和响应性能。