背景
常规消息树或菜单栏可以使用递归进行构建,但最近遇见一个需求,需要提供一个留言板的功能,不使用懒加载,前端页面需要一次性展示所有留言,并且支持新增留言和删除留言等功能;
为了提升后端API的高可用性能,所以针对此需求进行了一些简单的思考,并以此做一个记录;
具体实现
/**
* 无 限 层 级 树
* @author F-QiXian
* @date 2022/6/14 15:20
*/
@Data
@NoArgsConstructor
public class InfiniteMessageTree {
/**
* 初始化无限层级树
* @param list 消息列表
*/
public InfiniteMessageTree(List<MessageTreeNode> list){
this.init(list);
}
/**
* 消息树: 使用 CopyOnWriteArrayList 避免并发数据更新出现线程安全问题
*/
private List<MessageTreeNode> listTree = new CopyOnWriteArrayList<>();
/**
* 节点映射:通过映射对节点增删操作的时间复杂度为O(1), 使用 ConcurrentHashMap 避免并发数据更新出现线程安全问题
*/
private Map<String, List<MessageTreeNode>> nodeMapping = new ConcurrentHashMap<>();
/**
* 初始化无限层级树
* @param list 列表数据
* @return InfiniteMessageTree
*/
public void init(List<MessageTreeNode> list) {
// 通过父节点ID排序,使用降序插入的方式避免tree节点翻转
list.stream().sorted(Comparator.comparingLong(MessageTreeNode::getParentId)).forEach(this::findParentAdd);
}
/**
* 根据父级ID寻找节点子集插入
* @param node 目标节点
* @return 添加成功或失败
*/
public boolean findParentAdd(MessageTreeNode node) {
// 父级ID的 Key
String key = node.getParentId().toString();
// 设置子集映射
nodeMapping.put(node.getId().toString(), node.getChildren());
// 通过缓存插入记录或直接插入为头节点
return (nodeMapping.containsKey(key)) ? nodeMapping.get(key).add(node) : listTree.add(node);
}
/**
* 树节点
* @author F-QiXian
* @date 2022/6/14 15:15
*/
@Data
public static class MessageTreeNode {
/**
* 节点唯一ID
*/
private Long id;
/**
* 父节点ID
*/
private Long parentId;
/**
* 节点内容
*/
private String content;
/**
* 子节点
*/
private List<MessageTreeNode> children = new CopyOnWriteArrayList<>();
}
}
一些思考
这个消息树可以注册为一个单例的bean,系统在初始化的时候进行构建,当用户发生了更新行为直接进行异步API调用(因为是单例的bean所以使用 CopyOnWriteArrayList 和 ConcurrentHashMap 来保证并发修改的线程安全问题),当用户发起请求留言,此时需要进行数据持久化并返回主键的id,前端需要进行树内容的更新,因为可能存在其他用户修改留言信息,所以前端页面不能直接更新jsongArray data , 需要再次刷新;