評論模組優化 - 資料表優化、新增快取及用 Feign 與使用者服務通訊

solocoder發表於2018-11-16

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

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

  1. 之前評論一共分了兩張表,一個評論主表,一個回覆表。這兩張表的欄位區別不大,在主表上加個 pid 欄位就可以不用回覆表合成一張表了。
  2. 評論表中存了使用者頭像,會引發一些問題。比如使用者換頭像時要把評論也一起更新不太合適,還可能出現兩條評論頭像不一致的情況。

的確資料庫設計的有問題,感謝 wangbjunJWang

下面就對評論模組進行優化改造,首先更改表結構,合成一張表。評論表不存使用者頭像的話,需要從使用者服務獲取。使用者服務提供獲取頭像的介面,兩個服務間通過 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…


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

相關文章