利用Redis实现异步消息队列优化系统性能 (Redis高级应用)

12 篇文章 0 订阅
8 篇文章 0 订阅
				版权声明:如需转载,请注明出处					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 做异步消息队列的使用经验总结一下。首先明确使用目的,因为项目中,我们进行某个操作后可能后续会有一系列的其他耗时操作,但是我们不希望将主线程阻塞在此过程中,这时便可将其他操作异步化。举个栗子,当你给这篇博客点赞或评论的时候,博客系统会保存你的点赞评论信息,同时将此操作封装为事件发给异步消息队列,处理过程中会给我发个私信告诉我谁评论了我,或谁给我点了赞,这个发私信通知我的环节即为异步操作过程。

具体实现思路

主要分为如下几个部分:

  1. EventProducer:事件生产者,面向client,用户进行操作后如果会有后续异步操作,则会将用户此次操作的事件加入消息队列,fire/push 意思是点燃这个事件,至于它啥时候爆炸(被另一线程 handler 处理)主线程就不管了,主线程只关心 fire/push 操作。
  2. EventModel:事件模型,下图中的每个颜色小块代表一种事件模型,里面可以定义事件所属的种类、与该事件相关的对象id、具体内容等信息。即事件是由谁产生的、将要影响到谁、影响的内容是什么等...
  3. 消息队列:具体存放消息事件的数据结构,可以是Redis list,也可以是Java BlockingQueue等。这里选择Redis list实现。
  4. EentConsumer:另开线程对消息队列中的事件进行处理,我们需要在系统初始化后将所有的Handler注册进来(建立事件类型与Handler的映射),之后根据相应的事件找到对应的Handler进行处理。
  5. EventHandler:具体处理事件的接口,我们可以实现不同的事件处理业务的具体Handler。

具体过程图示如下:

 

代码实现

1、定义模型


 
 
  1. // 文章
  2. public class Article {
  3. private int id;
  4. private String articleName;
  5. private int ownerID;
  6. private int likeCount;
  7. private String content;
  8. // getter、setter
  9. ...
  10. }
  11. // 评论
  12. public class Comment {
  13. private int id;
  14. private int commentUserID;
  15. private int articleID;
  16. private String content;
  17. // getter、setter
  18. ...
  19. }
  20. // 私信类
  21. public class MessageLetter {
  22. private int id;
  23. private int fromUserID;
  24. private int toUserID;
  25. private String content;
  26. // getter、setter
  27. ...
  28. }
  29. // 用户
  30. public class User {
  31. private int id;
  32. private String userName;
  33. private String passWord;
  34. // getter、setter
  35. ...
  36. }

 2、定义事件类型、事件模型

首先定义事件类型,评论事件、上传文件事件、发送邮件事件等,这里以评论事件为例。


 
 
  1. public enum EventType {
  2. // 这里以评论事件类型作为演示
  3. COMMENT,
  4. LIKE,
  5. UPLOAD_FILE,
  6. MAIL
  7. }

 
 
  1. package com.bowen.BWQASystem.async;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. // 事件模型
  5. public class EventModel {
  6. /*
  7. * 事件类型,不同的事件类型会触发调用不同的Handler,
  8. * 一种事件类型可以注册多个Handler,对应于消息队列中
  9. * 每个事件可能触发多个Handler异步去做其他事情。
  10. **/
  11. private EventType eventType;
  12. // 触发事件用户ID
  13. private int triggerID;
  14. // 事件影响的用户ID
  15. private int receiverID;
  16. // 额外信息
  17. private Map<String,String> extraInfo = new HashMap<String,String>();
  18. // 构造方法
  19. public EventModel(EventType eventType) {
  20. this.eventType = eventType;
  21. }
  22. public EventType getEventType() {
  23. return eventType;
  24. }
  25. // 所有set方法,均返回当前调用此方法的对象,用于链式设置属性
  26. public EventModel setEventType(EventType eventType) {
  27. this.eventType = eventType;
  28. return this;
  29. }
  30. public int getTriggerID() {
  31. return triggerID;
  32. }
  33. // 其他setter、getter略
  34. ...
  35. }

3、定义处理各类事件接口(EventHandler)


 
 
  1. public interface EventHandler {
  2. // 各个具体Handler处理事件过程
  3. void doHandle(EventModel eventModel);
  4. /* 一个Handler可以处理多种事件,如评论CommentHandler
  5. * 可以处理基本的评论事件,
  6. * 此方法用于注册handler到对应的事件类型上的时候用
  7. **/
  8. List<EventType> getSupportedEvents();
  9. }

4、根据业务需求实现处理事件的Handler

这里我们需要实现一个Handler,它能够对用户在某篇文章评论区下评论或点赞的事件进行处理,此Handler会以私信的形式通知对方。告诉他,他的文章被评论(或点赞了)。

注意:我们每个Handler都需要用@Component注解标注,因为后面在注册环节会利用 Spring context 在 IOC 容器里来寻找继承自 EventHandler 的每一个真实 Handler。


 
 
  1. /*
  2. * 对评论、点赞事件执行发私信操作
  3. * */
  4. @Component
  5. public class CommentAndLikeHandler implements EventHandler {
  6. @Autowired
  7. MessageLetterService messageLetterService;
  8. @Autowired
  9. UserService userService;
  10. @Override
  11. public void doHandle(EventModel eventModel) {
  12. // 1、自动生成一私信
  13. MessageLetter messageLetter = new MessageLetter();
  14. // 触发此事件的用户id(即评论者id)
  15. messageLetter.setFromUserID(eventModel.getTriggerID());
  16. messageLetter.setToUserID(eventModel.getReceiverID());
  17. //messageLetter.setContent(eventModel.getExtraInfo().get("msgContent"));
  18. // 评论COMMENT事件
  19. // 自动发的私信中含有的文章题目、具体评论类容等信息存在extraInfo这个HashMap里
  20. if(eventModel.getEventType() == EventType.COMMENT){
  21. messageLetter.setContent( "用户" + eventModel.getTriggerID() +
  22. "在你文章" + eventModel.getExtraInfo().get( "articleName") +
  23. "评论中说到" + eventModel.getExtraInfo().get( "commentContent"));
  24. // 发私信给被评论的人
  25. messageLetterService.addMessageLetter(messageLetter);
  26. // // 2、目标用户积分加 1
  27. // User user = userService.getUserByUserID(String.valueOf(eventModel.getReceiverID()));
  28. // user.setScore(user.getScore() + 1 );
  29. // userService.save(user);
  30. }
  31. // 点赞LIKE事件
  32. if(eventModel.getEventType() == EventType.LIKE){
  33. messageLetter.setContent( "用户" + eventModel.getTriggerID() +
  34. "给你文章《" +
  35. eventModel.getExtraInfo().get( "articleName")
  36. + "》点赞了,去瞅瞅");
  37. messageLetterService.addMessageLetter(messageLetter);
  38. }
  39. }
  40. @Override
  41. public List<EventType> getSupportedEvents() {
  42. // 此handler仅处理COMMENT评论事件、LIKE点赞事件
  43. return Arrays.asList(EventType.COMMENT, EventType.LIKE);
  44. }
  45. }

5、实现消息队列生产者

因为生产者需要将事件 push 到消息队列中,在此之前我们需要初始化 Redis 连接池,而在消费端也需要对 Redis 进行操作,故我们将初始化 Redis 写成一个 Service ,在需要它的地方注入即可。

JedisUtilService.java

 
 
  1. @Service
  2. public class JedisUtilService implements InitializingBean {
  3. // 异步消息队列在Redis中的list的key名
  4. public static final String EVENT_QUEUE_KEY = "EventQueue";
  5. private JedisPool jedisPool;
  6. @Override
  7. public void afterPropertiesSet() throws Exception {
  8. jedisPool = new JedisPool( "redis://localhost:6379/0");
  9. }
  10. /*
  11. * list的push操作(事件入队列)
  12. * @Param String key list的名字,即key
  13. * @Param String value 将要放入的值value
  14. */
  15. public long lpush(String key, String value){
  16. Jedis jedis = null;
  17. try {
  18. jedis = jedisPool.getResource();
  19. return jedis.lpush(key, value);
  20. } catch (Exception e){
  21. return - 1;
  22. } finally {
  23. if(jedis != null) jedis.close();
  24. }
  25. }
  26. /*
  27. * list的brpop操作
  28. * @Param int timeout 超时时间
  29. * @Param String key list对应的key
  30. * @reutrn List<String> 返回list的名字key和对应的元素
  31. */
  32. public List<String> brpop(int timeout, String key){
  33. Jedis jedis = null;
  34. try {
  35. jedis = jedisPool.getResource();
  36. return jedis.brpop(timeout, key);
  37. } catch (Exception e){
  38. return null;
  39. } finally {
  40. if(jedis != null) jedis.close();
  41. }
  42. }
  43. }

生产者会在 Controller 层某个业务功能被调用时,通过调用 Redis api,将相应的事件加入异步消息队列,具体实现如下:


 
 
  1. public class EventProducer{
  2. @Autowired
  3. JedisUtilService jedisUtilService;
  4. public boolean pushEvent(EventModel eventModel){
  5. // 序列化
  6. String eventJson = JSONObject.toJSONString(eventModel);
  7. // 加入key为"EventQueue"的list里
  8. jedisUtilService.lpush(JedisUtilService.EVENT_QUEUE_KEY, eventJson);
  9. return true;
  10. }
  11. }

6、实现异步消息队列消费者

消费者端会开启一额外线程去从异步消息队列中获取事件,交给对应的 Handler 处理。在此之前我们需要在 所有 Bean 加载完毕后,找到每个 Handler ,并为每个 Handler 注册到相应的事件上,以便后续处理。

(一个事件类型可以对应多个handler处理,一个handler可以处理多个类型事件)


 
 
  1. public class EventConsumer implements InitializingBean,ApplicationContextAware {
  2. // 用来寻找Handler
  3. private ApplicationContext applicationContext;
  4. // 注册事件与Handler映射
  5. // (一个事件类型可以对应多个handler处理,一个handler可以处理多个类型事件)
  6. private Map<EventType, List<EventHandler>> eventConfig = new HashMap<>();
  7. @Autowired
  8. JedisUtilService jedisUtilService;
  9. // 注册Handler到相应的事件
  10. @Override
  11. public void afterPropertiesSet() throws Exception {
  12. Map<String, EventHandler> handlerBeans = applicationContext.getBeansOfType(EventHandler.class);
  13. if(handlerBeans != null){
  14. // 依次遍历每个找到的Handler(被@Component注解标注的)
  15. for (Map.Entry<String, EventHandler> entry : handlerBeans.entrySet()) {
  16. // 获取每个Handler支持的事件Event列表
  17. List<EventType> supportedEventTypeList = entry.getValue().getSupportedEvents();
  18. // 将每一个Handler支持的事件Event注册到config map中
  19. for(EventType eventType : supportedEventTypeList){
  20. if(!eventConfig.containsKey(eventType)){
  21. eventConfig.put(eventType, new ArrayList<EventHandler>());
  22. }
  23. eventConfig.get(eventType).add(entry.getValue());
  24. }
  25. }
  26. }
  27. // 消费线程池
  28. ExecutorService executorService = Executors.newCachedThreadPool();
  29. // 消费异步消息队列
  30. executorService.submit((Runnable) () ->{
  31. while ( true){
  32. // 弹出一元素
  33. List<String> eleList = jedisUtilService.brpop( 0, JedisUtilService.EVENT_QUEUE_KEY);
  34. EventModel eventModel = JSON.parseObject(eleList.get( 1), EventModel.class);
  35. if(eventConfig.containsKey(eventModel.getEventType())) {
  36. for (EventHandler eventHandler : eventConfig.get(eventModel.getEventType())){
  37. // 执行注册到该事件的每一个Handler
  38. eventHandler.doHandle(eventModel);
  39. }
  40. }
  41. }
  42. });
  43. // 消费其他事件队列(根据自己业务)
  44. // 略...
  45. // executorService.submit(...)
  46. }
  47. @Override
  48. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  49. this.applicationContext = applicationContext;
  50. }
  51. }

具体使用

我们直接在评论相关的 Controller 中编写评论逻辑代码,将评论内容保存后,我们直接生成一事件类型为 COMMENT 类型的事件,并将它加入异步队列中,供后台线程去处理后续操作(发私信)。CommentAndLikeHandler 便会对此评论事件进行处理。关于点赞事件的业务逻辑大致类似,这里就不赘述了,评论业务处理代码如下:


 
 
  1. @Controller
  2. public class ArticalController {
  3. @Autowired
  4. CommentService commentService;
  5. @Autowired
  6. ArticleService articleService;
  7. @Autowired
  8. UserHolder userHolder;
  9. @Autowired
  10. EventProducer eventProducer;
  11. @RequestMapping(path = { "/commentArticle"}, method = RequestMethod.POST)
  12. public void commentArticle(@RequestParam("articleID") int id,
  13. @RequestParam("commentContent") String commentContent){
  14. // 当前用户id
  15. int currentUserID = userHolder.getUsers().getId();
  16. Comment comment = new Comment();
  17. comment.setArticleID(id);
  18. comment.setCommentUserID(currentUserID);
  19. comment.setContent(commentContent);
  20. commentService.addComment(comment);
  21. // 当前文章
  22. Article article = articleService.findArticleByID(id);
  23. // 后续生成的私信内容需要这些额外信息
  24. HashMap<String, String> articleInfo = new HashMap();
  25. articleInfo.put( "articleName", article.getArticleName());
  26. articleInfo.put( "commentContent", article.getContent());
  27. // 后续发私信操作加入异步队列
  28. eventProducer.pushEvent( new EventModel(EventType.COMMENT)
  29. .setTriggerID(currentUserID).setReceiverID(article.getOwnerID())
  30. .setExtraInfo(articleInfo));
  31. }
  32. }

总结

1、这里利用了 Redis 的 list 数据结构,它支持 lpush 添加元素,blpop(从队尾),brpop(从队头,先加入的元素排在队头) 等阻塞式获取元素操作。

2、通过将事件模型对象序列化为 JSON 串的方式,将其保存至 Redis 数据库。

3、通过生产者消费者模型实现异步消息队列。

4、当然 Redis 还有其他各个方面的应用,如排行榜、验证码(定时失效)、PageView、异步消息队列、实时热点等等,通过选择不同的数据结构即能实现。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值