評論模組優化 - 資料表優化、新增快取及用 Feign 與使用者服務通訊
前段時間設計了系統的評論模組,並寫了篇文章 ofollow,noindex">評論模組 - 後端資料庫設計及功能實現 講解。
大佬們在評論區提出了些優化建議,總結一下:
- 之前評論一共分了兩張表,一個評論主表,一個回覆表。這兩張表的欄位區別不大,在主表上加個 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後端的全平臺型全棧專案,歡迎關注。
專案地址: github.com/cachecats/c…
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星:sparkles: ~