kafka:性能最好的消息队列服务器,能处理TB级别的服务器
这一章主要解决发送系统级消息/通知的问题
阻塞队列主要解决线程通信的问题

阻塞队列

1. 概念

BlockingQueue 是一个接口

  • 解决线程通信的问题。
  • 阻塞方法:put、take。
  • 男足生产者消费者模式
  • 生产者:产生数据的线程。
  • 消费者:使用数据的线程。
    BlockingQueue在生产者和消费者之间充当缓冲,可以平衡生产和消费的速度,避免cpu资源被浪费,如果丢满了就阻塞
  • 实现类
  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue、SynchronousQueue、DelayQueue等。

2. 代码模拟

public class BlockingQueueTests {

    public static void main(String[] args) {
        // 1. 实例化阻塞队列,队列中最多只能存储10个数
        BlockingQueue queue = new ArrayBlockingQueue(10);
        // 2. 创建生产者线程和消费者线程,1个生产者,3个消费者并发消费数据
        new Thread(new Producer(queue)).start();
        new Thread(new Consumer(queue)).start();
        new Thread(new Consumer(queue)).start();
        new Thread(new Consumer(queue)).start();
    }

}

// 一个文件只能有一个public的类
// 生产者线程
class Producer implements Runnable {

    private BlockingQueue<Integer> queue;

    public Producer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(20);
                queue.put(i);
                System.out.println(Thread.currentThread().getName() + "生产:" + queue.size());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 消费者线程
class Consumer implements Runnable {

    private BlockingQueue<Integer> queue;

    public Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                // 在0-1000间随机输出一个数,比20大的几率更大,即消费者的消费能力没有生产者快
                Thread.sleep(new Random().nextInt(1000));
                queue.take();
                System.out.println(Thread.currentThread().getName() +"消费:" + queue.size());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Kafka入门

1. 概念

  • Kafka简介
  • Kafka是一个分布式的流媒体平台。
  • 应用:消息系统、日志收集、用户行为追踪、流式处理。
  • Kafka特点
  • 高吞吐量
  • 消息持久化:kafka会把消息存在硬盘上,能永久保存
  • 高可靠性
  • 高扩展性:容易配备集群
  • Kafka术语
  • Broker:kafka的服务器
  • Zookeeper:是一个独立的应用,能用于管理其它的集群
  • Topic:消息队列实现方式有两种:点对点、发布订阅模式,这个消息可以被多个消费者先后读到,生产者把消息发布的地方就是topic
  • Partition:分区,一个主题分为多个分区,一个分区从前往后添加数据
  • Offset:消息在分区内存放的索引(序列)
  • Replica:副本,对数据作备份 ,每个分区拥有多个副本,
  • Leader Replica:负责作响应
  • Follower Replica:只负责备份数据
  1. 安装
    下载地址
    https://kafka.apache.org/ 配置
  • zookeeper.properties
  • server.properties-log.dirs
    注意,配置的data文件要和安装的kafka处于文件的同一级下

3. 使用

# 启动zookeeper
bin\windows\zookeeper-server-start.bat config\zookeeper.properties

# 启动kafka
bin\windows\kafka-server-start.bat config\server.properties

Spring整合Kafka

  1. 引入依赖
  • spring-kafka

    org.springframework.kafka
    spring-kafka
  1. 配置Kafka
  • 配置server、consumer

KafkaProperties

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=community-consumer-group
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-commit-interval=3000

3. 访问Kafka

  • 生产者
  • kafkaTemplate.send(topic, data);
  • topic:发布的主题
  • data:发布的内容
  • 消费者
  • @KafkaListener(topics = {“test”}) -
  • public void handleMessage(ConsumerRecord record) {}
    写一个或多个主题,下面的方法监听配置的主题,一旦发现主题有消息,就会调用方法包装消息成为ConsumerRecord record
@RunWith(SpringRunner.class)
@SpringBootTest
// 在测试代码中启用CommunityApplication作为测试类
@ContextConfiguration(classes = CommunityApplication.class)
public class KafkaTests {

   @Autowired
   private KafkaProducer kafkaProducer;

   /**
    * 用生产者发消息,看消费者能不能收到
    * 通常把生产者和消费者进行各自封装
    */
   @Test
   public void testKafka() {
       kafkaProducer.sendMessage("test", "你好");
       kafkaProducer.sendMessage("test", "在吗");

       try {
           // 睡10秒钟
           Thread.sleep(1000*10);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

@Component
class KafkaProducer {

   @Autowired
   private KafkaTemplate kafkaTemplate;

   public void sendMessage(String topic, String content) {
       kafkaTemplate.send(topic, content);
   }
}


@Component
class KafkaConsumer {

   // 消费者不需要引入KafkaTemplate,它是被动地处理消息
  //  如果没有消息就阻塞,如果有就读
  @KafkaListener(topics = {"test"})
   public void handleMessage(ConsumerRecord record) {
      System.out.println(record.value());
  }

}

发送系统通知

1. 需求

- 触发事件

  • 评论后,发布通知
  • 点赞后,发布通知
  • 关注后,发布通知
    生产者和消费者处理事务是并发的(异步进行),消费者往message里面存信息
    从事件的角度,这种方式为事件驱动的方式
  • 处理事件
  • 封装事件对象(而不是以消息进行封装)
  • 开发事件的生产者
  • 开发事件的消费者

2. 代码实现

定义事件event

public class Event {

    private String topic;
    // 事件触发的人
    private int userId;
    // 事件发生在哪个实体上
    private int entityType;
    private int entityId;
    // 实体的作者是谁
    private int entityUsrId;
    // 把其它额外的数据存到map里,使其具有扩展性
    private Map<String, Object> data = new HashMap<>();
    
}

重写set方法

开发事件的生产者和消费者

  • 生产者
Component
public class EventProducer {

    @Autowired
    private KafkaTemplate kafkaTemplate;

    // 处理事件
    public void fireEvent(Event event) {
        // 将事件发布到指定主题
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }
}
  • 消费者
@Component
public class EventConsumer implements CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);

    @Autowired
    private MessageService messageService;

    @KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
    public void handleCommentMessage(ConsumerRecord record) {
        if(record == null || record.value() == null) {
            logger.error("消息的内容为空!");
        }

        Event event = JSONObject.parseObject(record.value().toString(), Event.class);
        if (event == null) {
            logger.error("消息格式错误!");
            return;
        }

        // 如果内容没问题,格式也没问题 ->发送站内通知
        // 私信是两个用户发,发通知时系统后天发送给用户,user是不存在的,造一个虚拟的用户
        Message message = new Message();
        message.setFromId(SYSTEM_USER_ID);
        message.setToId(event.getEntityUserId());
        message.setConversationId(event.getTopic());
        message.setCreateTime(new Date());
        
        Map<String, Object> content = new HashMap<>();
        // 事件是谁触发的
        content.put("userId", event.getUserId());
        // 实体的类型和作者
        content.put("entityType", event.getEntityType());
        content.put("entityId", event.getEntityId());
        
        // 存到content的字段里
        if (!event.getData().isEmpty()) {
            for(Map.Entry<String, Object> entry : event.getData().entrySet()) {
                content.put(entry.getKey(), entry.getValue());
            }
        }
        message.setContent(JSONObject.toJSONString(content));
        messageService.addMessage(message);
}
  • 复用message表
    conversation_id 存主题
    content里包含对象,拼出“用户xx xx你的xx,点击查看”
Map<String, Object> content = new HashMap<>();
        // 事件是谁触发的
        content.put("userId", event.getUserId());
        // 实体的类型和作者
        content.put("entityType", event.getEntityType());
        content.put("entityId", event.getEntityId());
        
        // 存到content的字段里
        if (!event.getData().isEmpty()) {
            for(Map.Entry<String, Object> entry : event.getData().entrySet()) {
                content.put(entry.getKey(), entry.getValue());
            }
        }
        message.setContent(JSONObject.toJSONString(content));
        messageService.addMessage(message);

controller层

  • Commentcontroller
    添加评论以后通知
    线程里可以攒很多消息慢慢处理
// 触发评论事件
Event event = new Event()
        .setTopic(TOPIC_COMMENT)
        .setUserId(hostHolder.getUser().getId())
        .setEntityType(comment.getEntityType())
        .setEntityId(comment.getEntityId())
        .setData("postId", discussPostId);

// 评论的可能是帖子,也可能是评论,如果是帖子查帖子表,如果是评论查评论表
if (comment.getEntityType() == ENTITY_TYPE_POST) {
    DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
    event.setEntityUsrId(target.getUserId());
} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
    Comment target = commentService.findCommentById(comment.getEntityId());
    event.setEntityUsrId(target.getUserId());
}
eventProducer.fireEvent(event);
  • Likecontroller
    // 触发点赞事件
// 点一下是赞,再点一下取消赞,取消赞没必要通知
if(likeStatus == 1) {
    // 当前是点赞
    Event event = new Event()
            .setTopic(TOPIC_LIKE)
            .setUserId(hostHolder.getUser().getId())
            .setEntityType(entityType)
            .setEntityId(entityId)
            .setEntityUserId(entityUserId)
            .setData("postId", postId);
    // 将事件发布到主题
    eventProducer.fireEvent(event);
}
  • followcontroller
// 触发关注事件
// 关注:xxx关注了你,链接应该链接到主页上
Event event = new Event()
        .setTopic(TOPIC_FOLLOW)
        .setUserId(hostHolder.getUser().getId())
        .setEntityType(entityType)
        .setEntityId(entityId)
        .setEntityUserId(entityId);
eventProducer.fireEvent(event);

修改discuss-detail.html 和discuss.js

如果kafka崩溃,删除kafka-logs

显示系统通知

1. 需求

  • 通知列表
  • 显示评论、点赞、关注三种类型的通知
  • 通知详情
  • 分页显示某一类主题所包含的通知
  • 未读消息
  • 在页面头部显示所有的未读消息数量

2. 代码实现

通知列表

  • 数据访问层
// 查询某个主题下最新的通知
Message selectLatestNotice(int userId, String topic);

// 查询某个主题所包含的通知的数量
int selectNoticeCount(int userId, String topic);

// 查询未读的通知数量
int selectNoticeUnreadCount(int userId, String topic);
<!--Message selectLatestNotice(int userId, String topic);-->
<select id="selectLatestNotice" resultType="Message">
    select <include refid="selectFields"></include>
    from message
    where id in (
    select max(id) from message
    where status != 2
    and from_id = 1
    and to_id = #{userId}
    and conversation_id = #{topic}
    )
</select>

<!--int selectNoticeCount(int userId, String topic);-->
<select id="selectNoticeCount" resultType="int">
    select count(id) from message
    where status != 2
    and from_id = 1
    and to_id = #{userId}
      and conversation_id = #{topic}
</select>

<!--int selectNoticeUnreadCount(int userId, String topic);-->
<select id="selectNoticeUnreadCount" resultType="int">
    select count(id) from message
    where status = 0
    and from_id = 1
    and to_id = #{userId}
    <if test="topic!=null">
        and conversation_id = #{topic}
    </if>
</select>
  • 业务层messageService层里写
ublic Message findLatestNotice(int userId, String topic) {
    return messageMapper.selectLatestNotice(userId, topic);
}

public int findNoticeCount(int userId, String topic) {
    return messageMapper.selectNoticeCount(userId, topic);
}

public int findNoticeUnreadCount(int userId, String topic) {
    return messageMapper.selectNoticeUnreadCount(userId, topic);
}
  • controller层
// 显示通知列表
@RequestMapping(path = "/notice/list", method = RequestMethod.GET)
public String getNoticeList(Model model) {
    User user = hostHolder.getUser();

    // 查询评论类通知
    Message message = messageService.findLatestNotice(user.getId(), TOPIC_COMMENT);
    Map<String, Object> messageVO = new HashMap<>();
    if(message != null) {
        messageVO.put("message", message);

        // 对content里的转义字符进行反转义
        String content = HtmlUtils.htmlUnescape(message.getContent());
        // 转换为正常的对象
        Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);

        messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
        messageVO.put("entityType", data.get("entityType"));
        messageVO.put("entityId", data.get("entityId"));
        messageVO.put("postId", data.get("postId"));

        // 查评论总数量和未读数量,放入vo里
        int count = messageService.findNoticeCount(user.getId(), TOPIC_COMMENT);
        messageVO.put("count", count);

        int unread = messageService.findLetterUnreadCount(user.getId(), TOPIC_COMMENT);
        messageVO.put("unread", unread);
    }
    model.addAttribute("commentNotice", messageVO);

    // 查询点赞类通知
    message = messageService.findLatestNotice(user.getId(), TOPIC_LIKE);
    messageVO = new HashMap<>();
    if (message != null) {
        messageVO.put("message", message);

        String content = HtmlUtils.htmlUnescape(message.getContent());
        Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);

        messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
        messageVO.put("entityType", data.get("entityType"));
        messageVO.put("entityId", data.get("entityId"));
        messageVO.put("postId", data.get("postId"));

        int count = messageService.findNoticeCount(user.getId(), TOPIC_LIKE);
        messageVO.put("count", count);

        int unread = messageService.findNoticeUnreadCount(user.getId(), TOPIC_LIKE);
        messageVO.put("unread", unread);
    }
    model.addAttribute("likeNotice", messageVO);

    // 查询关注类的通知
    message = messageService.findLatestNotice(user.getId(), TOPIC_FOLLOW);
    messageVO = new HashMap<>();
    if (message != null) {
        messageVO.put("message", message);

        String content = HtmlUtils.htmlUnescape(message.getContent());
        Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);

        messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
        messageVO.put("entityType", data.get("entityType"));
        messageVO.put("entityId", data.get("entityId"));

        int count = messageService.findNoticeCount(user.getId(), TOPIC_FOLLOW);
        messageVO.put("count", count);

        int unread = messageService.findNoticeUnreadCount(user.getId(), TOPIC_FOLLOW);
        messageVO.put("unread", unread);
    }
    model.addAttribute("followNotice", messageVO);

    // 查询未读消息数量(所有的未读数量,所以传入null)
    int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
    model.addAttribute("letterUnreadCount", letterUnreadCount);
    int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
    model.addAttribute("noticeUnreadCount", noticeUnreadCount);
    
    return "/site/notice";
}

通知详情

  • 数据访问层
// 查询某个主题所包含的通知列表
List<Message> selectNotices(int userId, String topic, int offset, int limit);
<!--List<Message> selectNotices(int userId, String topic, int offset, int limit);-->
<select id="selectNotices" resultType="Message">
    select <include refid="selectFields"></include>
    from message
    where status != 2
    and from_id = 1
    and to_id = #{userId}
    and conversation_id = #{topic}
    order by create_time desc
    limit #{offset}, #{limit}
</select>
  • 业务层
public List<Message> findNotices(int userId, String topic, int offset, int limit) {
    return messageMapper.selectNotices(userId, topic, offset, limit);
}
  • controller层
@RequestMapping(path = "/notice/detail/{topic}", method = RequestMethod.GET)
public String getNoticeDetail(@PathVariable("topic") String topic, Page page, Model model) {
    // 获取当前用户
    User user = hostHolder.getUser();

    // 对分页查询加以限制
    page.setLimit(5);
    page.setPath("/notice/detail/"+topic);
    page.setRows(messageService.findNoticeCount(user.getId(), topic));

    List<Message> noticeList = messageService.findNotices(user.getId(), topic, page.getOffset(), page.getLimit());
    List<Map<String, Object>> noticeVoList = new ArrayList<>();
    if(noticeVoList != null) {
        for(Message notice : noticeList) {
            Map<String, Object> map = new HashMap<>();
            // 通知
            map.put("notice", notice);
            // 内容
            String content = HtmlUtils.htmlUnescape(notice.getContent());
            Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);
            map.put("user", userService.findUserById((Integer) data.get("userId")));
            map.put("entityType", data.get("entityType"));
            map.put("entityId", data.get("entityId"));
            map.put("postId", data.get("postId"));
            // 通知作者
            map.put("fromUser", userService.findUserById(notice.getFromId()));

            noticeVoList.add(map);
        }
    }
    model.addAttribute("notices", noticeVoList);
    
    // 设置已读
    List<Integer> ids = getLetterIds(noticeList);
    if (!ids.isEmpty()) {
        messageService.readMessage(ids);
    }

    return "/site/notice-detail";
}

未读消息

  • 新建拦截器
public class MessageInterceptor implements HandlerInterceptor {
    
    @Autowired
    private HostHolder hostHolder;
    
    @Autowired
    private MessageService messageService;

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        User user = hostHolder.getUser();
        if(user != null && modelAndView != null) {
            int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
            int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
            modelAndView.addObject("allUnreadCount", letterUnreadCount +noticeUnreadCount);
        }
    }
}
  • 配置拦截器
registry.addInterceptor(messageInterceptor)
        //    排除所有目录下的css文件 双星号**表示在任意目录下
        .excludePathPatterns("/**/*.css\", \"/**/*.js\", \"/**/*.png\", \"/**/*.jpg\", \"/**/*.jpeg");