项目对kafka的使用

使用场景

最近项目中需求使用kafka作为消息队列去做手机短信业务的处理,因此进行一下对以前项目使用kafka的复习。良好的处理此次需求。

对kafka的操作流程

一、使用zookeeper对kafka进行管理

通常kafka需要和zookeeper配合使用,因此需要启动zookeeper服务,在kafka2.8以后也将zookeeper也集成在了服务中,不过使用应该是一样的。
①config目录下zookeeper.properties修改,导入zookeeper的路径。
在这里插入图片描述
②因为消息队列在运行过程中,会生成大量的日志,因此也需要一个log文件用于保存,在windows下如下操作:
在这里插入图片描述
这样就可以启动了
直接点击zkServer.cmd启动zookeeper,启动kafka使用如下命令
在这里插入图片描述

二、spring整合kafka

pom中引入依赖如下:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>2.5.0.RELEASE</version>
</dependency>

对kafka进行配置application.properties中如下:

#kafkaProperties
spring.kafka.bootstrap-servers=localhost:9092
#kafka安装目录下config下consumer.properties中找
spring.kafka.consumer.group-id=test-consumer-group
#是否自动提交消费者偏移量,消费者是按偏移量读取数据,记录偏移量
spring.kafka.consumer.enable-auto-commit=true
#自动提交的频率
spring.kafka.consumer.auto-commit-interval=3000
spring.kafka.listener.missing-topics-fatal=false

三、对整合后的kafka进行测试

测试类主要是对producer和consumer的编写,将器使用@component注解让其成为spring接管的“bean”,方便后续的调用。生产者中,运用到了KafkaTemplate类,用来生产主题和消息。消费者中,使用了注解@KafkaListener(topics = {“test”}),指定主题的应用。

//分布式的时候需要指定测试的启动类
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class KafkaTest {
    @Autowired
    KafkaProducer kafkaProducer;
    @Autowired
    KafkaConsumer kafkaConsumer;
    @Test
    public void testKafka(){
        kafkaProducer.sendMessage("test","你好");
        kafkaProducer.sendMessage("test","在吗");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@Component
class KafkaProducer{          //生产者
    //导入模板
    @Autowired
    private KafkaTemplate<String,Object> kafkaTemplate;
    //编写发送消息方法
    public void sendMessage(String topic,String content){  //参数为主题和内容
        kafkaTemplate.send(topic,content);
    }
}
@Component
class KafkaConsumer{
    @KafkaListener(topics = {"test"})   //设置接收的主题
    public void handelMessage(ConsumerRecord record){
        System.out.println(record.value());
    }
}

结果如下:
在这里插入图片描述
得到了订阅主题的消息,并且可以看到,得到的消息在项目启动前完成的,简单明了的看出了kafka的异步特点。

四、kafka的特点

解耦合
耦合的状态表示当你实现某个功能的时候,是直接接入当前接口,而利用消息队列,可以将相应的消息发送到消息队列,这样的话,如果接口出了问题,将不会影响到当前的功能。

在这里插入图片描述

异步处理
异步处理替代了之前的同步处理,异步处理不需要让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下让其他业务处理接口从消息队列中拉取消费处理即可。

流量削峰
高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止了系统的高请求,减轻服务器的请求处理压力。

发布/订阅模式
即利用Topic存储消息,消息生产者将消息发布到Topic中,同时有多个消费者订阅此topic,消费者可以从中消费消息,注意发布到Topic中的消息会被多个消费者消费,消费者消费数据之后,数据不会被清除,Kafka会默认保留一段时间,然后再删除。
在这里插入图片描述

五、使用kafka发布系统通知

在个人博客的项目中,因为评论、点赞、关注时刻发生在用户的处理操作上,而每一次这样的操作,系统都需要记录一条事件并通知给用户,如果每次将用户发生的操作及时记录,尤其在用户活跃的时间段,难免会对数据库和服务器造成压力。因此项目中选择了kafka作为消息队列,利用其异步处理和流量削峰的特点,保存系统通知。

触发事件
评论后,发布通知
点赞后,发布通知
关注后,发布通知

处理事件
封装事件对象
开发事件的生产者
开发事件的消费者

封装事件对象代码如下:

@Data
@Accessors(chain = true)
public class Event {
    //张三给李四点赞----userId是张三,entityUserId是李四
    //主题
    private String topic;
    //点赞人id
    private int userId;
    //实体类型(评论、回复)
    private int entityType;
    //实体id
    private int entityId;
    //被点赞人,实体的用户id
    private int entityUserId;
    //额外的数据存入map
    private Map<String,Object> data = new HashMap<>();

    public void setData(Map<String, Object> data) {   
        this.data = data;
    }
    public Event setData(String key,String object){  //对map数据的链式set
        this.data.put(key,object);
        return this;
    }
}

事件的生产者新建如下:

@Component //声明为一个组件,每次都是自动检测装配为bean,每次调用都会产生一个新的实体
public class EventProducer {
    @Autowired
    private KafkaTemplate kafkaTemplate;
    //处理事件,本质上是发送消息
    public void fireEvent(Event event){  //触发事件
        //将事件发送到指定的主题
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }
}

消费者如下:因为后续消费后的处理会整合es、七牛云、wk,代码较为复杂,但是只需要看@Autowired注解修饰的service用到的部分。消费了不同的主题(点赞、评论、回帖),消费了发帖事件、删贴事件。

@Component
public class EventConsumer implements CommunityConstant {
    //记日志
    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
    //消息最终是要往message表中插入数据
    @Autowired
    private MessageService messageService;
    @Autowired
    private DiscussPostService discussPostService;
    @Autowired
    private ElasticsearchService elasticsearchService;

    @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;

    //定时器
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    @KafkaListener(topics = {TOPIC_COMMENT,TOPIC_FOLLOW,TOPIC_LIKE})   //订阅不同的主题评论、关注、点赞
    public void handleCommentMessage(ConsumerRecord record){
        if (record==null||record.value()==null){
            logger.error("消息的内容为空");
            return;
        }
        //json转换为对象,并指定类型
        Event event = JSONObject.parseObject(record.value().toString(), Event.class);
        if (event==null){
            logger.error("消息格式错误");
            return;
        }
        //发送站内通知,主要是构造message对象,复用message表
        Message message = new Message();
        //User表中id为1代表系统用户
        message.setFromId(1);
        message.setToId(event.getEntityUserId());
        //消息id改为存主题
        message.setConversationId(event.getTopic());
        message.setCreateTime(new Date());
        message.setStatus(0);
        //content内容,用来存显示的一句话,存json字符串用于拼接
        Map<String,Object> 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<String,Object> entry:
                 event.getData().entrySet()) {
                content.put(entry.getKey(),entry.getValue());
            }
        }
        message.setContent(JSONObject.toJSONString(content));
        messageService.addMessage(message);
    }
    //消费发帖事件
    @KafkaListener(topics = {TOPIC_PUBLIC})
    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;
        }
        //查询出这个帖子
        DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
        //往es中存数据
        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;
        }
        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("生成长图成功:" + cmd);
        } catch (IOException e) {
            logger.error("生成长图失败:" + cmd, e.getMessage());
        }

        //七牛云启动时开启
        //启动定时器,监视该图片,一旦生成,则上传至七牛云
        UploadTask task= new UploadTask(fileName, suffix);
        Future future = taskScheduler.scheduleAtFixedRate(task, 500);
        task.setFuture(future);
    }
    class UploadTask implements Runnable{
        //文件名称
        private String fileName;
        //文件后缀
        private String suffix;
        //future启动任务的返回值,可以用来停止定时器
        private Future future;
        //开始时间
        private long startTime;
        //上传次数
        private int uploadTimes;

        public void setFuture(Future future) {
            this.future = future;
        }

        public UploadTask(String fileName, String suffix) {
            this.fileName = fileName;
            this.suffix = suffix;
            this.startTime = System.currentTimeMillis();
        }

        @Override
        public void run() {
            //生成图片失败(超时)
            if (System.currentTimeMillis() - startTime > 30000){
                logger.error("执行时间过长,终止任务:" + fileName);
                future.cancel(true);
                return;
            }
            //上传失败
            if (uploadTimes >= 3){
                logger.error("上传次数过多,终止任务:" + fileName);
                future.cancel(true);//取消定时
                return;
            }
            //本地路径
            String path = wkImageStorage + "/" + fileName + suffix;
            File file = new File(path);
            if (file.exists()){
                logger.info(String.format("开始第%d次上传[%s]。",++uploadTimes,fileName));
                //设置响应信息
                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.zone1()));
                try {
                    //开始上传图片
                    Response response = manager.put(path, fileName, uploadToken, null, "/image" + suffix, false);
                    //处理响应结果
                    JSONObject jsonObject = JSONObject.parseObject(response.bodyString());
                    if (jsonObject == null||jsonObject.get("code")==null||!jsonObject.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));
                }
            }else {
                logger.info("等待图片生成:["+fileName+"]");
            }
        }
    }
}

处理评论事件–CommentController
在这里插入图片描述
处理点赞事件–LikeController
在这里插入图片描述
发送系统通知(这里就是普通的业务流程了,没看头)
service层

//查询某个主题下最新的通知
    public Message findLatestMessage(int userId, String topic){
        Integer max = messageMapper.selectLaterNotice(userId, topic);
        if (max == null){
            return null;
        }
        return messageMapper.selectOne(new QueryWrapper<Message>().eq("id",max));
    }
    //查询某个主题所包含的通知数量
    public int findNoticeCount(int userId,String topic){
        Integer count = messageMapper.selectCount(new QueryWrapper<Message>().ne("status", 2)
                .eq("from_id", 1).eq("to_id", userId).eq("conversation_id", topic));
        return count;
    }
    //查询未读的通知数量
    public int findNoticeUnreadCount(int userId,String topic){
        if (topic == null){  //所以主题,status==0未读
            return messageMapper.selectCount(new QueryWrapper<Message>().eq("status",0)
            .eq("from_id",1).eq("to_id",userId));
        }else {
            return messageMapper.selectCount(new QueryWrapper<Message>().eq("status",0)
                    .eq("from_id",1).eq("to_id",userId).eq("conversation_id",topic));
        }
    }

controller层

 @RequestMapping(path = "notice/list",method = RequestMethod.GET)
    public String getNoticeList(Model model) {
        User user = hostHolder.getUser();
        //查询评论类通知
        Message message = messageService.findLatestMessage(user.getId(), TOPIC_COMMENT);
        Map<String, Object> messageVO = new HashMap<>();
        if (message != null) {
            messageVO.put("message", message);
            String content = message.getContent();
            //{&quot;entityType&quot;:1,&quot;entityId&quot;:275,&quot;postId&quot;:275,&quot;userId&quot;:111}
            content = HtmlUtils.htmlUnescape(content);  //这步可以去除上方转义字符
            HashMap<String, Object> data = JSONObject.parseObject(content, HashMap.class);
            //谁发给我
            messageVO.put("user", userService.findUserById((int) 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_COMMENT);
            messageVO.put("count", count);
            int unread = messageService.findNoticeUnreadCount(user.getId(), TOPIC_COMMENT);
            messageVO.put("unread", unread);
        }
        model.addAttribute("commentNotice", messageVO);
        //查询点赞类通知
        message = messageService.findLatestMessage(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((int) 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.findLatestMessage(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((int) 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);
        //查询未读消息数量
        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";
    }

结果如下:
在这里插入图片描述
在这里插入图片描述

六、使用总结

后续还用到kafka对发帖事件和删帖事件做了处理,并整合了ElasticSearch搜索引擎,将帖子存入其中。对其进行了快速搜索和一些词条高亮处理。也使用了kafka处理生成长图功能。这些处理写倒后面的文章中吧。
回到开始的问题中,不同服务间的调用总结如下:
在这里插入图片描述

七、补充

使用docker作为kafka的启动容器

# docker直接拉取kafka和zookeeper的镜像
docker pull wurstmeister/kafka
docker pull wurstmeister/zookeeper 
# 首先需要启动zookeeper,如果不先启动,启动kafka没有地方注册消息
docker run -it --name zookeeper -p 12181:2181 -d wurstmeister/zookeeper:latest
# 启动kafka容器,注意需要启动三台,注意端口的映射,都是映射到9092
# 第一台
docker run -it --name kafka01 -p 19092:9092 -d -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=192.168.233.129:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.233.129:19092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第二台
docker run -it --name kafka02 -p 19093:9092 -d -e KAFKA_BROKER_ID=1 -e KAFKA_ZOOKEEPER_CONNECT=192.168.233.129:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.233.129:19093 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第三台
docker run -it --name kafka03 -p 19094:9092 -d -e KAFKA_BROKER_ID=2 -e KAFKA_ZOOKEEPER_CONNECT=192.168.233.129:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.233.129:19094 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest

  • 10
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值