評論模組優化 - 資料表優化、新增快取及用 Feign 與使用者服務通訊
前段時間設計了系統的評論模組,並寫了篇文章 評論模組 - 後端資料庫設計及功能實現 講解。
大佬們在評論區提出了些優化建議,總結一下:
- 之前評論一共分了兩張表,一個評論主表,一個回覆表。這兩張表的欄位區別不大,在主表上加個 pid 欄位就可以不用回覆表合成一張表了。
- 評論表中存了使用者頭像,會引發一些問題。比如使用者換頭像時要把評論也一起更新不太合適,還可能出現兩條評論頭像不一致的情況。
的確資料庫設計的有問題,感謝 wangbjun 和 JWang。
下面就對評論模組進行優化改造,首先更改表結構,合成一張表。評論表不存使用者頭像的話,需要從使用者服務獲取。使用者服務提供獲取頭像的介面,兩個服務間通過 Feign 通訊。
這樣有個問題,如果一個資源的評論比較多,每個評論都呼叫使用者服務查詢頭像還是有點慢,所以對評論查詢加個 Redis 快取。要是有新的評論,就把這個資源快取的評論刪除,下次請求時重新讀資料庫並將最新的資料快取到 Redis 中。
程式碼出自開源專案
coderiver
,致力於打造全平臺型全棧精品開源專案。
專案地址:github.com/cachecats/c…
本文將分四部分介紹
- 資料庫改造
- 使用者服務提供獲取頭像介面
- 評論服務用 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<CommentsInfoDTO> children;
}
複製程式碼
二、使用者服務提供獲取頭像介面
為了方便理解先看一下專案的結構,本專案中所有的服務都是這種結構
每個服務都分為三個 Module,分別是 client
, common
, server
。
client
:為其他服務提供資料,Feign 的介面就寫在這層。common
:放client
和server
公用的程式碼,比如公用的物件、工具類。server
: 主要的邏輯程式碼。
在 client
的 pom.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後端的全平臺型全棧專案,歡迎關注。
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~