评论模块优化 - 数据表优化、添加缓存及用 Feign 与用户服务通信
前段时间设计了系统的评论模块,并写了篇文章 评论模块 - 后端数据库设计及功能实现 讲解。
大佬们在评论区提出了些优化建议,总结一下:
- 之前评论一共分了两张表,一个评论主表,一个回复表。这两张表的字段区别不大,在主表上加个 pid 字段就可以不用回复表合成一张表了。
- 评论表中存了用户头像,会引发一些问题。比如用户换头像时要把评论也一起更新不太合适,还可能出现两条评论头像不一致的情况。
的确数据库设计的有问题,感谢 wangbjun 和 JWang。
下面就对评论模块进行优化改造,首先更改表结构,合成一张表。评论表不存用户头像的话,需要从用户服务获取。用户服务提供获取头像的接口,两个服务间通过 Feign 通信。
这样有个问题,如果一个资源的评论比较多,每个评论都调用用户服务查询头像还是有点慢,所以对评论查询加个 Redis 缓存。要是有新的评论,就把这个资源缓存的评论删除,下次请求时重新读数据库并将最新的数据缓存到 Redis 中。
代码出自开源项目
coderiver,致力于打造全平台型全栈精品开源项目。
项目地址:https://github.com/cachecats/coderiver
本文将分四部分介绍
- 数据库改造
- 用户服务提供获取头像接口
- 评论服务用 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后端的全平台型全栈项目,欢迎关注。
项目地址:https://github.com/cachecats/coderiver
您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~
阅读更多- Nginx服务优化(隐藏版本信息,设置管理用户和组,设置网页缓存时间)
- asp.net中为用户控件添加数据缓存依赖项时要注意的事项
- 关于api接口以及页面数据通信域名,缓存cdn设置优化
- Qt/E客户端服务期间通信--数据串行化
- oracle优化缓存对象与数据
- 稳扎稳打Silverlight(58) - 4.0通信之WCF RIA Services: 通过 Domain Service, 以 MVVM 模式实现数据的添加、删除、修改和查询
- C# 通过服务启动窗体(把窗体添加到服务里)实现用户交互的windows服务
- 我看数据安全,服务用户,符合常理
- MaxCompute理解数据、运算和用户的大脑:基于代价的优化器
- 医药采购系统的添加用户模块
- PHP简单系统数据添加以及数据删除模块源文件下载
- SpringCloud(第 004 篇)用户服务类(添加服务注册,将用户微服务注册到 EurekaServer 中)
- Win7 Chrome的缓存及用户数据位置修改
- 更改ArcGIS 已发布的缓存地图服务的服务属性——添加WMS服务
- 小细节大改变:用户信息缓存以及其他几点ajax请求的优化
- C#--WinForm查询模块--优化用户体验
- 添加用户记录时出现数据类型不匹配的问题。。。
- 如何提高APP用户留存率:优化APP推送服务
- SQL批量添加数据库中所有用户数据表描述
- 日处理20亿数据,实时用户行为服务系统架构实践