Kafka特性:(1)高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。(2) 可扩展性:kafka集群支持热扩展。(3)持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失。(4) 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)。(5) 高并发:支持数千个客户端同时读写。
使用场景:(1)日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。(2)消息系统:解耦和生产者和消费者、缓存消息等。(3)用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。(4) 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。(5) 流式处理:比如spark streaming和storm
Kafka对硬盘的连续读取性能是很高的,可能高于对内存的随机读取。Kafka将海量的数库存储到硬盘中,是持久化的。Broke是指kefka的服务器;Zookeeper是一个独立的应用,可以用来管理一个集群;
消息队列大致有两种实现方式:1、点对点式的,生产者往消息队列写数据,消费者从消息队列一个一个地拿数据;2、发布订阅模式:生产者将消息放到了某一个位置,可以同时有很多个消费者来订阅(读取)这个消息;生产者将消息发布到的某个位置就叫Topic;partition对位置的分区;对Topic分为了多个区域,便于多个线程同时处理,提高性能;offset为消息存放在某区域的一个索引;
Leader Replica:主副本,可以处理请求作响应;Follower Replica:从副本,只是做备份;当主副本挂掉的时候,可以从从副本中选出一个来作为新的主副本;
遇到的问题:kafka应该使用默认的版本,不然又杂又乱,版本依赖不同;
之前面向切面编程,将所有的service目录下的文件(由于只有Controller层调用了service)作为切入点访问,其中实现的是统一的日志记录:需要获取到了得到HttpServletRequest;而加上kafka消息队列在EventConsumer类中也调用了MessageService,而EventConsumer是不会出现HttpServletRequest的,所以会报空指针异常。
改进:
@Before("pointcut()") public void before(JoinPoint joinPoint) { // 用户[1.2.3.4],在[xxx],访问了[com.liu.community.service.xxx()]. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(attributes == null){ //====如果不是常规的页面Collector对service的调用,则attributes为空;如kafka中EventConsumer调用了service return; } HttpServletRequest request = attributes.getRequest();//得到HttpServletRequest String ip = request.getLocalAddr();//ip String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//时间 //得到切入处的:getDeclaringTypeName类名,getName方法名; String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target)); }
再经过了一次封装:将消息队列信息封装为事件;
封装事件:
public class Event {
//事件对信息进行了包装
//封装:点赞、评论、关注三类情形;
//topic指哪种情形;userId操作的用户id,entityId对什么实体操作的id,
// entityType对什么实体操作的类型,entityUserId为该被操作实体的用户id
private String topic;
private int userId;
private int entityType;
private int entityId;
private int entityUserId;
Map data = new HashMap<>();//支持可扩展
public String getTopic() {
return topic;
}
public Event setTopic(String topic) {
this.topic = topic;
return this;//即灵活,又方便;在使用的时候可以:setTopic().setUserId().......这样来使用
}
public int getUserId() {
return userId;
}
public Event setUserId(int userId) {
this.userId = userId;
return this;//即灵活,又方便;在使用的时候可以:setTopic().setUserId().......这样来使用
}
public int getEntityType() {
return entityType;
}
public Event setEntityType(int entityType) {
this.entityType = entityType;
return this;//即灵活,又方便;在使用的时候可以:setTopic().setUserId().......这样来使用
}
public int getEntityId() {
return entityId;
}
public Event setEntityId(int entityId) {
this.entityId = entityId;
return this;//即灵活,又方便;在使用的时候可以:setTopic().setUserId().......这样来使用
}
public int getEntityUserId() {
return entityUserId;
}
public Event setEntityUserId(int entityUserId) {
this.entityUserId = entityUserId;
return this;//即灵活,又方便;在使用的时候可以:setTopic().setUserId().......这样来使用
}
public Map getData() {
return data;
}
public Event setData(String key, Object value) {
this.data.put(key, value);
return this;//即灵活,又方便;在使用的时候可以:setTopic().setUserId().......这样来使用
}
}
生产事件:
@Component
public class EventProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
//处理事件
public void fireEvent(Event event) {
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
//将Event对象转化为(串行化为)json格式的string数据,再以其topic发送出去
}
}
消费事件:通知消息:关注、评论、点赞;es更新:发帖(发帖子、对帖子置顶、加精、加评论,会将帖子加到TOPIC_PUBLISH中),删帖;分享数据:生成长图上传云服务器;
@Component
public class EventConsumer implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
@Autowired
private MessageService messageService;
@Autowired
private DiscussPostService discussPostService;
@Autowired
private ElasticsearchService elasticsearchService;
//spring可以执行定时任务的线程池
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@Value("${wk.image.command}")
private String wkImageCommand;
@Value("${wk.image.storage}")
private String wkImageStorage;
//&&&&&&&&&&&&&&&&&上传到云服务器重构&&&&&&&&&&&&&&&&&&&&
@Value("${qiniu.key.access}")
private String accessKey;
@Value("${qiniu.key.secret}")
private String secretKey;
@Value("${qiniu.bucket.share.name}")
private String shareBucketName;
//系统通知功能:提示:关注; 评论;点赞;
//可以一个方法消费一个主题,也可以一个方法消费多个主题;
//一个主题也可以被多个方法消费
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_FOLLOW, TOPIC_LIKE})
public void handleCommentMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息为空");
return;
}
//将json格式的数据转化为string再转化为Event对象格式
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误");
return;
}
//此处发送通知:本系统是系统id为1(message表中的发送者id:fromId为1),conversationId
// 也不用存拼起来的id了,也以用来存储系统通知的主题如:关注、点赞、评论
//发送站内通知:复用了message表
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());//存主题
message.setCreateTime(new Date());
Map content = new HashMap<>();
content.put("userId", event.getUserId());
content.put("entityType", event.getEntityType());
content.put("entityId", event.getEntityId());
if (!event.getData().isEmpty()) {
for (Map.Entry entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
//具体内容由以上信息拼出:在页面显示的是 : 用户**评论了你的帖子 ,点击查看;
messageService.addMessage(message);
}
//=======================消费传来的触发消息:根据消息向elasticsearch服务器增加数据==============================
// 消费发帖事件
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
//得到的消息没有问题,就开始处理事件
//先查找到帖子,再将帖子保存到elasticsearch服务器
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@消费删帖事件
@KafkaListener(topics = {TOPIC_DELETE})
public void handleDeleteMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
//得到的消息没有问题,就开始处理事件
//先查找到帖子,再将帖子保存到elasticsearch服务器
elasticsearchService.deleteDiscussPost(event.getEntityId());
}
//%%%%%%%%%%%-----消费分享事件-------%%%%%%%%%%%%%%%%%
@KafkaListener(topics = {TOPIC_SHARE})
public void handleShareMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
String htmlUrl = (String) event.getData().get("htmlUrl");
String fileName = (String) event.getData().get("fileName");
String suffix = (String) event.getData().get("suffix");
String cmd = wkImageCommand + " --quality 75 " + htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;
try {
Runtime.getRuntime().exec(cmd);//生成图片较慢
logger.info("生成长图成功");//这句先执行完
} catch (IOException e) {
logger.error("生成长图失败" + e.getMessage());
}
//启用定时器,监视图片并上传至云服务器;
//需要完成任务就关闭;还需要考虑传不成功的情况:1生成图片失败 2传图失败:网络或云服务器出现问题
UploadTask uploadTask = new UploadTask(fileName, suffix);
Future future = taskScheduler.scheduleAtFixedRate(uploadTask, 500);//每隔500ms执行一次,返回值future可以用来停止任务
uploadTask.setFuture(future);
}
//&&&&&&&&&&&&&&&&&上传到云服务器重构&&&&&&&&&&&&&&&&&&&&
//由于生成图片较慢,上传到云服务器上需要已经生成好的长图,阻塞等待也不好,
// 在这里使用定时器每个一段时间判断是否生成完成;
//在多个服务器环境中,虽然都部署了这个类,消费者有一个抢占的机制,一旦有个消息过来,只能有一台服务器占到;
//所以这个方法只有某个服务器在执行,可以用普通定时器;不必用Spring quartz定时器;
// 而之前的程序是在程序启动时就运行,在每一个服务器就要开始运行(由完成的功能需求决定)
class UploadTask implements Runnable {
//文件名
private String fileName;
//文件后缀
private String suffix;
//启动任务的返回值
private Future future;
//开始时间
private long startTime;
//上传次数
private int uploadTimes;
public UploadTask(String fileName, String suffix) {
this.fileName = fileName;
this.suffix = suffix;
this.startTime = System.currentTimeMillis();
}
public void setFuture(Future future) {
this.future = future;
}
@Override
public void run() {
//生成失败
if ((System.currentTimeMillis() - startTime) > 30000) {//30s内
logger.error("执行时间过长,终止任务:" + fileName);
future.cancel(true);//停止任务
return;
}
//上传失败
if (uploadTimes > 3) {
logger.error("上传次数过多,终止任务:" + fileName);
future.cancel(false);
return;
}
String filePath = wkImageStorage + "/" + fileName + suffix;
File file = new File(filePath);
if (file.exists()) {
logger.info(String.format("开始第%d次上传[%s]", ++uploadTimes, fileName));
//设置响应信息,七牛云的格式;
//通常都是异步的响应信息,成功的时候给页面传递json字符串
StringMap policy = new StringMap();
policy.put("returnBody", CommunityUtil.getJSONString(0));
//生成上传凭证
Auth auth = Auth.create(accessKey, secretKey);
String uploadToken = auth.uploadToken(shareBucketName, fileName, 3600, policy);
//上传至指定机房
UploadManager manager = new UploadManager(new Configuration(Zone.zone2()));//华南区Zone.zone2()
try {//开始上传图片
Response response = manager.put(filePath,fileName ,
uploadToken, null, "image/", false);
//处理响应结果
JSONObject json = JSONObject.parseObject(response.bodyString());
if (json == null || json.get("code") == null || !json.get("code").toString().equals("0")){
logger.info(String.format("第%d次上传失败[%s]", uploadTimes, fileName));
}else {
logger.info(String.format("第%d次上传成功[%s].", uploadTimes, fileName));
future.cancel(true);
}
} catch (QiniuException e) {
logger.info(String.format("第%d次上传失败[%s]", uploadTimes, fileName));
// e.printStackTrace();
}
} else {
logger.info("等待图片生成[" + fileName + "]");
}
}
}
}
//==========Elasticsearch新增发布帖子事件:发布帖子时,将帖子异步的提交到Elasticsearch服务器===========
Event event = new Event().setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
eventProducer.fireEvent(event);//触发事件//还有在评论层加上:增加评论后更新情况
补充:公共常量接口类:
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS = 0;
/**
* 重复激活
*/
int ACTIVATION_REPEAT = 1;
/**
* 激活失败
*/
int ACTIVATION_FAILURE = 2;
/**
* 默认状态的登录凭证的超时时间
*/
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
/**
* 记住状态的登录凭证超时时间
*/
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
/**
* 实体类型: 帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 实体类型: 评论
*/
int ENTITY_TYPE_COMMENT = 2;
/**
* 实体类型: 用户
*/
int ENTITY_TYPE_USER = 3;
/**
* 主题:评论
* */
String TOPIC_COMMENT = "comment";
/**
* 主题:点赞
* */
String TOPIC_LIKE = "like";
/**
* 主题:关注
* */
String TOPIC_FOLLOW = "follow";
/**
* 主题: 发帖
*/
String TOPIC_PUBLISH = "publish";
/**
* 主题: 删帖
*/
String TOPIC_DELETE = "delete";
/**
* 系统用户id
* */
int SYSTEM_USER_ID = 1;
/**
* 权限: 普通用户
*/
String AUTHORITY_USER = "user";
/**
* 权限: 管理员
*/
String AUTHORITY_ADMIN = "admin";
/**
* 权限: 版主
*/
String AUTHORITY_MODERATOR = "moderator";
/**
* 分享
* */
String TOPIC_SHARE = "share";
}
以上就是更新数据信息,下面页面显示:
显示系统通知列表:在此页面上显示的是三类消息每一类消息最近的消息;点击可以进去分页显示每一类主题所包含的通知列表;在消息页面头部显示所有未读消息数量。
显示系统通知详情:mapper层:通过userId和topic查询:某个主题(点赞、评论、关注)下最新的一条通知、某个主题所包含的通知的数量,查询未读的通知数量;Service层对mapper层的封装。在messageController层:添加系统通知页面,将数据传到前端页面模板。
首页上有个消息(所有私信、通知未读的)总数量,由于每个页面都要显示,每次请求都会有,所以使用拦截器来实现,新加一个拦截器
mapper层:
@Mapper
public interface MessageMapper {
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.
List selectConversations(int userId, int offset, int limit);
// 查询当前用户的会话数量.
int selectConversationCount(int userId);
// 查询某个会话所包含的私信列表.
List selectLetters(String conversationId, int offset, int limit);
// 查询某个会话所包含的私信数量.
int selectLetterCount(String conversationId);
// 查询未读私信的数量
int selectLetterUnreadCount(int userId, String conversationId);
//新增消息
int insertMessage(Message message);
//修改消息的状态
int updateStatus(List ids, int status);
//一个用户,多个会话;一个会话是两个人之间的会话,一个会话,多条消息;
// 查询本用户的会话列表:包含对方的user信息,与用户会话的最后一条消息;
//查询某个主题(点赞、评论、关注)下最新的一条通知
Message selectLatestNotice(int userId, String topic);
//查询某个主题所包含的通知的数量
int selectNoticeCount(int userId, String topic);
//查询未读的通知数量
int selectNoticeUnreadCount(int userId, String topic);
//当topic为null是查询的是所有的主题(comment、like、follow)的未读的消息总数
//查询某个主题所包含的通知列表
List selectNotices(int userId, String topic, int offset, int limit);
}
Service层:
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
@Autowired
private SensitiveFilter sensitiveFilter;
public List findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversations(userId, offset, limit);
}
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
public List findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetters(conversationId, offset, limit);
}
public int findLetterCount(String conversationId) {
return messageMapper.selectLetterCount(conversationId);
}
public int findLetterUnreadCount(int userId, String conversationId) {
return messageMapper.selectLetterUnreadCount(userId, conversationId);
}
public int addMessage(Message message){
message.setContent(sensitiveFilter.filter(HtmlUtils.htmlEscape(message.getContent())));
return messageMapper.insertMessage(message);
}
public int readMessage(List ids){//对id集合:更新读的状态
return messageMapper.updateStatus(ids, 1);//0为未读,1为已读,2为无权限
}
//=+++++++++++++++++++++++系统通知的业务逻辑代码+++++++++++++++++++++++++++
// (点赞、评论与关注的系统通知是通过kafka实现,但数据读写等操作仍然是在MySQL数据库中):====
public 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);
}
public List findNotices(int userId, String topic, int offset, int limit) {
return messageMapper.selectNotices(userId, topic, offset, limit);
}
}
messageController层:正常调用service中数据就行。
MessageInterceptor 拦截器的实现:
@Component
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);
}
}
}