版权声明:如需转载,请注明出处 https://blog.csdn.net/whdxjbw/article/details/81706967 </div>
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-f57960eb32.css">
<div class="htmledit_views" id="content_views">
<h1><a name="t0"></a>写在前面</h1>
今天把之前在项目中使用 Redis 做异步消息队列的使用经验总结一下。首先明确使用目的,因为项目中,我们进行某个操作后可能后续会有一系列的其他耗时操作,但是我们不希望将主线程阻塞在此过程中,这时便可将其他操作异步化。举个栗子,当你给这篇博客点赞或评论的时候,博客系统会保存你的点赞评论信息,同时将此操作封装为事件发给异步消息队列,处理过程中会给我发个私信告诉我谁评论了我,或谁给我点了赞,这个发私信通知我的环节即为异步操作过程。
具体实现思路
主要分为如下几个部分:
- EventProducer:事件生产者,面向client,用户进行操作后如果会有后续异步操作,则会将用户此次操作的事件加入消息队列,fire/push 意思是点燃这个事件,至于它啥时候爆炸(被另一线程 handler 处理)主线程就不管了,主线程只关心 fire/push 操作。
- EventModel:事件模型,下图中的每个颜色小块代表一种事件模型,里面可以定义事件所属的种类、与该事件相关的对象id、具体内容等信息。即事件是由谁产生的、将要影响到谁、影响的内容是什么等...
- 消息队列:具体存放消息事件的数据结构,可以是Redis list,也可以是Java BlockingQueue等。这里选择Redis list实现。
- EentConsumer:另开线程对消息队列中的事件进行处理,我们需要在系统初始化后将所有的Handler注册进来(建立事件类型与Handler的映射),之后根据相应的事件找到对应的Handler进行处理。
- EventHandler:具体处理事件的接口,我们可以实现不同的事件处理业务的具体Handler。
具体过程图示如下:
代码实现
1、定义模型
-
// 文章
-
public
class Article {
-
private
int id;
-
private String articleName;
-
private
int ownerID;
-
private
int likeCount;
-
private String content;
-
// getter、setter
-
...
-
}
-
-
// 评论
-
public
class Comment {
-
private
int id;
-
private
int commentUserID;
-
private
int articleID;
-
private String content;
-
// getter、setter
-
...
-
}
-
-
// 私信类
-
public
class MessageLetter {
-
private
int id;
-
private
int fromUserID;
-
private
int toUserID;
-
private String content;
-
// getter、setter
-
...
-
}
-
-
// 用户
-
public
class User {
-
private
int id;
-
private String userName;
-
private String passWord;
-
// getter、setter
-
...
-
}
2、定义事件类型、事件模型
首先定义事件类型,评论事件、上传文件事件、发送邮件事件等,这里以评论事件为例。
-
public
enum EventType {
-
// 这里以评论事件类型作为演示
-
COMMENT,
-
LIKE,
-
UPLOAD_FILE,
-
MAIL
-
}
-
package com.bowen.BWQASystem.async;
-
-
import java.util.HashMap;
-
import java.util.Map;
-
-
// 事件模型
-
public
class EventModel {
-
/*
-
* 事件类型,不同的事件类型会触发调用不同的Handler,
-
* 一种事件类型可以注册多个Handler,对应于消息队列中
-
* 每个事件可能触发多个Handler异步去做其他事情。
-
**/
-
private EventType eventType;
-
// 触发事件用户ID
-
private
int triggerID;
-
// 事件影响的用户ID
-
private
int receiverID;
-
-
// 额外信息
-
private Map<String,String> extraInfo =
new HashMap<String,String>();
-
-
// 构造方法
-
public EventModel(EventType eventType) {
-
this.eventType = eventType;
-
}
-
-
public EventType getEventType() {
-
return eventType;
-
}
-
-
// 所有set方法,均返回当前调用此方法的对象,用于链式设置属性
-
public EventModel setEventType(EventType eventType) {
-
this.eventType = eventType;
-
return
this;
-
}
-
-
public int getTriggerID() {
-
return triggerID;
-
}
-
-
// 其他setter、getter略
-
...
-
}
3、定义处理各类事件接口(EventHandler)
-
public
interface EventHandler {
-
// 各个具体Handler处理事件过程
-
void doHandle(EventModel eventModel);
-
-
/* 一个Handler可以处理多种事件,如评论CommentHandler
-
* 可以处理基本的评论事件,
-
* 此方法用于注册handler到对应的事件类型上的时候用
-
**/
-
List<EventType> getSupportedEvents();
-
}
4、根据业务需求实现处理事件的Handler
这里我们需要实现一个Handler,它能够对用户在某篇文章评论区下评论或点赞的事件进行处理,此Handler会以私信的形式通知对方。告诉他,他的文章被评论(或点赞了)。
注意:我们每个Handler都需要用@Component注解标注,因为后面在注册环节会利用 Spring context 在 IOC 容器里来寻找继承自 EventHandler 的每一个真实 Handler。
-
/*
-
* 对评论、点赞事件执行发私信操作
-
* */
-
@Component
-
public
class CommentAndLikeHandler implements EventHandler {
-
-
@Autowired
-
MessageLetterService messageLetterService;
-
@Autowired
-
UserService userService;
-
-
@Override
-
public void doHandle(EventModel eventModel) {
-
-
// 1、自动生成一私信
-
MessageLetter messageLetter =
new MessageLetter();
-
// 触发此事件的用户id(即评论者id)
-
messageLetter.setFromUserID(eventModel.getTriggerID());
-
messageLetter.setToUserID(eventModel.getReceiverID());
-
-
//messageLetter.setContent(eventModel.getExtraInfo().get("msgContent"));
-
// 评论COMMENT事件
-
// 自动发的私信中含有的文章题目、具体评论类容等信息存在extraInfo这个HashMap里
-
if(eventModel.getEventType() == EventType.COMMENT){
-
messageLetter.setContent(
"用户" + eventModel.getTriggerID() +
-
"在你文章" + eventModel.getExtraInfo().get(
"articleName") +
-
"评论中说到" + eventModel.getExtraInfo().get(
"commentContent"));
-
// 发私信给被评论的人
-
messageLetterService.addMessageLetter(messageLetter);
-
-
// // 2、目标用户积分加 1
-
// User user = userService.getUserByUserID(String.valueOf(eventModel.getReceiverID()));
-
// user.setScore(user.getScore() + 1 );
-
// userService.save(user);
-
}
-
-
// 点赞LIKE事件
-
if(eventModel.getEventType() == EventType.LIKE){
-
messageLetter.setContent(
"用户" + eventModel.getTriggerID() +
-
"给你文章《" +
-
eventModel.getExtraInfo().get(
"articleName")
-
+
"》点赞了,去瞅瞅");
-
messageLetterService.addMessageLetter(messageLetter);
-
}
-
}
-
-
@Override
-
public List<EventType> getSupportedEvents() {
-
// 此handler仅处理COMMENT评论事件、LIKE点赞事件
-
return Arrays.asList(EventType.COMMENT, EventType.LIKE);
-
}
-
}
5、实现消息队列生产者
因为生产者需要将事件 push 到消息队列中,在此之前我们需要初始化 Redis 连接池,而在消费端也需要对 Redis 进行操作,故我们将初始化 Redis 写成一个 Service ,在需要它的地方注入即可。
JedisUtilService.java
-
@Service
-
public
class JedisUtilService implements InitializingBean {
-
-
// 异步消息队列在Redis中的list的key名
-
public
static
final String EVENT_QUEUE_KEY =
"EventQueue";
-
private JedisPool jedisPool;
-
-
@Override
-
public void afterPropertiesSet() throws Exception {
-
jedisPool =
new JedisPool(
"redis://localhost:6379/0");
-
}
-
-
/*
-
* list的push操作(事件入队列)
-
* @Param String key list的名字,即key
-
* @Param String value 将要放入的值value
-
*/
-
public long lpush(String key, String value){
-
Jedis jedis =
null;
-
try {
-
jedis = jedisPool.getResource();
-
return jedis.lpush(key, value);
-
}
catch (Exception e){
-
return -
1;
-
}
finally {
-
if(jedis !=
null) jedis.close();
-
}
-
}
-
-
/*
-
* list的brpop操作
-
* @Param int timeout 超时时间
-
* @Param String key list对应的key
-
* @reutrn List<String> 返回list的名字key和对应的元素
-
*/
-
public List<String> brpop(int timeout, String key){
-
Jedis jedis =
null;
-
try {
-
jedis = jedisPool.getResource();
-
return jedis.brpop(timeout, key);
-
}
catch (Exception e){
-
return
null;
-
}
finally {
-
if(jedis !=
null) jedis.close();
-
}
-
}
-
-
}
生产者会在 Controller 层某个业务功能被调用时,通过调用 Redis api,将相应的事件加入异步消息队列,具体实现如下:
-
public
class EventProducer{
-
@Autowired
-
JedisUtilService jedisUtilService;
-
-
public boolean pushEvent(EventModel eventModel){
-
// 序列化
-
String eventJson = JSONObject.toJSONString(eventModel);
-
// 加入key为"EventQueue"的list里
-
jedisUtilService.lpush(JedisUtilService.EVENT_QUEUE_KEY, eventJson);
-
return
true;
-
}
-
}
6、实现异步消息队列消费者
消费者端会开启一额外线程去从异步消息队列中获取事件,交给对应的 Handler 处理。在此之前我们需要在 所有 Bean 加载完毕后,找到每个 Handler ,并为每个 Handler 注册到相应的事件上,以便后续处理。
(一个事件类型可以对应多个handler处理,一个handler可以处理多个类型事件)
-
public
class EventConsumer implements InitializingBean,ApplicationContextAware {
-
-
// 用来寻找Handler
-
private ApplicationContext applicationContext;
-
// 注册事件与Handler映射
-
// (一个事件类型可以对应多个handler处理,一个handler可以处理多个类型事件)
-
private Map<EventType, List<EventHandler>> eventConfig =
new HashMap<>();
-
-
@Autowired
-
JedisUtilService jedisUtilService;
-
-
// 注册Handler到相应的事件
-
@Override
-
public void afterPropertiesSet() throws Exception {
-
Map<String, EventHandler> handlerBeans = applicationContext.getBeansOfType(EventHandler.class);
-
if(handlerBeans !=
null){
-
// 依次遍历每个找到的Handler(被@Component注解标注的)
-
for (Map.Entry<String, EventHandler> entry : handlerBeans.entrySet()) {
-
// 获取每个Handler支持的事件Event列表
-
List<EventType> supportedEventTypeList = entry.getValue().getSupportedEvents();
-
// 将每一个Handler支持的事件Event注册到config map中
-
for(EventType eventType : supportedEventTypeList){
-
if(!eventConfig.containsKey(eventType)){
-
eventConfig.put(eventType,
new ArrayList<EventHandler>());
-
}
-
eventConfig.get(eventType).add(entry.getValue());
-
}
-
}
-
}
-
-
// 消费线程池
-
ExecutorService executorService = Executors.newCachedThreadPool();
-
// 消费异步消息队列
-
executorService.submit((Runnable) () ->{
-
while (
true){
-
// 弹出一元素
-
List<String> eleList = jedisUtilService.brpop(
0, JedisUtilService.EVENT_QUEUE_KEY);
-
EventModel eventModel = JSON.parseObject(eleList.get(
1), EventModel.class);
-
if(eventConfig.containsKey(eventModel.getEventType())) {
-
for (EventHandler eventHandler : eventConfig.get(eventModel.getEventType())){
-
// 执行注册到该事件的每一个Handler
-
eventHandler.doHandle(eventModel);
-
}
-
}
-
}
-
});
-
-
// 消费其他事件队列(根据自己业务)
-
// 略...
-
// executorService.submit(...)
-
-
}
-
-
@Override
-
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
-
this.applicationContext = applicationContext;
-
}
-
}
具体使用
我们直接在评论相关的 Controller 中编写评论逻辑代码,将评论内容保存后,我们直接生成一事件类型为 COMMENT 类型的事件,并将它加入异步队列中,供后台线程去处理后续操作(发私信)。CommentAndLikeHandler 便会对此评论事件进行处理。关于点赞事件的业务逻辑大致类似,这里就不赘述了,评论业务处理代码如下:
-
@Controller
-
public
class ArticalController {
-
-
@Autowired
-
CommentService commentService;
-
@Autowired
-
ArticleService articleService;
-
@Autowired
-
UserHolder userHolder;
-
@Autowired
-
EventProducer eventProducer;
-
-
@RequestMapping(path = {
"/commentArticle"}, method = RequestMethod.POST)
-
public void commentArticle(@RequestParam("articleID") int id,
-
@RequestParam("commentContent") String commentContent){
-
// 当前用户id
-
int currentUserID = userHolder.getUsers().getId();
-
Comment comment =
new Comment();
-
comment.setArticleID(id);
-
comment.setCommentUserID(currentUserID);
-
comment.setContent(commentContent);
-
commentService.addComment(comment);
-
-
// 当前文章
-
Article article = articleService.findArticleByID(id);
-
// 后续生成的私信内容需要这些额外信息
-
HashMap<String, String> articleInfo =
new HashMap();
-
articleInfo.put(
"articleName", article.getArticleName());
-
articleInfo.put(
"commentContent", article.getContent());
-
-
// 后续发私信操作加入异步队列
-
eventProducer.pushEvent(
new EventModel(EventType.COMMENT)
-
.setTriggerID(currentUserID).setReceiverID(article.getOwnerID())
-
.setExtraInfo(articleInfo));
-
}
-
-
}
总结
1、这里利用了 Redis 的 list 数据结构,它支持 lpush 添加元素,blpop(从队尾),brpop(从队头,先加入的元素排在队头) 等阻塞式获取元素操作。
2、通过将事件模型对象序列化为 JSON 串的方式,将其保存至 Redis 数据库。
3、通过生产者消费者模型实现异步消息队列。
4、当然 Redis 还有其他各个方面的应用,如排行榜、验证码(定时失效)、PageView、异步消息队列、实时热点等等,通过选择不同的数据结构即能实现。