巧妙利用数据结构优化部门查询

目录

一、出现的问题

部门树接口超时

二、问题分析

源代码分析

三、解决方案

具体实现思路

四、优化的效果


一、出现的问题

部门树接口超时

无论是在A项目还是在B项目中,都存在类似的页面,其实就是一个部门列表或者叫组织列表。

从页面的展示形式上来看它是一个树状结构。因此在最早的接口设计上也是通过一个接口返回该租户的部门页面进行显示。

接口的返回形式如上。其实也很简单。就是一个通过一个嵌套的对象实现了树

但是在我们私有化的过程中有一些大型的集团客户,他们整个集团的部门数量上万个甚至可能出现 10w 多个部门。因此出现了接口超时20s 都没有结果的情况出现了。

数据权限查询子部门超时

其实,不光有这个场景,在我们的数据权限设计上有一个,这个意思呢就是。如果用户的数据权限为本级及子级的,那这个用户就能查看用户所属部门和该部门子部门下产生的数据。按照目前的实现方式,那我们就要知道这个用户的部门和子部门。

那这个查询子部门的过程也会同样出现超时的情况。

二、问题分析

源代码分析

走读原先的部门树接口逻辑发现,是通过递归查询的方式实现。

在数据表的设计上,是通过一个 parent_id 字段关联的是父部门的 id。因此可以通过该字段查询到这个部门的子部门的列表。然后通过递归不断地遍历就能查询出所有的部门并且组装成树。

伪代码逻辑其实就是

// 递归查询函数,参数为要查找的父部门id
List<Department> findDepartmentsByParentId(int parentId) {
    List<Department> departmentList = queryFromMysql(parentId);
  // 用于存放查询结果的列表
    List<Department> result = new List<Department>();  
    for (Department department : departmentList) {
        if (department.parentId == parentId) {
            result.add(department);
            // 递归调用,查找当前部门下的子部门,并将结果合并
            List<Department> subDepartments = findDepartmentsByParentId(department.id);
            result.addAll(subDepartments);
        }
    }

    return result;

 可能出现问题的原因

  1. 子部门数量太多。导致queryFromMysql 这个方法执行的次数多
  2. 部门层级深。导致queryFromMysql 这个方法执行的次数多

核心原因其实还是每一次查询子部门都需要通过queryFromMysql这个方法执行一次,类似如下的

select * from department where tenant_id=xxx and parent_id=xxx;

不难发现,问题的根本原因就是 sql 查询执行次数太多了。那么现在问题的核心就是减少 sql 查询的次数。就能解决问题。

当然这里没有考虑改交互的方式,比如一次只查询一级,慢慢展开呢,其实一样,数据权限的子部门那里保持原样还是会超时。

另外,细心的同学可能发现这个代码有问题呀,为什么要从 root 一层一层地查下去,直接用租户 id 查询出所有的部门数据然后再组装成树不是会解决吗。确实可以解决部门树的查询,但是数据权限的子部门那里保持原样还是会超时。

因此要解决的问题核心还是传入任何一个部门 id 能够快速查询出子部门列表

三、解决方案

  1. 加缓存,可能存的数据比较多吧,每一个部门 id 都需要存一份子部门的数据。初始化构建缓存的时候查询依旧慢,这里不讨论了。因为核心问题是解决查询数据库慢的问题
  2. 减少查询数据库的次数,从而减少响应时间

具体实现思路

回顾标题,要用数据结构去优化查询,那可能就是在原先表的基础增加一些辅助字段来提高查询的效率。加索引肯定没啥大作用了,因为这里的查询慢是因为次数多而不是因为单词查询数据量大导致。当然索引该加还是得加。

 

我们可以添加为每个节点添加两个值,代表数据的范围(leftValut, rightValue)

需要满足以下要求:

    1. 每个节点的 leftValut < rightValue
    2. 子节点的 leftValut > 父节点的 leftValue
    3. 子节点的 rightValue < 父节点的 rightValue

我们按照,这个规则给上面的结构赋值下:

再观察下,怎么去查子部门呢?

比如  2-1(2,9) 的 子部门为  3-1(3,6) 和  3-2(7,8)以及 3-1 的子部门 4-1(4,5)。 可以看出 3,4,5,6,7,8 都在 2,9 之间。

因此可以用,2< leftValue/rightValue <9  查询到 3-1,3-2,4-1 .而这几个正好是 2-1 的子部门。

根据我们赋值的限制条件不难看出一个部门的所有子部门的 leftValue 和 rightValue 是在父部门的范围内的。所以我们在查询一个部门的所有子部门时,就可以用如下的伪代码去查询

//1. 查询该部门的 leftValue 和 rightValue
Dept dept = queryDeptFromMysql(parentId);
int leftValue = dept.getLeftValue();
int rightValue = dept.getRightValue();

//2. 通过leftValue 和 rightValue 查询子部门
List<Dept> childDepts = queryChildDeptsFromMysql(leftValue,rightValue)

//sql 的话就是这样
// select * from dept where tenant_id=xxx and left_value > #{leftValue} and left_value < #{rightValue}
  
//3. 根据实际情况返回列表,或者组装成树

在我们有这两个左右值的情况下,一条 sql 就能查询出所有符合条件的结果。那么现在的问题来到了如何去为每个部门节点进行赋值。

如何对部门进行赋值

可以分为以下几种情况讨论:(这里只考虑增加修改的情况都是单部门)

​​​​​​​初始化   初始化是最简单的,我们只需要以根节点(leftValue 值为 1)为开始深度优先遍历, 每次+1 即可。

这里的初始值设置为 1 即可。很简单

​​​​​​​新增部门

 

可以看出变化的部分是

    1. 新增了一个部门 4-2 他的值分别是 4-1 的 (rightValue +1,rightValue+2) 即 6,7
    2. 因为 3-1 增加了子部门所以他的值范围必定发生变化,变化的增长值就是 2 ( 也是增加的节点个数*2)因为部门 4-2 已经是 6,7 了 所以 3-1 部门会发生变化为 3,8
    3. 同理后续右边(比发生变化的值大的)节点的值都会发生对应的变化

其实不难看出,每个值大于等于 4-1 的rightValue +1,(即新增加的4-2的 leftValue) 的值都增加了 2 ,无论他是leftValue还是rightValue。

那么新增加一个子节点可以这样去更新。当然你还要把新加的部门加进去

  UPDATE deptSET left_value = IF(left_value > #{leftValue}, left_value + #{changeValue}, left_value),right_value = IF(right_value > #{leftValue}, right_value +  #{changeValue}, right_value)where tenant_id= #{tenantId};

 

changeValue 是什么呢。其实就是变化的节点的个数*2,而这里就是 2,因为只新增了一个子节点 4-2

那如果,新增加的部门不是一个,而是多个呢?

其实这种情况在我们这种页面上不会出现。因为添加部门的时候只会添加一个。

那删除和修改就是一个道理了~

就不多演示了

​​​​​​​感兴趣的可以评论区讨论~

四、优化的效果

部门树(约有 1.8w 个部门)的接口返回了如此巨大数据的情况的下只用了 400ms。其中还对部门重新进行了排序,并且带有其他逻辑。效果堪称显著。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/894573.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT简单实现验证码(字符)

0&#xff09; 运行结果 1&#xff09; 生成随机字符串 Qt主要通过QRandomGenerator类来生成随机数。在此之前的版本中&#xff0c;qrand()函数也常被使用&#xff0c;但从Qt 5.10起&#xff0c;推荐使用更现代化的QRandomGenerator类。 在头文件添加void generateRandomNumb…

JavaFX - 3D 形状

在前面的章节中&#xff0c;我们已经了解了如何在 JavaFX 应用程序中的 XY 平面上绘制 2D 形状。除了这些 2D 形状之外&#xff0c;我们还可以使用 JavaFX 绘制其他几个 3D 形状。 通常&#xff0c;3D 形状是可以在 XYZ 平面上绘制的几何图形。它们由两个或多个维度定义&#…

深入理解开放寻址法中的三种探测序列

一、引言 开放寻址法是解决散列表中冲突的一种重要方法&#xff0c;当发生冲突&#xff08;即两个不同的键通过散列函数计算得到相同的散列值&#xff09;时&#xff0c;它会在散列表中寻找下一个可用的存储位置。而探测序列就是用于确定在发生冲突后&#xff0c;依次尝试哪些…

【双指针题目】

双指针 美丽区间&#xff08;滑动窗口&#xff09;合并数列&#xff08;双指针的应用&#xff09;等腰三角形全部所有的子序列 美丽区间&#xff08;滑动窗口&#xff09; 美丽区间 滑动窗口模板&#xff1a; int left 0, right 0;while (right < nums.size()) {// 增大…

嵌入式八股文面试题(一)C语言部分

1. 变量/函数的声明和定义的区别&#xff1f; &#xff08;1&#xff09;变量 定义不仅告知编译器变量的类型和名字&#xff0c;还会分配内存空间。 int x 10; // 定义并初始化x int x; //同样是定义 声明只是告诉编译器变量的名字和类型&#xff0c;但并不为它分配内存空间…

go-zero学习笔记(三)

利用goctl生成rpc服务 编写proto文件 // 声明 proto 使用的语法版本 syntax "proto3";// proto 包名 package demoRpc;// golang 包名(可选) option go_package "./demo";// 如需为 .proto 文件添加注释&#xff0c;请使用 C/C 样式的 // 和 /* ... */…

【25考研】南开软件考研复试复习重点!

一、复试内容 复试采取现场复试的方式。复试分为笔试、机试和面试三部分。三部分合计100分&#xff0c;其中笔试成绩占30%、机试成绩占30%、面试成绩占40%。 1.笔试&#xff1a;专业综合基础测试 考核方式&#xff1a;闭卷考试&#xff0c;时长为90分钟。 笔试考查内容范围…

【最长上升子序列Ⅱ——树状数组,二分+DP,纯DP】

题目 代码&#xff08;只给出树状数组的&#xff09; #include <bits/stdc.h> using namespace std; const int N 1e510; int n, m; int a[N], b[N], f[N], tr[N]; //f[i]表示以a[i]为尾的LIS的最大长度 void init() {sort(b1, bn1);m unique(b1, bn1) - b - 1;for(in…

012-51单片机CLD1602显示万年历+闹钟+农历+整点报时

1. 硬件设计 硬件是我自己设计的一个通用的51单片机开发平台&#xff0c;可以根据需要自行焊接模块&#xff0c;这是用立创EDA画的一个双层PCB板&#xff0c;所以模块都是插针式&#xff0c;不是表贴的。电路原理图在文末的链接里&#xff0c;PCB图暂时不选择开源。 B站上传的…

对象的实例化、内存布局与访问定位

一、创建对象的方式 二、创建对象的步骤: 一、判断对象对应的类是否加载、链接、初始化: 虚拟机遇到一条new指令&#xff0c;首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用&#xff0c;并且检查这个符号引用代表的类是否已经被加载、解析和初始化…

传输层协议 UDP 与 TCP

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;Linux 目录 一&#xff1a;&#x1f525; 前置复盘&#x1f98b; 传输层&#x1f98b; 再谈端口号&#x1f98b; 端口号范围划分&#x1f98b; 认识知名端口号 (Well-Know Port Number) 二&#xf…

实验十一 Servlet(二)

实验十一 Servlet(二) 【实验目的】 1&#xff0e;了解Servlet运行原理 2&#xff0e;掌握Servlet实现方式 【实验内容】 改造实验10&#xff0c;引入数据库&#xff0c;创建用户表&#xff0c;包括用户名和密码&#xff1a;客户端通过login.jsp发出登录请求&#xff0c;请求…

服务SDK三方新版中央仓库和私服发布详解

预备信息Github仓库发布Gradle版本匹配Gradle项目构建全局变量定义Gradle项目Nexus仓库配置与发布过程Gradle项目发布至Sonatype中央仓库配置过程总结当我们在实现一个项目技术总结、工具类封装或SDK封装,通常是为了方便开发者使用特定服务或平台而提供的一组工具和API。您可能…

openmv的端口被拆分为两个 导致电脑无法访问openmv文件系统解决办法 openmv USB功能改动 openmv驱动被更改如何修复

我之前误打误撞遇到一次&#xff0c;直接把openmv的全部端口删除卸载然后重新插上就会自动重新装上一个openmv端口修复成功&#xff0c;大家可以先试试不行再用下面的方法 全部卸载再重新插拔openmv 要解决OpenMV IDE中出现的两个端口问题&#xff0c;可以尝试以下步骤&#x…

LabVIEW双光子成像系统:自主创新,精准成像,赋能科研

双光子成像系统&#xff1a;自主创新&#xff0c;精准成像&#xff0c;赋能科研 第一部分&#xff1a;概述 双光子成像利用两个低能量光子同时激发荧光分子&#xff0c;具有深层穿透、高分辨率、低光损伤等优势。它能实现活体深层组织的成像&#xff0c;支持实时动态观察&…

Deepseek-R1 和 OpenAI o1 这样的推理模型普遍存在“思考不足”的问题

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Vue3学习笔记-Vue开发前准备-1

一、安装15.0或更高版本的Node.js node -v npm -v 二、创建Vue项目 npm init vuelatest 三、Vue项目结构 node_modules: Vue项目运行的依赖文件public&#xff1a;资源文件夹package.json&#xff1a;信息描述文件

数据结构:时间复杂度

文章目录 为什么需要时间复杂度分析&#xff1f;一、大O表示法&#xff1a;复杂度的语言1.1 什么是大O&#xff1f;1.2 常见复杂度速查表 二、实战分析&#xff1a;解剖C语言代码2.1 循环结构的三重境界单层循环&#xff1a;线性时间双重循环&#xff1a;平方时间动态边界循环&…

S4 HANA明确税金汇差科目(OBYY)

本文主要介绍在S4 HANA OP中明确税金汇差科目(OBYY)相关设置。具体请参照如下内容&#xff1a; 1. 明确税金汇差科目(OBYY) 以上配置点定义了在外币挂账时&#xff0c;当凭证抬头汇率和税金行项目汇率不一致时&#xff0c;造成的差异金额进入哪个科目。此类情况只发生在FB60/F…

87.(3)攻防世界 web simple_php

之前做过&#xff0c;回顾 12&#xff0c;攻防世界simple_php-CSDN博客 进入靶场 <?php // 显示当前 PHP 文件的源代码&#xff0c;方便调试或查看代码结构 // __FILE__ 是 PHP 的一个魔术常量&#xff0c;代表当前文件的完整路径和文件名 show_source(__FILE__);// 包含…