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:只负责备份数据
- 安装
下载地址
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
- 引入依赖
- spring-kafka
org.springframework.kafka
spring-kafka
- 配置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");