美文网首页
评论模块优化 - 数据表优化、添加缓存及用 Feign 与用户服

评论模块优化 - 数据表优化、添加缓存及用 Feign 与用户服

作者: solocoder | 来源:发表于2018-11-17 11:34 被阅读0次

    前段时间设计了系统的评论模块,并写了篇文章 评论模块 - 后端数据库设计及功能实现 讲解。

    大佬们在评论区提出了些优化建议,总结一下:

    1. 之前评论一共分了两张表,一个评论主表,一个回复表。这两张表的字段区别不大,在主表上加个 pid 字段就可以不用回复表合成一张表了。
    2. 评论表中存了用户头像,会引发一些问题。比如用户换头像时要把评论也一起更新不太合适,还可能出现两条评论头像不一致的情况。

    的确数据库设计的有问题,感谢 wangbjunJWang

    下面就对评论模块进行优化改造,首先更改表结构,合成一张表。评论表不存用户头像的话,需要从用户服务获取。用户服务提供获取头像的接口,两个服务间通过 Feign 通信。

    这样有个问题,如果一个资源的评论比较多,每个评论都调用用户服务查询头像还是有点慢,所以对评论查询加个 Redis 缓存。要是有新的评论,就把这个资源缓存的评论删除,下次请求时重新读数据库并将最新的数据缓存到 Redis 中。

    代码出自开源项目 coderiver,致力于打造全平台型全栈精品开源项目。
    项目地址:https://github.com/cachecats/coderiver

    本文将分四部分介绍

    1. 数据库改造
    2. 用户服务提供获取头像接口
    3. 评论服务用 Feign 访问用户服务取头像
    4. 使用 Redis 缓存数据

    一、数据库改造

    数据库表重新设计如下

    CREATE TABLE `comments_info` (
      `id` varchar(32) NOT NULL COMMENT '评论主键id',
      `pid` varchar(32) DEFAULT '' COMMENT '父评论id',
      `owner_id` varchar(32) NOT NULL COMMENT '被评论的资源id,可以是人、项目、资源',
      `type` tinyint(1) NOT NULL COMMENT '评论类型:对人评论,对项目评论,对资源评论',
      `from_id` varchar(32) NOT NULL COMMENT '评论者id',
      `from_name` varchar(32) NOT NULL COMMENT '评论者名字',
      `to_id` varchar(32) DEFAULT '' COMMENT '被评论者id',
      `to_name` varchar(32) DEFAULT '' COMMENT '被评论者名字',
      `like_num` int(11) DEFAULT '0' COMMENT '点赞的数量',
      `content` varchar(512) DEFAULT NULL COMMENT '评论内容',
      `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
      PRIMARY KEY (`id`),
      KEY `owner_id` (`owner_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
    

    相比之前添加了父评论id pid ,去掉了用户头像。owner_id 是被评论的资源id,比如一个项目下的所有评论的 owner_id 都是一样的,便于根据资源 id 查找该资源下的所有评论。

    与数据表对应的实体类 CommentsInfo

    package com.solo.coderiver.comments.dataobject;
    
    import lombok.Data;
    import org.hibernate.annotations.DynamicUpdate;
    
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import java.io.Serializable;
    import java.util.Date;
    
    /**
     * 评论表主表
     */
    @Entity
    @Data
    @DynamicUpdate
    public class CommentsInfo implements Serializable{
    
        private static final long serialVersionUID = -4568928073579442976L;
    
        //评论主键id
        @Id
        private String id;
    
        //该条评论的父评论id
        private String pid;
    
        //评论的资源id。标记这条评论是属于哪个资源的。资源可以是人、项目、设计资源
        private String ownerId;
    
        //评论类型。1用户评论,2项目评论,3资源评论
        private Integer type;
    
        //评论者id
        private String fromId;
    
        //评论者名字
        private String fromName;
    
        //被评论者id
        private String toId;
    
        //被评论者名字
        private String toName;
    
        //获得点赞的数量
        private Integer likeNum;
    
        //评论内容
        private String content;
    
        //创建时间
        private Date createTime;
    
        //更新时间
        private Date updateTime;
    }
    

    数据传输对象 CommentsInfoDTO

    在 DTO 对象中添加了用户头像,和子评论列表 children,因为返给前端要有层级嵌套。

    package com.solo.coderiver.comments.dto;
    
    import lombok.Data;
    
    import java.io.Serializable;
    import java.util.Date;
    import java.util.List;
    
    @Data
    public class CommentsInfoDTO implements Serializable {
    
        private static final long serialVersionUID = -6788130126931979110L;
    
        //评论主键id
        private String id;
    
        //该条评论的父评论id
        private String pid;
    
        //评论的资源id。标记这条评论是属于哪个资源的。资源可以是人、项目、设计资源
        private String ownerId;
    
        //评论类型。1用户评论,2项目评论,3资源评论
        private Integer type;
    
        //评论者id
        private String fromId;
    
        //评论者名字
        private String fromName;
    
        //评论者头像
        private String fromAvatar;
    
        //被评论者id
        private String toId;
    
        //被评论者名字
        private String toName;
    
        //被评论者头像
        private String toAvatar;
    
        //获得点赞的数量
        private Integer likeNum;
    
        //评论内容
        private String content;
    
        //创建时间
        private Date createTime;
    
        //更新时间
        private Date updateTime;
    
        private List<CommentsInfoDTO> children;
    
    }
    

    二、用户服务提供获取头像接口

    为了方便理解先看一下项目的结构,本项目中所有的服务都是这种结构

    image

    每个服务都分为三个 Module,分别是 client , common , server

    • client :为其他服务提供数据,Feign 的接口就写在这层。
    • common :放 clientserver 公用的代码,比如公用的对象、工具类。
    • server : 主要的逻辑代码。

    clientpom.xml 中引入 Feign 的依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency> 
    

    用户服务 user 需要对外暴露获取用户头像的接口,以使评论服务通过 Feign 调用。

    user_service 项目的 server 下新建 ClientController , 提供获取头像的接口。

    package com.solo.coderiver.user.controller;
    
    import com.solo.coderiver.user.common.UserInfoForComments;
    import com.solo.coderiver.user.dataobject.UserInfo;
    import com.solo.coderiver.user.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 对其他服务提供数据的 controller
     */
    @RestController
    @Slf4j
    public class ClientController {
    
        @Autowired
        UserService userService;
    
        /**
         * 通过 userId 获取用户头像
         *
         * @param userId
         * @return
         */
        @GetMapping("/get-avatar")
        public UserInfoForComments getAvatarByUserId(@RequestParam("userId") String userId) {
            UserInfo info = userService.findById(userId);
            if (info == null){
                return null;
            }
            return new UserInfoForComments(info.getId(), info.getAvatar());
        }
    }
    

    然后在 client 定义 UserClient 接口

    package com.solo.coderiver.user.client;
    
    import com.solo.coderiver.user.common.UserInfoForComments;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @FeignClient(name = "user")
    public interface UserClient {
    
        @GetMapping("/user/get-avatar")
        UserInfoForComments getAvatarByUserId(@RequestParam("userId") String userId);
    }
    

    三、评论服务用 Feign 访问用户服务取头像

    在评论服务的 server 层的 pom.xml 里添加 Feign 依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency> 
    

    并在入口类添加注解 @EnableFeignClients(basePackages = "com.solo.coderiver.user.client") 注意到配置扫描包的全类名

    package com.solo.coderiver.comments;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableSwagger2
    @EnableFeignClients(basePackages = "com.solo.coderiver.user.client")
    @EnableCaching
    public class CommentsApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CommentsApplication.class, args);
        }
    }
    

    封装 CommentsInfoService ,提供保存评论和获取评论的接口

    package com.solo.coderiver.comments.service;
    
    import com.solo.coderiver.comments.dto.CommentsInfoDTO;
    
    import java.util.List;
    
    public interface CommentsInfoService {
    
        /**
         * 保存评论
         *
         * @param info
         * @return
         */
        CommentsInfoDTO save(CommentsInfoDTO info);
    
        /**
         * 根据被评论的资源id查询评论列表
         *
         * @param ownerId
         * @return
         */
        List<CommentsInfoDTO> findByOwnerId(String ownerId);
    }
    

    CommentsInfoService 的实现类

    package com.solo.coderiver.comments.service.impl;
    
    import com.solo.coderiver.comments.converter.CommentsConverter;
    import com.solo.coderiver.comments.dataobject.CommentsInfo;
    import com.solo.coderiver.comments.dto.CommentsInfoDTO;
    import com.solo.coderiver.comments.repository.CommentsInfoRepository;
    import com.solo.coderiver.comments.service.CommentsInfoService;
    import com.solo.coderiver.user.client.UserClient;
    import com.solo.coderiver.user.common.UserInfoForComments;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @Service
    @Slf4j
    public class CommentsInfoServiceImpl implements CommentsInfoService {
    
        @Autowired
        CommentsInfoRepository repository;
    
        @Autowired
        UserClient userClient;
    
        @Override
        @CacheEvict(cacheNames = "comments", key = "#dto.ownerId")
        public CommentsInfoDTO save(CommentsInfoDTO dto) {
            CommentsInfo result = repository.save(CommentsConverter.DTO2Info(dto));
            return CommentsConverter.info2DTO(result);
        }
    
        @Override
        @Cacheable(cacheNames = "comments", key = "#ownerId")
        public List<CommentsInfoDTO> findByOwnerId(String ownerId) {
            List<CommentsInfo> infoList = repository.findByOwnerId(ownerId);
            List<CommentsInfoDTO> list = CommentsConverter.infos2DTOList(infoList)
                    .stream()
                    .map(dto -> {
                        //从用户服务取评论者头像
                        UserInfoForComments fromUser = userClient.getAvatarByUserId(dto.getFromId());
                        if (fromUser != null) {
                            dto.setFromAvatar(fromUser.getAvatar());
                        }
    
                        //从用户服务取被评论者头像
                        String toId = dto.getToId();
                        if (!StringUtils.isEmpty(toId)) {
                            UserInfoForComments toUser = userClient.getAvatarByUserId(toId);
                            if (toUser != null) {
                                dto.setToAvatar(toUser.getAvatar());
                            }
                        }
                        return dto;
                    }).collect(Collectors.toList());
            return sortData(list);
        }
    
        /**
         * 将无序的数据整理成有层级关系的数据
         *
         * @param dtos
         * @return
         */
        private List<CommentsInfoDTO> sortData(List<CommentsInfoDTO> dtos) {
            List<CommentsInfoDTO> list = new ArrayList<>();
            for (int i = 0; i < dtos.size(); i++) {
                CommentsInfoDTO dto1 = dtos.get(i);
                List<CommentsInfoDTO> children = new ArrayList<>();
                for (int j = 0; j < dtos.size(); j++) {
                    CommentsInfoDTO dto2 = dtos.get(j);
                    if (dto2.getPid() == null) {
                        continue;
                    }
                    if (dto1.getId().equals(dto2.getPid())) {
                        children.add(dto2);
                    }
                }
                dto1.setChildren(children);
                //最外层的数据只添加 pid 为空的评论,其他评论在父评论的 children 下
                if (dto1.getPid() == null || StringUtils.isEmpty(dto1.getPid())) {
                    list.add(dto1);
                }
            }
            return list;
        }
    }
    

    从数据库取出来的评论是无序的,为了方便前端展示,需要对评论按层级排序,子评论在父评论的 children 字段中。

    返回的数据:

    {
      "code": 0,
      "msg": "success",
      "data": [
        {
          "id": "1542338175424142145",
          "pid": null,
          "ownerId": "1541062468073593543",
          "type": 1,
          "fromId": "555555",
          "fromName": "张扬",
          "fromAvatar": null,
          "toId": null,
          "toName": null,
          "toAvatar": null,
          "likeNum": 0,
          "content": "你好呀",
          "createTime": "2018-11-16T03:16:15.000+0000",
          "updateTime": "2018-11-16T03:16:15.000+0000",
          "children": []
        },
        {
          "id": "1542338522933315867",
          "pid": null,
          "ownerId": "1541062468073593543",
          "type": 1,
          "fromId": "555555",
          "fromName": "张扬",
          "fromAvatar": null,
          "toId": null,
          "toName": null,
          "toAvatar": null,
          "likeNum": 0,
          "content": "你好呀嘿嘿",
          "createTime": "2018-11-16T03:22:03.000+0000",
          "updateTime": "2018-11-16T03:22:03.000+0000",
          "children": []
        },
        {
          "id": "abc123",
          "pid": null,
          "ownerId": "1541062468073593543",
          "type": 1,
          "fromId": "333333",
          "fromName": "王五",
          "fromAvatar": "http://avatar.png",
          "toId": null,
          "toName": null,
          "toAvatar": null,
          "likeNum": 3,
          "content": "这个小伙子不错",
          "createTime": "2018-11-15T06:06:10.000+0000",
          "updateTime": "2018-11-15T06:06:10.000+0000",
          "children": [
            {
              "id": "abc456",
              "pid": "abc123",
              "ownerId": "1541062468073593543",
              "type": 1,
              "fromId": "222222",
              "fromName": "李四",
              "fromAvatar": "http://222.png",
              "toId": "abc123",
              "toName": "王五",
              "toAvatar": null,
              "likeNum": 2,
              "content": "这个小伙子不错啊啊啊啊啊",
              "createTime": "2018-11-15T06:08:18.000+0000",
              "updateTime": "2018-11-15T06:36:47.000+0000",
              "children": []
            }
          ]
        }
      ]
    }
    

    四、使用 Redis 缓存数据

    其实缓存已经在上面的代码中做过了,两个方法上的

    @Cacheable(cacheNames = "comments", key = "#ownerId")
    @CacheEvict(cacheNames = "comments", key = "#dto.ownerId")
    

    两个注解就搞定了。第一次请求接口会走方法体

    关于 Redis 的使用方法,我专门写了篇文章介绍,就不在这里多说了,需要的可以看看这篇文章:

    Redis详解 - SpringBoot整合Redis,RedisTemplate和注解两种方式的使用

    以上就是对评论模块的优化,欢迎大佬们提优化建议~


    代码出自开源项目 coderiver,致力于打造全平台型全栈精品开源项目。

    coderiver 中文名 河码,是一个为程序员和设计师提供项目协作的平台。无论你是前端、后端、移动端开发人员,或是设计师、产品经理,都可以在平台上发布项目,与志同道合的小伙伴一起协作完成项目。

    coderiver河码 类似程序员客栈,但主要目的是方便各细分领域人才之间技术交流,共同成长,多人协作完成项目。暂不涉及金钱交易。

    计划做成包含 pc端(Vue、React)、移动H5(Vue、React)、ReactNative混合开发、Android原生、微信小程序、java后端的全平台型全栈项目,欢迎关注。

    项目地址:https://github.com/cachecats/coderiver


    您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

    相关文章

      网友评论

          本文标题:评论模块优化 - 数据表优化、添加缓存及用 Feign 与用户服

          本文链接:https://www.haomeiwen.com/subject/fqmbfqtx.html