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

栏目: 数据库 · 发布时间: 6年前

内容简介:前段时间设计了系统的评论模块,并写了篇文章大佬们在评论区提出了些优化建议,总结一下:的确数据库设计的有问题,感谢wangbjun 和JWang。

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

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

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

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

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

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

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

项目地址: github.com/cachecats/c…

本文将分四部分介绍

  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;

}
复制代码

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

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

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

每个服务都分为三个 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 后端的全平台型全栈项目,欢迎关注。

项目地址: github.com/cachecats/c…

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


以上所述就是小编给大家介绍的《评论模块优化 - 数据表优化、添加缓存及用 Feign 与用户服务通信》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Developer's Guide to Social Programming

Developer's Guide to Social Programming

Mark D. Hawker / Addison-Wesley Professional / 2010-8-25 / USD 39.99

In The Developer's Guide to Social Programming, Mark Hawker shows developers how to build applications that integrate with the major social networking sites. Unlike competitive books that focus on a s......一起来看看 《Developer's Guide to Social Programming》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具