消息队列与Kafka | 青训营笔记

176 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记,应该也是在青训营里的最后一篇笔记了。

简介

本次笔记分享消息队列相关的内容。

关键词:消息队列 BlockingQueue Redis Kafka

三个作用与两个模式

  • 缓冲:优化控制流向消费者的速度,解决生产和消费速度不一致的情况。
  • 解耦:可以独立修改生产者或消费者
  • 异步通信:生产者生产出的消息,可以不被立即处理,而是在合适的时候由消费者去处理

  • 点对点模式
    • 消费者主动拉取数据,消息收到后清除消息
  • 发布订阅模式
    • 可以有对多个主题
    • 消费者消费数据后,不删除数据
    • 每个消费者相互独立,可以消费到相同的数据

BlockingQueue实现消息队列

BlockingQueue可以当作消息队列,生产者线程产生数据后向BlockingQueueput(),消费者线程需要数据就向BlockingQueuetake()

生产者

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++) {//生产100个元素
                Thread.sleep(10);
                queue.put(1);//将元素加入队列中
                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) {
                Thread.sleep(new Random().nextInt(100));
                queue.take();//从队列中取出元素
                System.out.println(Thread.currentThread().getName() + "消费元素。现在队列中还有" + queue.size() + "个元素。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

主方法

生产者和所有消费者共用一个BlockingQueue

    public static void main(String[] args) {
        BlockingQueue queue = new ArrayBlockingQueue(5);//生产者与消费者共用这个阻塞队列,队列中最多只能存5个元素
        new Thread(new Producer(queue)).start();//生产者线程
        new Thread(new Consumer(queue)).start();//消费者线程
        new Thread(new Consumer(queue)).start();//消费者线程
        new Thread(new Consumer(queue)).start();//消费者线程
    }

运行结果

Thread-0生产元素。现在队列中还有1个元素。
Thread-0生产元素。现在队列中还有2个元素。
Thread-1消费元素。现在队列中还有1个元素。
Thread-0生产元素。现在队列中还有2个元素。
Thread-0生产元素。现在队列中还有3个元素。
Thread-1消费元素。现在队列中还有2个元素。
Thread-0生产元素。现在队列中还有3个元素。
Thread-0生产元素。现在队列中还有4个元素。
Thread-0生产元素。现在队列中还有5个元素。
Thread-3消费元素。现在队列中还有4个元素。
...
...
...
Thread-2消费元素。现在队列中还有4个元素。
Thread-3消费元素。现在队列中还有3个元素。
Thread-1消费元素。现在队列中还有2个元素。
Thread-1消费元素。现在队列中还有1个元素。
Thread-2消费元素。现在队列中还有0个元素。

Redis实现消息队列

使用redis的list数据结构作为消息队列。

业务:作品被点赞后,将点赞信息(谁什么时候点赞了哪个作品)发送给作者。

controller层

  1. 获取点赞信息
  2. 点赞信息写入数据库
  3. 新建点赞事件,把事件交给生产者处理。此处逻辑上属于生产者。
eventProducer.fireEvent(
    new EventModel(EventType.LIKE)//事件类型
        .setActorId(hostHolder.getUser().getId())//事件触发者
        .setEntityId(newsId)//实体id
        .setEntityType(EntityType.ENTITY_NEWS)//实体类型
        .setEntityOwnerId(news.userId)//实体的拥有者
        );
  1. 响应前端

生产者(service层)

将事件放入redis中,事件在redis中以json字符串的形式存在。

public boolean fireEvent(EventModel eventModel) {
    try {
        String message = JSONObject.toJSONString(eventModel);
        String key = RedisKeyUtil.getEventQueueKey();
        jedisAdapter.lpush(key, message);//生产者将信息加入消息队列中
        return true;
    } catch (Exception e) {
        return false;
    }
}

Redis Lpush 命令_将一个或多个值插入到列表头部

消费者(service层)

  1. 注册各个事件的handler列表到config
  2. 启动一个线程来消费事件
    1. brpop从redis中拿事件,拿不到就阻塞
    2. 根据根据事件的类型eventModel.getType()选择对应的事件处理器,可能一个事件有许多事件处理器,事件处理器在线程启动前就已经注册到了config
    3. 通过事件处理器处理事件
//启动一个线程来消费事件
Thread thread = new Thread(() -> {
    while (true) {
        String key = RedisKeyUtil.getEventQueueKey();
        List<String> messages = jedisAdapter.brpop(0, key);//从redis中拿事件,拿不到就阻塞

        EventModel eventModel = JSON.parseObject(messages.get(1), EventModel.class);

        // 找到这个事件的处理handler列表
        if (!config.containsKey(eventModel.getType())) {
            logger.error("不能识别的事件");
            continue;
        }

        for (EventHandler handler : config.get(eventModel.getType())) {
            handler.doHandle(eventModel);//通过事件处理器处理事件
        }

    }
});

Redis Brpop 命令移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。 反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的 key ,第二个元素是被弹出元素的值。

Redis Brpop 命令

事件处理器

public void doHandle(EventModel model)

根据事件里的信息,发送一条消息给目标用户。此处逻辑上属于消费者。

Kafka

入门

小朋友也可以懂的Kafka入门教程,还不快来学

  • Apache Kafka 是一个开源分布式事件流平台
  • 应用:消息系统、日志收集、用户行为追踪、流式处理
  • 特点:高吞吐量、消息持久化、高可靠性、高扩展性

走进消息队列 | 青训营笔记

使用kafka

  • 基于springboot使用kafka。
  • 要实现的功能
    • 帖子收到评论后向帖子的作者发送一条通知
    • 帖子被点赞后向帖子的作者发送一条通知
    • 被关注后向被关注者发送一条通知
  • 代码展示以评论为例。

Event类

private String topic;//事件的主体
private int userId;//事件触发者的id
private int entityType;//实体的类型
private int entityId;//实体的id
private int entityUserId;//实体的所有者的id
private Map<String, Object> data = new HashMap<>();//其他信息

controller层

  1. 接收评论参数
  2. 评论写入数据库
  3. 新建一个评论事件,将事件交给生产者。此处逻辑上属于生产者。
eventProducer.fireEvent(event);

event中包含了事件的主题、事件的触发者等信息。

  1. 响应前端

生产者

处理事件,事件按照主题放进消息队列中,事件在消息队列中以json字符串的形式存在

@Component
public class EventProducer {

    @Autowired
    private KafkaTemplate kafkaTemplate;
    
    public void fireEvent(Event event) {
        //将事件发布到指定的主题
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }
}

消费者

利用@KafkaListener注解,让handleSendMessage方法监听kafka中TOPIC_LIKETOPIC_FOLLOWTOPIC_COMMENT这三个主题下的消息,如果有消息就会拿过来处理。

@KafkaListener(topics = {TOPIC_LIKE, TOPIC_FOLLOW, TOPIC_COMMENT})
public void handleSendMessage(ConsumerRecord record)
Event event = JSONObject.parseObject(record.value().toString(), Event.class);//通过record参数拿到事件

根据事件中的各种信息,调用messageService发送消息。