这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记,应该也是在青训营里的最后一篇笔记了。
简介
本次笔记分享消息队列相关的内容。
关键词:消息队列 BlockingQueue Redis Kafka
三个作用与两个模式
- 缓冲:优化控制流向消费者的速度,解决生产和消费速度不一致的情况。
- 解耦:可以独立修改生产者或消费者
- 异步通信:生产者生产出的消息,可以不被立即处理,而是在合适的时候由消费者去处理
- 点对点模式
- 消费者主动拉取数据,消息收到后清除消息
- 发布订阅模式
- 可以有对多个主题
- 消费者消费数据后,不删除数据
- 每个消费者相互独立,可以消费到相同的数据
BlockingQueue实现消息队列
BlockingQueue可以当作消息队列,生产者线程产生数据后向BlockingQueue里put(),消费者线程需要数据就向BlockingQueue里take()
生产者
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层
- 获取点赞信息
- 点赞信息写入数据库
- 新建点赞事件,把事件交给生产者处理。此处逻辑上属于生产者。
eventProducer.fireEvent(
new EventModel(EventType.LIKE)//事件类型
.setActorId(hostHolder.getUser().getId())//事件触发者
.setEntityId(newsId)//实体id
.setEntityType(EntityType.ENTITY_NEWS)//实体类型
.setEntityOwnerId(news.userId)//实体的拥有者
);
- 响应前端
生产者(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;
}
}
消费者(service层)
- 注册各个事件的handler列表到
config中 - 启动一个线程来消费事件
- 用
brpop从redis中拿事件,拿不到就阻塞 - 根据根据事件的类型
eventModel.getType()选择对应的事件处理器,可能一个事件有许多事件处理器,事件处理器在线程启动前就已经注册到了config中 - 通过事件处理器处理事件
- 用
//启动一个线程来消费事件
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 ,第二个元素是被弹出元素的值。
事件处理器
public void doHandle(EventModel model)
根据事件里的信息,发送一条消息给目标用户。此处逻辑上属于消费者。
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层
- 接收评论参数
- 评论写入数据库
- 新建一个评论事件,将事件交给生产者。此处逻辑上属于生产者。
eventProducer.fireEvent(event);
event中包含了事件的主题、事件的触发者等信息。
- 响应前端
生产者
处理事件,事件按照主题放进消息队列中,事件在消息队列中以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_LIKE、TOPIC_FOLLOW、TOPIC_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发送消息。