项目理解(六)消息队列Kafka实现及性能优化

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、发布订阅模式:生产者将消息放到了某一个位置,可以同时有很多个消费者来订阅(读取)这个消息;生产者将消息发布到的某个位置就叫Topicpartition对位置的分区;对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);
        }
    }
}

 

 

 

 

你可能感兴趣的:(后端开发与性能优化)