評論模塊優化 - 數據表優化、添加緩存及用 Feign 與用戶服務通訊

前段時間設計了系統的評論模塊,並寫了篇文章 評論模塊 - 後端數據庫設計及功能實現 講解。前端

大佬們在評論區提出了些優化建議,總結一下:java

  1. 以前評論一共分了兩張表,一個評論主表,一個回覆表。這兩張表的字段區別不大,在主表上加個 pid 字段就能夠不用回覆表合成一張表了。
  2. 評論表中存了用戶頭像,會引起一些問題。好比用戶換頭像時要把評論也一塊兒更新不太合適,還可能出現兩條評論頭像不一致的狀況。

的確數據庫設計的有問題,感謝 wangbjunJWanggit

下面就對評論模塊進行優化改造,首先更改表結構,合成一張表。評論表不存用戶頭像的話,須要從用戶服務獲取。用戶服務提供獲取頭像的接口,兩個服務間經過 Feign 通訊。程序員

這樣有個問題,若是一個資源的評論比較多,每一個評論都調用用戶服務查詢頭像仍是有點慢,因此對評論查詢加個 Redis 緩存。要是有新的評論,就把這個資源緩存的評論刪除,下次請求時從新讀數據庫並將最新的數據緩存到 Redis 中。github

代碼出自開源項目 coderiver,致力於打造全平臺型全棧精品開源項目。
項目地址:github.com/cachecats/c…web

本文將分四部分介紹spring

  1. 數據庫改造
  2. 用戶服務提供獲取頭像接口
  3. 評論服務用 Feign 訪問用戶服務取頭像
  4. 使用 Redis 緩存數據

1、數據庫改造

數據庫表從新設計以下sql

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 查找該資源下的全部評論。數據庫

與數據表對應的實體類 CommentsInfojson

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;

}
複製代碼

2、用戶服務提供獲取頭像接口

爲了方便理解先看一下項目的結構,本項目中全部的服務都是這種結構

每一個服務都分爲三個 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);
}
複製代碼

3、評論服務用 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": []
        }
      ]
    }
  ]
}
複製代碼

4、使用 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…


您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~

相關文章
相關標籤/搜索