资讯专栏INFORMATION COLUMN

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

iflove / 2998人阅读

摘要:前段时间设计了系统的评论模块,并写了篇文章评论模块后端数据库设计及功能实现讲解。下面就对评论模块进行优化改造,首先更改表结构,合成一张表。评论表不存用户头像的话,需要从用户服务获取。用户服务提供获取头像的接口,两个服务间通过通信。

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

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

之前评论一共分了两张表,一个评论主表,一个回复表。这两张表的字段区别不大,在主表上加个 pid 字段就可以不用回复表合成一张表了。

评论表中存了用户头像,会引发一些问题。比如用户换头像时要把评论也一起更新不太合适,还可能出现两条评论头像不一致的情况。

的确数据库设计的有问题,感谢 wangbjun 和 JWang。

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

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

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

本文将分四部分介绍

数据库改造

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

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

使用 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 children;

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

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

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

client :为其他服务提供数据,Feign 的接口就写在这层。

common :放 clientserver 公用的代码,比如公用的对象、工具类。

server : 主要的逻辑代码。

clientpom.xml 中引入 Feign 的依赖


    org.springframework.cloud
    spring-cloud-starter-openfeign
 

用户服务 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 依赖


    org.springframework.cloud
    spring-cloud-starter-openfeign
 

并在入口类添加注解 @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 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 findByOwnerId(String ownerId) {
        List infoList = repository.findByOwnerId(ownerId);
        List 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 sortData(List dtos) {
        List list = new ArrayList<>();
        for (int i = 0; i < dtos.size(); i++) {
            CommentsInfoDTO dto1 = dtos.get(i);
            List 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/...

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72272.html

相关文章

  • 你想要的全平台全栈开源项目-Vue、React、小程序、安卓、ReactNative、java后端

    摘要:无论你是前端后端移动端开发人员,或是设计师产品经理,都可以在平台上发布项目,与志同道合的小伙伴一起协作完成项目。 全平台全栈开源项目 coderiver 今天终于开始前后端联调了~ 首先感谢大家的支持,coderiver 在 GitHub 上开源两周,获得了 54 个 Star,9 个 Fork,5 个 Watch。 这些鼓励和认可也更加坚定了我继续写下去的决心~ 再次感谢各位大佬! ...

    Maxiye 评论0 收藏0
  • 2021 年最新基于 Spring Cloud 的微服务架构分析

    摘要:是一个相对比较新的微服务框架,年才推出的版本虽然时间最短但是相比等框架提供的全套的分布式系统解决方案。提供线程池不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务器雪崩的问题。通过互相注册的方式来进行消息同步和保证高可用。 Spring Cloud 是一个相对比较新的微服务框架,...

    cikenerd 评论0 收藏0
  • 外行人都能看懂的SpringCloud,错过了血亏!

    摘要:集群系统中的单个计算机通常称为节点,通常通过局域网连接,但也有其它的可能连接方式。这样就高兴了,可以专心写自己的,前端就专门交由小周负责了。于是,小周和就变成了协作开发。都是为了项目正常运行以及迭代。 一、前言 只有光头才能变强 认识我的朋友可能都知道我这阵子去实习啦,去的公司说是用SpringCloud(但我觉得使用的力度并不大啊~~)... 所以,这篇主要来讲讲SpringClou...

    沈建明 评论0 收藏0
  • 外行人都能看懂的SpringCloud,错过了血亏!

    摘要:集群系统中的单个计算机通常称为节点,通常通过局域网连接,但也有其它的可能连接方式。这样就高兴了,可以专心写自己的,前端就专门交由小周负责了。于是,小周和就变成了协作开发。都是为了项目正常运行以及迭代。 一、前言 只有光头才能变强 认识我的朋友可能都知道我这阵子去实习啦,去的公司说是用SpringCloud(但我觉得使用的力度并不大啊~~)... 所以,这篇主要来讲讲SpringClou...

    enda 评论0 收藏0

发表评论

0条评论

iflove

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<