假设读者现在已经对RedisTemplate的基本操作已经很熟悉了,开题之前先介绍一下
RedisTemplate 核心方法 execute
在
RedisTemplate
中,定义了几个
execute()
方法,这些方法是
RedisTemplate
的核心方法。
RedisTemplate
中很多其他方法均是通过调用
execute
来执行具体的操作。例如:
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisOperations#delete(java.util.Collection)
*/
@Override
public Long delete(Collection<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0L;
}
byte[][] rawKeys = rawKeys(keys);
return execute(connection -> connection.del(rawKeys), true);
}
上述方法是
RedisTemplate
中
delete
方法的源码,它就是使用
execute()
来执行具体的删除操作(即调用
connection.del(rawKeys)
方法)。
方法说明如下表:
方法定义 | 方法说明 |
---|---|
T execute(RedisCallback action) | 在 Redis 连接中执行给定的操作 |
T execute(RedisCallback action, boolean exposeConnection) | 在连接中执行给定的操作对象,可以公开也可以不公开。 |
T execute(RedisCallback action, boolean exposeConnection, boolean pipeline) | 在可以公开或不公开的连接中执行给定的操作对象。 |
T execute(RedisScript script, List keys, Object… args) | 执行给定的 RedisScript |
T execute(RedisScript script, RedisSerializer<?> argsSerializer, RedisSerializer resultSerializer, List keys, Object… args) | 执行给定的 RedisScript,使用提供的 RedisSerializers 序列化脚本参数和结果。 |
T execute(SessionCallback session) | 执行 Redis 会话 |
示例
execute(RedisCallback) 简单用法
使用 RedisTemplate 直接调用 opsFor** 来操作 Redis 数据库,每执行一条命令是要重新拿一个连接,因此很耗资源。如果让一个连接直接执行多条语句的方法就是使用 RedisCallback(它太复杂,不常用),推荐使用 SessionCallback。
本例将演示使用 RedisCallback 向 Redis 写入数据,然后再将写入的数据取出来,输出到控制台。如下:
示例:
execute(RedisCallback) 简单用法
使用
RedisTemplate
直接调用
opsFor**
来操作
Redis
数据库,每执行一条命令是要重新拿一个连接,因此很耗资源。如果让一个连接直接执行多条语句的方法就是使用
RedisCallback(它太复杂,不常用),推荐使用 SessionCallback。
本例将演示使用
RedisCallback 向 Redis
写入数据,然后再将写入的数据取出来,输出到控制台。如下:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ExecuteSimple {
/** 注入 RedisTemplate */
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
public void contextLoads() {
redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
RedisStringCommands commands = connection.stringCommands();
// 写入缓存
commands.set("execute_key".getBytes(), "hello world".getBytes());
// 从缓存获取值
byte[] value = commands.get("execute_key".getBytes());
System.out.println(new String(value));
return null;
}
});
}
}
运行示例,输出结果如下:
hello world
其实,在
RedisTemplate 中
,其他很多方法均是通过调用
execute()
方法来实现,只是不同的方法实现不同的回调接口。部分源码如下:
// ...
@Override
public Long increment(K key) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.incr(rawKey), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ValueOperations#increment(java.lang.Object, long)
*/
@Override
public Long increment(K key, long delta) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.incrBy(rawKey, delta), true);
}
@Override
public void set(K key, V value, long timeout, TimeUnit unit) {
byte[] rawKey = rawKey(key);
byte[] rawValue = rawValue(value);
execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
potentiallyUsePsetEx(connection);
return null;
}
public void potentiallyUsePsetEx(RedisConnection connection) {
if (!TimeUnit.MILLISECONDS.equals(unit) || !failsafeInvokePsetEx(connection)) {
connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue);
}
}
private boolean failsafeInvokePsetEx(RedisConnection connection) {
boolean failed = false;
try {
connection.pSetEx(rawKey, timeout, rawValue);
} catch (UnsupportedOperationException e) {
// in case the connection does not support pSetEx return false to allow fallback to other operation.
failed = true;
}
return !failed;
}
}, true);
}
// ...
execute(SessionCallback) 简单用法
使用
RedisTemplate
直接调用
opsFor**
来操作 Redis 数据库,每执行一条命令是要重新拿一个连接,因此很耗资源。如果让一个连接直接执行多条语句的方法就是使用
SessionCallback
,还可以使用
RedisCallback
(它太复杂,不常用)。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SessionCallbackSimple {
/** 注入 RedisTemplate */
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
public void contextLoads() {
redisTemplate.execute(new SessionCallback() {
@Override
public String execute(RedisOperations operations) throws DataAccessException {
operations.opsForValue().set("valueK1", "value1");
System.out.println("valueK1 = " + operations.opsForValue().get("valueK1"));
operations.opsForList().leftPushAll("listK1", "one", "two");
System.out.println("listK1 = " + operations.opsForList().size("listK1"));
return null;
}
});
}
}
运行示例,输出如下:
valueK1 = value1
listK1 = 2
关于RedisTemplate的底层核心方法详情可参考
RedisTemplate 核心方法 execute
下面代码演示各大社交网站常见功能
点赞
实现思路:
- 1.获取当前登录的用户
- 2.使用redis的set类型的数据结构,k-被点赞的实体类型+对应实体id,v-对该实体进行点赞操作的用户的id
- 3
代码实现:
public void like(int userId,int entityType,int entityId,int entityUserId){
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
// 构建被点赞的实体对应redis的k
String entityLikeKey = "like:entity:entityType:entityId";
// 构建被点赞的实体对应的作者再redis中的key,用于统计后期某用户总共收获了多少个赞,社交论坛(例如虎扑)就有这个功能
String userLikeKey = "like:user:entityUserId";
// 判断集合中是否有userId这个值
Boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
// 开启事务
operations.multi();
if (isMember){
// 移除userId这个值
operations.opsForSet().remove(entityLikeKey,userId);
//减一(方便统计用户后期获得赞的总数)
operations.opsForValue().decrement(userLikeKey);
}else {
operations.opsForSet().add(entityLikeKey,userId);
//加一(方便统计用户后期获得赞的总数)
operations.opsForValue().increment(userLikeKey);
}
// 提交事务
return operations.exec();
}
});
}
然后在前端局部刷新页面的时候统计一下被点赞的实体对应redis的k中包含的vue的数量就是点赞数,同时判断一下vue列表是否包含当前登录的用户id就能知道当前用户是否已经对某个帖子进行过点赞:
// 查询某实体点赞的数量
public long findEntityLikeCount(int entityType,int entityId){
String entityLikeKey = "like:entity:entityType:entityId";
return redisTemplate.opsForSet().size(entityLikeKey);
}
//查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId,int entityType,int entityId){
String entityLikeKey = "like:entity:entityType:entityId";
return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1 : 0;
}
一般情况下,内存中的数据一般在磁盘上也要有一份作为数据持久化存储,网站的运营人员可以依据数据库中的数据来更好的分析网站的用户使用情况,用户爱看哪一类文章等等,如果只是存储在内存中很容易丢失,市面上的做法一般都是数据库与内存中都保持一份,但是如果直接更新内存之后在去操作数据库,在并发量大的时候,对数据库的压力来讲是很大的,大量请求进来造成大量行锁,对于使用者来讲体验肯定是不好的,一般这种情况下可以引入消息队列来进行异步处理数据库,达到更好的响应效果
1.将用户出发的点赞,关注啥的操作封装成一个事件
/**
* 事件(将用户触发的事件封装成一个对象)
*/
public class Event {
private String topic; // 事件的主题
private int userId; // 事件的来源,触发的人
private int entityType; // 事件发生在哪种类型上
private int entityId; // 事件发生在的实体的id
private int entityUserId; //事件发生的实体对应的作者的id
private Map<String,Object> data = new HashMap<>();
public String getTopic() {
return topic;
}
public Event setTopic(String topic) {
this.topic = topic;
return this;
}
public int getUserId() {
return userId;
}
public Event setUserId(int userId) {
this.userId = userId;
return this;
}
public int getEntityType() {
return entityType;
}
public Event setEntityType(int entityType) {
this.entityType = entityType;
return this;
}
public int getEntityId() {
return entityId;
}
public Event setEntityId(int entityId) {
this.entityId = entityId;
return this;
}
public int getEntityUserId() {
return entityUserId;
}
public Event setEntityUserId(int entityUserId) {
this.entityUserId = entityUserId;
return this;
}
public Map<String, Object> getData() {
return data;
}
public Event setData(String key, Object value) {
this.data.put(key, value);
return this;
}
@Override
public String toString() {
return "Event{" +
"topic='" + topic + '\'' +
", userId=" + userId +
", entityType=" + entityType +
", entityId=" + entityId +
", entityUserId=" + entityUserId +
", data=" + data +
'}';
}
}
2.获取当前登陆的用户对某个帖子的点赞状态,也就是是否已经点过赞
//查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId,int entityType,int entityId){
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1 : 0;
}
-
如果当前登录的用户该对帖子是没有点过赞的,那摩就可以通过消息队列往数据库存一份数据,
如果是取消点赞,只需要清除redis中的数据即可,数据库的可以不用管,防止频繁点赞取消对数据库和队列造成过大压力,查询的时候只需要去redis中获取就行,如果追求完美,其实也可以删除数据库对应的记录,但是为了防止流量过大,这里应该做限流,比如一秒钟只允许发送多少个请求,redis可以实现这种功能,这里不细说,可以翻阅我的博客查找相关文章
新建一张表用于存储发生的点赞,关注事件,表实体类如下:
/**
* 会话列表对应实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Message {
private int id;
private int fromId; // 此条消息来源于谁 1-来源于系统通知(点赞关注操作这里都放置于系统通知,两个用户评论或者聊天才放置对应的id)
private int toId; // 此条消息发送给谁
private String conversationId; // 两者之间会话的id
private String content; // 会话的内容
private int status; // 消息的状态,0-未读,1-已读,2-删除
private Date createTime; // 创建的时间
}
完整代码如下:可以只看核心部分
@Controller
public class LikeController implements EmailStatus {
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
@Autowired
private EventProducer eventProducer;
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/like")
@ResponseBody
public String like(@RequestParam("entityType") int entityType,
@RequestParam("entityId") int entityId,@RequestParam("entityUserId") int entityUserId,@RequestParam("postId") int postId){
// 获取到当前用户
User user = hostHolder.getUser();
// 点赞
likeService.like(user.getId(),entityType,entityId,entityUserId);
// 获取点赞的数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 获取当前用户点赞的状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 返回的结果,封装成一个Map集合
Map<String,Object> map = new HashMap<>();
map.put("likeCount",likeCount);
map.put("likeStatus",likeStatus);
//触发点赞事件
if (likeStatus==1){
Event event = new Event().setTopic(TOPIC_LIKE)
.setEntityId(entityId)
.setEntityType(entityType)
.setUserId(hostHolder.getUser().getId())
.setEntityUserId(entityUserId)
.setData("postId",postId);
eventProducer.fireEvent(event);
}
if (entityType == ENTITY_TYPE_POST){
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,postId) ;
}
return StringUtil.getJsonString(0,null,map);
}
生产者端:
@Component
public class EventProducer {
@Autowired
private ConfirmCallbackService confirmCallbackService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送系统消息
*/
public void fireEvent(Event event){
rabbitTemplate.setConfirmCallback(confirmCallbackService);
rabbitTemplate.convertAndSend("messageExchange", event.getTopic(), JSONObject.toJSONString(event));
}
}
消费者端:
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "aa",durable ="true"), // 声明临时队列
exchange = @Exchange(name = "messageExchange",type = "topic"),
key = {TOPIC_COMMENT,TOPIC_LIKE,TOPIC_FOLLOW}
)
})
public void handleCommentMessage(@Payload String msg) {
if (msg == null ) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(msg, Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
// 发送站内通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId());
content.put("entityType", event.getEntityType());
content.put("entityId", event.getEntityId());
if(!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
messageService.addMessage(message);
}
以上这种方式应对高并发场景是比较好的,当然也有为了简单直接将点赞数存在mysql的,将点赞数当作帖子的一个属性,这种在面对高并发时修改点赞数会触发大量行锁是性能是不太好的,不太推荐
关注
同点赞差不多,这里使用zset
实现思路:
- 1.获取当前登录的用户
- 2.使用redis的zset类型的数据结构,k-被关注的用户对应的id,v-对该实体进行点赞操作的用户的id
- 3
/**
* 关注
* @param userId 执行关注操作的用户的id
* @param entityType 被关注的实体类型
* @param entityId 被关注的实体的id
* @return
*/
public void follow(int userId,int entityType,int entityId){
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
// 构造目标key
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
// 构造粉丝key
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
// 开启事务
operations.multi();
// 构建当前登录用户的关注列表
operations.opsForZSet().add(followeeKey,entityId,System.currentTimeMillis());
// 构建被关注用户的粉丝列表
operations.opsForZSet().add(followerKey,userId,System.currentTimeMillis());
// 提交事务
return operations.exec();
}
});
}
完整代码:
//关注
@PostMapping("/follow")
@ResponseBody
public String follow(int entityType,int entityId){
// 获取到当前登录的用户
User user = hostHolder.getUser();
if(user == null){
throw new RuntimeException("当前用户未登录");
}
// 关注
followService.follow(user.getId(),entityType,entityId);
// 触发关注事件
Event event = new Event().setTopic(TOPIC_FOLLOW)
.setUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityId);
eventProducer.fireEvent(event);
return StringUtil.getJsonString(0,"已关注");
}
消费者端同上一样
取消关注:
/**
* 取消关注
* @param userId 执行取消关注操作的用户的id
* @param entityType 被取消关注的实体类型
* @param entityId 被取消关注的实体的id
*/
public void unfollow(int userId,int entityType,int entityId){
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
// 构造目标key
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
// 构造粉丝key
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
// 开启事务
operations.multi();
operations.opsForZSet().remove(followeeKey,entityId);
operations.opsForZSet().remove(followerKey,userId);
// 提交事务
return operations.exec();
}
});
}
完整代码:
//取消关注
@PostMapping("/unfollow")
@ResponseBody
public String unfollow(int entityType,int entityId){
// 获取到当前登录的用户
User user = hostHolder.getUser();
if(user == null){
throw new RuntimeException("当前用户未登录");
}
// 关注
followService.unfollow(user.getId(),entityType,entityId);
return StringUtil.getJsonString(0,"已取消关注");
}
取top榜单排名前五:
这里大家应该想到使用redis的zset类型了,
思路:
- 1.构建k-随意取常量字符串,见名知意就行,v-帖子的id,score 这里可以根据点赞量,阅读量啥的某种算法去做运算,也可以直接存点赞量
- 2.代码实现:
String key = BLOG_LIKED_KEY + "like";
// 1.查询top5的点赞帖子或者用户id, zrange key 0 4
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 2.解析出其中的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idStr = StrUtil.join(",", ids);
// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
List<UserDTO> userDTOS = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 4.返回
return Result.ok(userDTOS);
下面说一下评论功能,先看一下抖音的评论区:
这里大致说一下,就是某个博主发了一条视频,然后底下一群人对这个视频进行评论,如果是对视频进行评论,那摩这里设置父id = 0,如果是针对别人的评论进行评论,那摩被评论的那句话就是执行评论操作的这条语句的父id
这里简单设计一下数据库表,字段如下:
假设现在表中数据有如下:
现在根据抖音那种样式肯定是要展示所有评论,因此代码实现如下:
-
先定义一个Vo类,该类相当于一条评论,因此他具有父节点或者子节点:
评论实体类:
/**
* 获取所有评论
* @return
*/
@GetMapping("/all/comment")
public List<CommentTree> getAllComment(){
List<CommentTree> commentData = getCommentData(0);
return commentData;
}
/**
* 递归获取子节点下的子节点
* @param parentId
* @param parentId
* @return
*/
public List<CommentTree> getCommentData(Integer parentId){
List<CommentTree> commentTrees = new ArrayList<>();
// 通过父id查询列表
List<Comment> comments = userMapper.selectByParentId(parentId);
for (Comment comment : comments) {
List<CommentTree> commentData = getCommentData(comment.getId());
CommentTree commentTree = new CommentTree();
commentTree.setCommentId(comment.getId());
commentTree.setParentId(comment.getParentId());
commentTree.setFromId(comment.getFromId());
commentTree.setCommentContent(comment.getContent());
// 如果为空,说明该节点下没有子节点
if (CollectionUtils.isEmpty(commentData)){
commentTrees.add(commentTree);
continue;
}else {
commentTree.setChildCommentTrees(commentData);
commentTrees.add(commentTree);
}
}
return commentTrees;
}
最终效果如下:
[
{
"commentId": 1,
"commentContent": "这篇文章写得好",
"parentId": 0,
"fromId": 1,
"childCommentTrees": [
{
"commentId": 2,
"commentContent": "写得确实不错",
"parentId": 1,
"fromId": 2,
"childCommentTrees": []
},
{
"commentId": 3,
"commentContent": "那是你觉得不错",
"parentId": 1,
"fromId": 3,
"childCommentTrees": []
}
]
},
{
"commentId": 4,
"commentContent": "博主这篇文章似曾相识",
"parentId": 0,
"fromId": 4,
"childCommentTrees": [
{
"commentId": 5,
"commentContent": "你觉得他是抄袭的吗?",
"parentId": 4,
"fromId": 5,
"childCommentTrees": [
{
"commentId": 6,
"commentContent": "我也感觉在哪里看到过",
"parentId": 5,
"fromId": 6,
"childCommentTrees": []
}
]
}
]
}
]
然后交给前端进行数据展示就ok,但是这种算法其实是在for循环中进行了多次访问数据库操作,如果一级父类比较多的话,那摩接口的rt肯定是不得行的,因此这里给出优化,首先一次性将所有数据查出来,然后再jvm内存中使用stream流将数据进行过滤,代码如下:
/**
* 获取所有评论方法2
* @return
*/
@GetMapping("/all/comment2")
public List<CommentTree> getAllComment2(){
List<Comment> comments = userMapper.selectAll();
// 组装成父子的树状结构图
// 获取parentId = 0 的数据,也就是一级分类
List<CommentTree> commentTrees= comments.stream().filter(Objects::nonNull).filter(comment -> comment.getParentId() == 0)
.map(comment -> {
CommentTree commentTree = new CommentTree();
commentTree.setCommentId(comment.getId());
commentTree.setCommentContent(comment.getContent());
commentTree.setFromId(comment.getFromId());
commentTree.setParentId(comment.getParentId());
commentTree.setChildCommentTrees(getChildrens(comment,comments));
return commentTree;
}).collect(Collectors.toList());
return commentTrees;
}
//递归查找所有菜单的子菜单
private List<CommentTree> getChildrens(Comment root, List<Comment> all) {
List<CommentTree> children = all.stream().filter(comment -> {
return comment.getParentId().equals(root.getId());
}).map(comment -> {
CommentTree commentTree = new CommentTree();
commentTree.setParentId(comment.getParentId());
commentTree.setCommentId(comment.getId());
commentTree.setFromId(comment.getFromId());
commentTree.setCommentContent(comment.getContent());
commentTree.setChildCommentTrees(getChildrens(comment, all));
return commentTree;
}).collect(Collectors.toList());
return children;
}
最终效果如下:
[
{
"commentId": 1,
"commentContent": "这篇文章写得好",
"parentId": 0,
"fromId": 1,
"childCommentTrees": [
{
"commentId": 2,
"commentContent": "写得确实不错",
"parentId": 1,
"fromId": 2,
"childCommentTrees": []
},
{
"commentId": 3,
"commentContent": "那是你觉得不错",
"parentId": 1,
"fromId": 3,
"childCommentTrees": []
}
]
},
{
"commentId": 4,
"commentContent": "博主这篇文章似曾相识",
"parentId": 0,
"fromId": 4,
"childCommentTrees": [
{
"commentId": 5,
"commentContent": "你觉得他是抄袭的吗?",
"parentId": 4,
"fromId": 5,
"childCommentTrees": [
{
"commentId": 6,
"commentContent": "我也感觉在哪里看到过",
"parentId": 5,
"fromId": 6,
"childCommentTrees": []
}
]
}
]
}
]