项目1在线交流平台-5.Kafka构建异步消息系统-4.使用kafak发送系统通知


参考牛客网高级项目教程

尚硅谷kafka教学笔记

功能需求

在这里插入图片描述

  • 对指定用户评论、点赞、关注等事件触发后,系统要向指定用户发送通知消息
    • 因为事件比较多,也为了方便扩展,解耦-使用kafka消息队列,向消息队列中发布数据
    • 消费topic,即需要将发布的信息数据存入mysql数据库,采用异步方式,提高响应速度,也起到消峰作用
  • kafka发送消息做了设计优化,以事件为驱动
    • 封装事件对象
      • 因要发送的消息内容相似,可以将消息封装成一个事件对象
      • 将触发事件中包含消息的所有数据进行封装,方便扩展
    • 开发事件的生产者
      • 这样,发送的不只是一条消息,而是一个以topic分类的事件对象,方便消费者统一处理topic中的数据
      • 以JSON格式储存对象
    • 开发事件的消费者
      • 最终消费者会将队列中存入的事件对象数据(消息)取出,创建message对象入库保存

在这里插入图片描述

1. 封装事件对象

  • 封装事件触发者、触发对象一般均有的属性信息
  • 用map来接收其他数据信息-例如对评论和点赞来说,需要帖子的id
  • 修改各自的set方法,设置返回类型为Event当前类,方便对当前对象重复设置调用
    • map数据,传入key,value,向map中添加数据,返回类型当前对象,方便多次调用添加键值对
    • 注意:一定要先初始化一个实例
package com.nowcoder.community.entity;

import java.util.Map;

public class Event {
    // kafka服务器要识别的topic事件类型
    private String topic; 
    // 事件触发者信息
    private int fromUserId;
    // 事件触发对象的信息
    private int entityType;
    private int entityId;
    private int entityUserId;
    // 其他信息-用map封装保存
    private Map<String, Object> data = new HashMap<>();

    public int getFromUserId() {
        return fromUserId;
    }

    /**
     * 修改各自的set方法,设置返回类型为Event当前类,方便对当前对象重复设置调用
     * @param fromUserId
     * @return  返回Event类型对象
     */
    public Event setFromUserId(int fromUserId) {
        this.fromUserId = fromUserId;
        return this;
    }
    ...

    /**
     * map数据,传入key,value,向map中添加数据,返回类型当前对象,方便多次调用添加键值对
     * @param key       传入map的key
     * @param value     传入map的value
     * @return
     */
    public Event setData(String key, Object value) {
        this.data.put(key, value);
        return this;
    }
}

2. 开发事件的生产者

定义事件主题常量

/**
 * kafka主题-事件:帖子-评论
 */
String TOPIC_COMMENT = "comment";

/**
 * kafka主题-事件:关注
 */
String TOPIC_FOLLOW = "follow";

/**
 * kafka主题-事件:点赞
 */
String TOPIC_LIKE = "like";

/**
 * 系统用户id
 */
int SYSTEM_USER_ID = 1;

生产者发送消息

JSONObject.toJSONString(event)
  • 将消息发送到指定主题上
  • 发送的是事件对象,发送时,转为JSON字符串的格式发送
@Component
public class EventProducer implements CommunityConstant {
    @Autowired
    KafkaTemplate kafkaTemplate;

    /**
     * 将消息发送到指定主题上
     * @param event      要发送的消息封装成的主题对象
     */
    public void sendEvent(Event event) {
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }
}

3. 消费者获取消息,并异步入库

  • 1.边界条件:先检查有无取到消息
  • 2.将拿到的消息恢复成Object类型,方便操作
  • 3.用拿到的数据创建Message对象,入库
    • Message的content,是要通知的内容,由消息中的数据拼接而成

Map.Entry<String, Object> entry : event.getData().entrySet()

  • event中的其他封装在map中的数据,也都一一拿出来放进content中
@Component
public class EventConsumer implements CommunityConstant {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    KafkaTemplate kafkaTemplate;
    
    @Autowired
    MessageService messageService;

    /**
     * 被动订阅消息,并将消息入库
     * @param record
     */
    @KafkaListener(topics = {TOPIC_FOLLOW, TOPIC_COMMENT, TOPIC_LIKE})
    public void handleMessage(ConsumerRecord record) {
        // 1.边界条件:先检查有无取到消息
        if (record == null || record.value() == null) {
            logger.error("消息的内容为空!");
            return;
        }
        // 2.将拿到的消息恢复成Object类型,方便操作
        Event event = JSONObject.parseObject(record.value().toString(), Event.class);
        if(event == null) {
            logger.error("消息的格式错了!");
            return;
        }
        // 3.用拿到的数据创建Message对象,入库
        Message message = new Message();
        message.setFromId(SYSTEM_USER_ID);
        message.setToId(event.getEntityUserId());
        message.setConversationId(event.getTopic());
        message.setCreateTime(new Date());
        // Message的content,是要通知的内容,由消息中的数据拼接而成
        // xxx了您的xxx
        Map<String, Object> content = new HashMap<>();
        content.put("userId", event.getFromUserId());
        content.put("entityType", event.getEntityType());
        content.put("entityId", event.getEntityId());
        // event中的其他数据,也都放进content中
        if (event.getData() != null) {
            for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
                content.put(entry.getKey(), entry.getValue());
            }
        }
        message.setContent(JSONObject.toJSONString(content));
        
        // 入库-调用service层业务代码-增加了过滤器
        messageService.addMessage(message);
    }
}

4. 在controller层更新系统发送通知代码

1. 评论

  • 添加完评论后,系统向目标用户发送通知-触发评论事件
    • 当前评论所属的帖子id-属于其他信息
      • 为了在显示系统通知页面能链接到指定的帖子详情页面
    • 事件对象的作者-分情况判定-帖子作者、评论作者
@RequestMapping(value = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
    // 除请求中需要写的评论内容外,需提供其他素材
    comment.setCreateTime(new Date());
    comment.setUserId(hostHolder.getUser().getId());
    comment.setStatus(0);
    // 将数据交给service处理
    commentService.addComment(comment);

    // 添加完评论后,系统向目标用户发送通知-触发评论事件
    // 封装评论事件信息
    Event event = new Event()
            .setTopic(TOPIC_COMMENT)
            .setFromUserId(hostHolder.getUser().getId())
            .setEntityType(comment.getEntityType()) // 评论的可以是帖子,回帖,回复
            .setEntityId(comment.getEntityId())
            .setData("postId", discussPostId);  // 当前评论所属的帖子id
    // 事件对象的作者-分情况判定-帖子作者、评论作者
    if (comment.getEntityType() == ENTITY_TYPE_POST) {
        DiscussPost target = discussPostService.selectPostById(comment.getEntityId());
        event.setEntityUserId(target.getUserId());
    } else (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
        Comment target = commentService.selectCommentById(comment.getEntityId());
        event.setEntityUserId(target.getUserId());
    }
    // 将信息发送到消息队列中
    eventProducer.sendEvent(event);
    return "redirect:/discuss/detail/" + discussPostId;
}

2.点赞

  • 触发点赞事件后-系统向目标用户发送通知
  • 为了获取当前点赞实体所属的帖子,需要传入帖子id,因此,模板页面和js也需要相应修改
/**
 * 处理点赞的异步请求
 * @param entityType
 * @param entityId
 * @return  json字符串,不传递msg,如果有问题,直接在网页alert提示
 */
@RequestMapping(value = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId, int postId) {
    // 用来封装信息的map
    Map<String, Object> map = new HashMap<>();
    // 权限-统一管理,先获取当前用户
    User user = hostHolder.getUser();
    // 点赞事件处理
    likeService.like(user.getId(), entityType, entityId, entityUserId);
    // 点赞数量获取
    long likeCount = likeService.findEntityLikeCount(entityType, entityId);
    // 点赞状态获取
    int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
    // 统一封装到map传给前端页面
    map.put("likeCount", likeCount);
    map.put("likeStatus", likeStatus);

    // 触发点赞事件后-系统向目标用户发送通知
    if(likeStatus == 1) {
        Event event = new Event()
                .setTopic(TOPIC_LIKE)
                .setFromUserId(hostHolder.getUser().getId())
                .setEntityType(entityType)
                .setEntityId(entityId)
                .setEntityUserId(entityUserId)
                .setData("postId", postId);
        eventProducer.sendEvent(event);
    }
    return CommunityUtil.getJSONString(0, null, map);
}

3.关注

  • 目前关注的都是人,因此实体类型都是User
/**
 * 处理关注的异步请求
 * @param entityType    关注对象类型
 * @param entityId      关注对象id
 * @return
 */
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
    User user = hostHolder.getUser();
    if(user == null) {  // 拦截器已经拦截,若拦截不成功,再次抛出异常
        throw new IllegalArgumentException("用户没有登录!");
    }
    followService.follow(user.getId(), entityType, entityId);
    // 触发关注事件-系统向关注对象发送通知
    Event event = new Event()
            .setTopic(TOPIC_FOLLOW)
            .setFromUserId(hostHolder.getUser().getId())
            .setEntityType(entityType)
            .setEntityId(entityId)
            .setEntityUserId(entityId); // 目前关注的都是人,因此实体类型都是User
    eventProducer.sendEvent(event);
    return CommunityUtil.getJSONString(0, "关注成功!");
}

测试

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值