From 5545322a6e5d7504fc7dec563c56e2e3fe68fdfc Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Wed, 2 Apr 2025 18:28:46 +0800 Subject: [PATCH] =?UTF-8?q?Commit=20on=202025/04/02=20=E5=91=A8=E4=B8=89?= =?UTF-8?q?=2018:28:46.48?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 科研/数学基础.md | 153 +++-- 科研/草稿.md | 100 ++-- 科研/郭款论文.md | 117 +++- 科研/颜佳佳论文.md | 20 + 自学/JAVA面试题.md | 92 +++ 自学/JavaWeb——后端.md | 311 ++++++---- 自学/Mysql数据库.md | 197 +++++-- 自学/力扣Hot 100题.md | 110 +++- 自学/苍穹外卖.md | 2 + 自学/草稿.md | 1248 +---------------------------------------- 10 files changed, 821 insertions(+), 1529 deletions(-) create mode 100644 自学/JAVA面试题.md diff --git a/科研/数学基础.md b/科研/数学基础.md index 5b2cfad..0649b51 100644 --- a/科研/数学基础.md +++ b/科研/数学基础.md @@ -506,11 +506,44 @@ $$ \text{Cov}(aX, bY) = ab \cdot \text{Cov}(X, Y) $$ +$\text{cov}(AX, AX) = A\text{cov}(X, X)A^T$ + +**推导:** + +(1) 展开协方差定义 +$$ +\text{cov}(AX, AX) = \mathbb{E}[(AX - \mathbb{E}[AX])(AX - \mathbb{E}[AX])^T] +$$ + +(2) 线性期望性质 +$$ +\mathbb{E}[AX] = A\mathbb{E}[X] \\ +\Rightarrow AX - \mathbb{E}[AX] = A(X - \mathbb{E}[X]) +$$ + +(3) 代入展开式 +$$ += \mathbb{E}[A(X - \mathbb{E}[X])(A(X - \mathbb{E}[X]))^T] \\ += \mathbb{E}[A(X - \mathbb{E}[X])(X - \mathbb{E}[X])^T A^T] +$$ + +(4) 提取常数矩阵 +$$ += A \mathbb{E}[(X - \mathbb{E}[X])(X - \mathbb{E}[X])^T] A^T +$$ + +(5) 协方差矩阵表示 +$$ += A \text{cov}(X, X) A^T +$$ + ### **协方差矩阵** -对于一个随机向量 $\mathbf{X} = [X_1, X_2, \dots, X_n]^T$,其中 $X_1, X_2, \dots, X_n$ 是 $n$ 个随机变量,协方差矩阵 $\Sigma$ 是一个 $n \times n$ 的矩阵,其**元素**表示不同随机变量之间的**协方差**。 +对于一个随机向量 $\mathbf{X} = [X_1, X_2, \dots, X_n]^T$,其中 $X_1, X_2, \dots, X_n$ 是 $n$ 个随机变量,协方差矩阵 $\Sigma$ 是一个 $n \times n$ 的矩阵,其**元素**表示不同**随机变量**之间的**协方差**。 + +(注意:每对变量指的是$\mathbf{X}$中任意两个分量之间的组合,如$X_1, X_2$) 协方差矩阵的元素是通过计算每对随机变量之间的协方差来获得的。协方差矩阵 $\Sigma$ 的元素可以表示为: @@ -525,53 +558,90 @@ $$ 其中: -- 对角线上的元素 $\text{Cov}(X_i, X_i)$ 是每个变量的方差,即 $\text{Var}(X_i)$, +- 对角线上的元素 $\text{Cov}(X_i, X_i)$ 是每个变量的方差,即 $\text{Var}(X_i)$。 - 非对角线上的元素 $\text{Cov}(X_i, X_j)$ 是变量 $X_i$ 和 $X_j$ 之间的协方差。 -**举例** +**计算举例** -假设我们有两个随机变量 $X$ 和 $Y$,它们的样本数据如下: - -- $X = [4, 7, 8, 5, 6]$ -- $Y = [2, 3, 6, 7, 4]$ - -我们首先计算每个变量的均值、方差和协方差,然后将这些信息组织成协方差矩阵。 - -步骤1:计算均值 - -- $\mu_X = \frac{4 + 7 + 8 + 5 + 6}{5} = 6$ -- $\mu_Y = \frac{2 + 3 + 6 + 7 + 4}{5} = 4.4$ - -步骤2:计算方差 - -- $\text{Var}(X) = \frac{1}{5} \left[(4-6)^2 + (7-6)^2 + (8-6)^2 + (5-6)^2 + (6-6)^2\right] = 2$ -- $\text{Var}(Y) = \frac{1}{5} \left[(2-4.4)^2 + (3-4.4)^2 + (6-4.4)^2 + (7-4.4)^2 + (4-4.4)^2\right] = 2.64$ - -步骤3:计算协方差 -$$ -\text{Cov}(X, Y) = \frac{1}{5} \left[(4-6)(2-4.4) + (7-6)(3-4.4) + (8-6)(6-4.4) + (5-6)(7-4.4) + (6-6)(4-4.4)\right]= 4 -$$ - -步骤4:构建协方差矩阵 - -根据以上结果,协方差矩阵 $\Sigma$ 是: +假设我们有 **3 个特征**($n=3$)和 **4 个样本**($m=4$),则数据矩阵 $X$ 的构造如下: $$ -\Sigma = \begin{bmatrix} -\text{Var}(X) & \text{Cov}(X, Y) \\ -\text{Cov}(X, Y) & \text{Var}(Y) -\end{bmatrix} -= \begin{bmatrix} -2 & 4 \\ -4 & 2.64 +X = +\begin{bmatrix} +x_1^{(1)} & x_1^{(2)} & x_1^{(3)} & x_1^{(4)} \\ +x_2^{(1)} & x_2^{(2)} & x_2^{(3)} & x_2^{(4)} \\ +x_3^{(1)} & x_3^{(2)} & x_3^{(3)} & x_3^{(4)} \end{bmatrix} $$ +假设特征为: + +- 第1行 $x_1$:身高(cm) +- 第2行 $x_2$:体重(kg) +- 第3行 $x_3$:年龄(岁) + +对应4个样本(人)的数据: +$$ +X = +\begin{bmatrix} +170 & 165 & 180 & 155 \\ +65 & 55 & 75 & 50 \\ +30 & 25 & 40 & 20 +\end{bmatrix} +$$ + +1. **中心化数据**(每行减去均值): + + - 计算每行均值: + $$ + \mu_1 = \frac{170+165+180+155}{4} = 167.5, \quad \mu_2 = 61.25, \quad \mu_3 = 28.75 + $$ + + - 中心化后的矩阵 $X_c$: + $$ + X_c = + \begin{bmatrix} + 2.5 & -2.5 & 12.5 & -12.5 \\ + 3.75 & -6.25 & 13.75 & -11.25 \\ + 1.25 & -3.75 & 11.25 & -8.75 + \end{bmatrix} + $$ + +2. **计算协方差矩阵**: + $$ + \text{Cov} = \frac{1}{m} X_c X_c^T = \frac{1}{4} + \begin{bmatrix} + 2.5 & -2.5 & 12.5 & -12.5 \\ + 3.75 & -6.25 & 13.75 & -11.25 \\ + 1.25 & -3.75 & 11.25 & -8.75 + \end{bmatrix} + \begin{bmatrix} + 2.5 & 3.75 & 1.25 \\ + -2.5 & -6.25 & -3.75 \\ + 12.5 & 13.75 & 11.25 \\ + -12.5 & -11.25 & -8.75 + \end{bmatrix} + $$ + 最终结果(对称矩阵): + $$ + \text{Cov} \approx + \begin{bmatrix} + 93.75 & 100.31 & 62.50 \\ + 100.31 & 120.31 & 75.00 \\ + 62.50 & 75.00 & 48.44 + \end{bmatrix} + $$ + + - 对角线是各特征的方差(如身高的方差为93.75) + - 非对角线是协方差(如身高与体重的协方差为100.31) -**如何生成均值为0,协方差为Q的噪声?** + + + +### **如何生成均值为0,协方差为Q的噪声?** 1. 生成标准正态随机变量 $$ @@ -615,14 +685,6 @@ w = L @ Z # 等价于 np.dot(L, Z) -$\text{cov}(AX, AX) = A\text{cov}(X, X)A^T$ - -**推导:** - -![400000](https://pic.bitday.top/i/2025/03/19/u8etaq-2.png) - - - ## 高斯分布 高斯分布的概率密度函数: @@ -1153,11 +1215,12 @@ $$ -## **谱分解** +## **谱分解**与网络重构 一个对称矩阵可以通过其特征值和特征向量进行分解。对于一个 $n \times n$ 的对称矩阵 $A$,其谱分解可以表示为: $$ +A = Q \Lambda Q^T \\ A = \sum_{i=1}^{n} \lambda_i x_i x_i^T $$ diff --git a/科研/草稿.md b/科研/草稿.md index 86ba03a..91a5792 100644 --- a/科研/草稿.md +++ b/科研/草稿.md @@ -1,81 +1,47 @@ -### 协方差矩阵(Covariance Matrix) +- # FCM算法时间复杂度分析 -**协方差矩阵** 是一个方阵,用来描述一组随机变量之间的协方差关系。它是多元统计分析中的重要工具,尤其在处理多维数据时,协方差矩阵提供了所有变量之间的协方差信息。 + ## 1. FCM算法的步骤与时间复杂度 -对于一个随机向量 $\mathbf{X} = [X_1, X_2, \dots, X_n]^T$,其中 $X_1, X_2, \dots, X_n$ 是 $n$ 个随机变量,协方差矩阵 $\Sigma$ 是一个 $n \times n$ 的矩阵,其元素表示不同随机变量之间的协方差。 + ### **1. 初始化步骤** + - **初始化簇中心**:需要 $O(K)$ 时间($K$ 是簇数) + - **初始化隶属度矩阵 $U$**($n \times K$ 矩阵):$O(nK)$ -### 协方差矩阵的定义 + ### **2. 更新隶属度** + - 每个数据点 $a_{ij}$ 计算与每个簇中心 $c_k$ 的距离:$O(K)$ + - 更新所有数据点的隶属度矩阵:$O(nK^2)$ -协方差矩阵的元素是通过计算每对随机变量之间的协方差来获得的。协方差矩阵 $\Sigma$ 的元素可以表示为: + ### **3. 更新簇中心** + - 计算每个簇的新中心(加权平均):$O(nK)$ + - 总体簇中心更新:$O(nK)$ -$$ -\Sigma = \begin{bmatrix} -\text{Cov}(X_1, X_1) & \text{Cov}(X_1, X_2) & \dots & \text{Cov}(X_1, X_n) \\ -\text{Cov}(X_2, X_1) & \text{Cov}(X_2, X_2) & \dots & \text{Cov}(X_2, X_n) \\ -\vdots & \vdots & \ddots & \vdots \\ -\text{Cov}(X_n, X_1) & \text{Cov}(X_n, X_2) & \dots & \text{Cov}(X_n, X_n) \\ -\end{bmatrix} -$$ + ### **4. 判断收敛** + - 计算簇中心变化量 $\Delta C$:$O(K)$ -其中: + ### **5. 量化处理** + - 将元素分配到簇并替换值:$O(nK)$ -- 对角线上的元素 $\text{Cov}(X_i, X_i)$ 是每个变量的方差,即 $\text{Var}(X_i)$, -- 非对角线上的元素 $\text{Cov}(X_i, X_j)$ 是变量 $X_i$ 和 $X_j$ 之间的协方差。 + ## 2. 总时间复杂度 -### 协方差矩阵和协方差的关系 + 每次迭代的总时间复杂度: + $$ + O(nK^2) + O(nK) + O(K) \approx O(nK^2) + $$ -协方差矩阵是多维协方差的扩展。对于一个二维随机变量 $\mathbf{X} = [X_1, X_2]^T$,它的协方差矩阵就是一个 $2 \times 2$ 的矩阵: + 其中: + - $n$:数据点数量 + - $K$:簇数量 -$$ -\Sigma = \begin{bmatrix} -\text{Var}(X_1) & \text{Cov}(X_1, X_2) \\ -\text{Cov}(X_1, X_2) & \text{Var}(X_2) -\end{bmatrix} -$$ + ## 3. 初始簇中心选取优化方法的时间复杂度 -所以,协方差矩阵包含了每一对变量之间的协方差信息。如果你有多个随机变量,协方差矩阵将为你提供这些变量之间的所有协方差。 + - 选择 $K$ 个簇中心时: + - 需要计算候选集内元素间最小距离 + - 每次选择复杂度:$O(n^2)$ + - 总体选择复杂度:$O(Kn^2)$ -### 举个例子 + ## 4. 图片分析结论 -假设我们有两个随机变量 $X$ 和 $Y$,它们的样本数据如下: + 图片中的时间复杂度分析是合理的: + - **标准FCM算法**:$O(nK^2)$ + - **优化簇中心选择**:$O(n^3)$ -- $X = [4, 7, 8, 5, 6]$ -- $Y = [2, 3, 6, 7, 4]$ - -我们首先计算每个变量的均值、方差和协方差,然后将这些信息组织成协方差矩阵。 - -步骤1:计算均值 - -- $\mu_X = \frac{4 + 7 + 8 + 5 + 6}{5} = 6$ -- $\mu_Y = \frac{2 + 3 + 6 + 7 + 4}{5} = 4.4$ - -步骤2:计算方差 - -- $\text{Var}(X) = \frac{1}{5} \left[(4-6)^2 + (7-6)^2 + (8-6)^2 + (5-6)^2 + (6-6)^2\right] = 2$ -- $\text{Var}(Y) = \frac{1}{5} \left[(2-4.4)^2 + (3-4.4)^2 + (6-4.4)^2 + (7-4.4)^2 + (4-4.4)^2\right] = 2.64$ - -步骤3:计算协方差 -$$ -\text{Cov}(X, Y) = \frac{1}{5} \left[(4-6)(2-4.4) + (7-6)(3-4.4) + (8-6)(6-4.4) + (5-6)(7-4.4) + (6-6)(4-4.4)\right] -$$ - -我们已经在前面计算过,协方差 $\text{Cov}(X, Y) = 4$。 - -步骤4:构建协方差矩阵 - -根据以上结果,协方差矩阵 $\Sigma$ 是: - -$$ -\Sigma = \begin{bmatrix} -\text{Var}(X) & \text{Cov}(X, Y) \\ -\text{Cov}(X, Y) & \text{Var}(Y) -\end{bmatrix} -= \begin{bmatrix} -2 & 4 \\ -4 & 2.64 -\end{bmatrix} -$$ - -### 总结 - -协方差矩阵是用于描述多个随机变量之间协方差关系的矩阵,它是协方差的自然扩展。当你有多个变量时,协方差矩阵包含了所有变量之间的协方差及每个变量的方差信息。在二维情况中,协方差矩阵是一个 $2 \times 2$ 矩阵,在多维情况下,它是一个 $n \times n$ 矩阵,其中 $n$ 是变量的个数。 + 该分析准确反映了FCM算法的计算复杂度。 diff --git a/科研/郭款论文.md b/科研/郭款论文.md index 3e5c3aa..6546031 100644 --- a/科研/郭款论文.md +++ b/科研/郭款论文.md @@ -1,3 +1,5 @@ +KAN不稳定/卡尔曼滤波 小波变换 + ## 郭款论文 ### **整体逻辑** @@ -28,12 +30,6 @@ - - -# KAN不稳定/卡尔曼滤波 小波变换 - - - ### 矢量量化 矢量量化的基本思想是将输入数据点视为多维向量,并将其映射到一个码本(codebook)中的最接近的码字。码本是预先确定的一组离散的向量,通常通过无监督学习方法(如**K-means**)从大量训练数据中得到。在矢量量化中,输入数据点与码本中的码字之间的距离度量通常使用**欧氏距离**。通过选择最接近的码字作为量化结果,可以用较少的码字表示输入数据,从而实现数据的压缩。同一个码字能够代表多个相似的多维向量,从而实现了**多对一的映射**。 @@ -416,14 +412,65 @@ $$ +#### FCM算法时间复杂度分析 + +网络节点数目为 $n$(不是矩阵的维度!),聚类簇数$K$ + +**初始化步骤** + +- 初始化簇中心和隶属度矩阵 $U$:需要 $O(nK)$ + +**更新隶属度** + +- 每个数据点 $a_{ij}$ 计算与每个簇中心 $c_k$ 的距离:$O(K)$ +- 更新所有数据点的隶属度矩阵:$O(nK^2)$ + +**更新簇中心** + +- 计算每个簇的新中心(加权平均):$O(nK)$ +- 总体簇中心更新:$O(nK)$ + +**判断收敛** + +计算簇中心变化量 $\Delta C$:$O(K)$ + +**量化处理** + +将元素分配到簇并替换值:$O(nK)$ + +**每次迭代的总时间复杂度:** +$$ +O(nK^2) + O(nK) + O(K) \approx O(nK^2) +$$ + +其中: + +- $n$:数据点数量 +- $K$:簇数量 + + + +**如果初始簇中心选取优化方法** + +- 选择 $K$ 个簇中心时: + - 需要计算候选集内元素间最小距离 + - 每次选择复杂度:$O(n^2)$ +- 总体选择复杂度:$O(Kn^2)$ + +整个FCM时间复杂度:$O(Kn^2)+O(TnK^2)$,$T$为迭代次数 + + + ### 网络重构 -对称非负矩阵分解(SNMF)来构造网络的低维嵌入表示,从而实现对高维网络邻接矩阵的精确重构。 +对称非负矩阵分解(SNMF)来构造网络的**低维嵌入**表示,从而实现对高维网络邻接矩阵的精确重构。 + +低维指的是分解后的$U$矩阵维度小->秩小->重构后的矩阵秩也小。 $$ A \approx U U^T. $$ -只需计算 $U U^T$ 就能重构出 $A$ 的低秩近似版本。如果选择保留全部特征(即 $r = n$),则可以精确还原 $A$;如果只取部分($r < n$),那么重构结果就是 $A$ 的低秩近似。 +只需计算 $U U^T$ 就能重构出 $A$ 的**低秩**近似版本。如果选择保留全部特征(即 $r = n$),则可以精确还原 $A$;如果只取部分($r < n$),那么重构结果就是 $A$ 的低秩近似。 #### 节点移动模型 @@ -487,7 +534,7 @@ $$ **1. 初始步骤:构造 $B$ 和 $Q$** -首先对 $A$ 做谱分解,得到(已知结果): +首先*对 $A$ 做谱分解*,*或者卡尔曼滤波预测*得到(已知结果): - 特征值:$\lambda_1=4, \lambda_2=2$ @@ -583,13 +630,63 @@ A \approx U U^T. $$ 此时 $U$(尺寸为 $2 \times 2$ 的矩阵)就是对 $A$ 进行低维嵌入得到的“特征表示”,它不仅满足非负性,而且通过内积 $U U^T$ 能近似重构出原矩阵 $A$。 + + +#### **时间复杂度分析** + +(1) 初始构造阶段(假设特征值 特征向量已提前获取,不做分析) + +**构造矩阵** $B = X\Lambda^{1/2}$ + +$X$ 是 $n \times r$,$\Lambda^{1/2}$ 是 $r \times r$ 对角矩阵。 + +$$\mathcal{O}(nr^2) \quad (r \ll n)$$ + + + +(2) 迭代过程 + +**计算 $U = \max(0, B Q)$** + +$B$ 是 $n \times r$,$Q$ 是 $r \times r$。 + +$\mathcal{O}(n r^2)$ + +**计算 $F = U^T B$** + +$U^T$ 是 $r \times n$,$B$ 是 $n \times r$。 + +$\mathcal{O}(n r^2)$ + +**对 $F$ 进行 SVD** + +$F$ 是 $r \times r$ 的矩阵。 + +SVD 的时间复杂度:$\mathcal{O}(r^3)$。 + +**更新 $Q = V H^T$** + +$V$ 和 $H$ 是 $r \times r$。 + +$\mathcal{O}(r^3)$ + + + +(3) 由于通常 $n \gg r$,$\mathcal{O}(n r^2)$ 是主导项,故总复杂度(其中 $T$ 为迭代次数) +$$ +T \cdot \mathcal{O}(nr^2) +$$ + + + + **两种求对称非负矩阵分解的方法** 1. **纯梯度下降** - 优点:实现原理简单,对任意大小/形状的矩阵都能做。 - 缺点:收敛速度有时较慢,且对初始值敏感。 -2. **rEVD + 旋转截断** +2. **rEVD + 旋转截断** (示例方法) - 优点:利用了特征分解,可以先一步把主要信息“压缩”进 $B$,后续只需解决“如何把负数修正掉”以及“如何微调逼近”。 - 缺点:需要先做特征分解,适合于对称矩阵或低秩场景。 diff --git a/科研/颜佳佳论文.md b/科研/颜佳佳论文.md index a20c335..5f1718f 100644 --- a/科研/颜佳佳论文.md +++ b/科研/颜佳佳论文.md @@ -1,2 +1,22 @@ ## 颜佳佳论文 +### 网络结构优化 + +**直接SNMF分解(无优化)** + +- **输入矩阵**:原始动态网络邻接矩阵 $A$(可能稠密或高秩) +- **处理流程**: + - 直接对称非负矩阵分解:$A \approx UU^T$ + - 通过迭代调整$U$和旋转矩阵$Q$逼近目标 +- **存在问题**: + - 高秩矩阵需要保留更多特征值($\kappa$较大) + - **非稀疏矩阵计算效率低** + +**先优化再SNMF(论文方法)** + +- **优化阶段**(ADMM): + - 目标函数:$\min_{A_{\text{opt}}} (1-\alpha)\|A_{\text{opt}}\|_* + \alpha\|A_{\text{opt}}\|_1$ + - 输出优化矩阵$A_{\text{opt}}$ +- **SNMF阶段**: + - 输入变为优化后的$A_{\text{opt}}$ + - 保持相同分解流程但效率更高 diff --git a/自学/JAVA面试题.md b/自学/JAVA面试题.md new file mode 100644 index 0000000..c4f508e --- /dev/null +++ b/自学/JAVA面试题.md @@ -0,0 +1,92 @@ +## JAVA面试题(1) + +### [说说 Java 中 HashMap 的原理?](https://www.mianshiya.com/bank/1860871861809897474/question/1834107117591187457) + +![image-20250402170337830](https://pic.bitday.top/i/2025/04/02/s66t0w-0.png) + +**为什么引入红黑树:** +当hash冲突较多的时候,链表中的元素会增多,插入、删除、查询的效率会变低,退化成O(n)使用红黑树可以优化插入、删除、查询的效率,logN级别。 + +转换时机: +链表上的元素个数大于等于8 且 数组长度大于等于64; +链表上的元素个数小于等于6的时候,红黑树退化成链表。 + + + +**链表插入方式变更:从"头插法"改为"尾插法"** + +- **头插法特点**: + - 插入时不需要遍历链表 + - 直接替换头结点 + - 扩容时会导致链表逆序 + - 多线程环境下可能产生**死循环** + +- **尾插法改进**: + - 避免扩容时的链表逆序 + - 解决多线程环境下的潜在死循环问题 + + + +### [Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?](https://www.mianshiya.com/bank/1860871861809897474/question/1780933294813114369) + +![image-20250402170530218](https://pic.bitday.top/i/2025/04/02/s7ahq2-0.png) + +![image-20250402170553033](https://pic.bitday.top/i/2025/04/02/s7f94s-0.png) + +#### ConcurrentHashMap 不同JDK版本的实现对比 + +1. **数据结构** + +- JDK1.7: + - 使用 `Segment(分段锁) + HashEntry数组 + 链表` 的数据结构 + +- JDK1.8及之后: + - 使用 `数组 + 链表/红黑树` 的数据结构(与HashMap类似) + +2. **锁的类型与宽度** + +- JDK1.7: + - 分段锁(Segment)继承了 `ReentrantLock` + - Segment容量默认16,不会扩容 → 默认支持16个线程并发访问 + +- JDK1.8: + - 使用 `synchronized + CAS` 保证线程安全 + - 空节点:通过CAS添加(**put操作**,多个线程可能同时想要将一个新的键值对插入到同一个桶中,这时它们会使用 CAS 来比较当前桶中的元素(或节点)是否已经被修改过。) + - 非空节点:通过synchronized加锁,**只锁住该桶**,其他桶可以并行访问。 + +3. **渐进式扩容(JDK1.8+)** + +- **触发条件**:元素数量 ≥ `数组容量 × 负载因子(默认0.75)` +- **扩容过程**: + 1. 创建2倍大小的新数组 + 2. 线程操作数据时,逐步迁移旧数组数据到新数组 + 3. 使用 `transferIndex` 标记迁移进度 + 4. 直到旧数组数据完全迁移完成 + + + +### [500. 什么是 Java 的 CAS(Compare-And-Swap)操作?](https://www.mianshiya.com/question/1780933295027023873) + +CAS操作包含三个基本操作数: + +1. **内存位置(V)**:要更新的变量 +2. **预期原值(A)**:认为变量当前应该具有的值 +3. **新值(B)**:想要更新为的值 + +CAS 工作原理: + +1. 读取内存位置V的当前值为A +2. 计算新值B +3. 当且仅当V的值等于A时,将V的值设置为B +4. 如果不等于A,则操作失败(通常重试) + +```text +伪代码表示: +if (V == A) { + V = B; + return true; +} else { + return false; +} +``` + diff --git a/自学/JavaWeb——后端.md b/自学/JavaWeb——后端.md index bcde7ad..4afa6d9 100644 --- a/自学/JavaWeb——后端.md +++ b/自学/JavaWeb——后端.md @@ -407,7 +407,7 @@ public class DeptController { ### 快速启动 -1. 新建spring initializr module +1. 新建**spring initializr** project 2. 删除以下文件 ![image-20240302142835694](https://pic.bitday.top/i/2025/03/19/u6qkax-2.png) @@ -1239,6 +1239,18 @@ public class SpringbootWebConfig2Application { 9. lombok的相关注解。非常实用的工具库。 + - 在pom.xml文件中引入依赖 + + ```xml + + + org.projectlombok + lombok + + ``` + + - 在实体类上添加以下注解(加粗为常用) + | **注解** | **作用** | | ----------------------- | ------------------------------------------------------------ | | @Getter/@Setter | 为所有的属性提供get/set方法 | @@ -1300,32 +1312,31 @@ public class SpringbootWebConfig2Application { ![image-20240307125505211](https://pic.bitday.top/i/2025/03/19/u6pfoj-2.png) -1. 创建springboot工程,并导入 mybatis的起步依赖、mysql的驱动包。创建用户表user,并创建对应的实体类User - -![image-20240307125820685](https://pic.bitday.top/i/2025/03/19/u6q96d-2.png) +1. 创建springboot工程(Spring Initializr),并导入 mybatis的起步依赖、mysql的驱动包。创建用户表user,并创建对应的实体类User + ![image-20240307125820685](https://pic.bitday.top/i/2025/03/19/u6q96d-2.png) 2. 在springboot项目中,可以编写main/resources/application.properties文件,配置数据库连接信息。 -```java -#驱动类名称 -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -#数据库连接的url -spring.datasource.url=jdbc:mysql://localhost:3306/mybatis -#连接数据库的用户名 -spring.datasource.username=root -#连接数据库的密码 -spring.datasource.password=1234 -``` + ``` + #驱动类名称 + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + #数据库连接的url + spring.datasource.url=jdbc:mysql://localhost:3306/mybatis + #连接数据库的用户名 + spring.datasource.username=root + #连接数据库的密码 + spring.datasource.password=1234 + ``` 3. 在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper -![image-20240307132356616](https://pic.bitday.top/i/2025/03/19/u6qtz4-2.png) + ![image-20240307132356616](https://pic.bitday.top/i/2025/03/19/u6qtz4-2.png) @Mapper注解:表示是mybatis中的Mapper接口 -- 程序运行时:框架会自动生成接口的**实现类对象(代理对象)**,并交给Spring的IOC容器管理 +​ -程序运行时:框架会自动生成接口的**实现类对象(代理对象)**,并交给Spring的IOC容器管理 - @Select注解:代表的就是select查询,用于书写select查询语句 +@Select注解:代表的就是select查询,用于书写select查询语句 ```java @Mapper @@ -1336,49 +1347,69 @@ public interface UserMapper { } ``` + + ### 数据库连接池 -数据库连接池是个容器,负责分配、管理数据库**连接(Connection)** +数据库连接池是一个容器,负责管理和分配数据库连接(`Connection`)。 -- 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象 +- 在程序启动时,连接池会创建一定数量的数据库连接。 +- 客户端在执行 SQL 时,从连接池获取连接对象,执行完 SQL 后,将连接归还给连接池,以供其他客户端复用。 +- 如果连接对象长时间空闲且超过预设的最大空闲时间,连接池会自动释放该连接。 -允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个 +**优势**:避免频繁创建和销毁连接,提高数据库访问效率。 -- 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用) -- 客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象 -### lombok +Druid(德鲁伊) -Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码。 +* Druid连接池是阿里巴巴开源的数据库连接池项目 -| **注解** | **作用** | -| ------------------- | ------------------------------------------------------------ | -| @Getter/@Setter | 为所有的属性提供get/set方法 | -| @ToString | 会给类自动生成易阅读的 toString 方法 | -| @EqualsAndHashCode | 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 | -| **@Data** | **提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)** | -| @NoArgsConstructor | 为实体类生成无参的构造器方法 | -| @AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 | +* 功能强大,性能优秀,是Java语言最好的数据库连接池之一 -**使用** +把默认的 Hikari 数据库连接池切换为 Druid 数据库连接池: + +1. 在pom.xml文件中引入依赖 + + ```xml + + + com.alibaba + druid-spring-boot-starter + 1.2.8 + + ``` + +2. 在application.properties中引入数据库连接配置 + + ```properties + spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver + spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis + spring.datasource.druid.username=root + spring.datasource.druid.password=1234 + ``` + + + +### SQL注入问题 + +SQL注入:由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。 + +在Mybatis中提供的参数占位符有两种:${...} 、#{...} + +- #{...} + - 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值 + - 使用时机:参数传递,都使用#{…} + +- ${...} + - 拼接SQL。直接将参数拼接在SQL语句中,**存在SQL注入问题** + - 使用时机:如果对表名、列表进行动态设置时使用 -```java -import lombok.Data; -@Data -public class User { - private Integer id; - private String name; - private Short age; - private Short gender; - private String phone; -} -``` ### 日志输出 -在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。 +只建议开发环境使用:在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果 1. 打开application.properties文件 @@ -1391,11 +1422,55 @@ mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl +### 驼峰命名法 + +在 Java 项目中,数据库表字段名一般使用 **下划线命名法**(snake_case),而 Java 中的变量名使用 **驼峰命名法**(camelCase)。 + +- [x] **小驼峰命名(lowerCamelCase)**: + +- 第一个单词的首字母小写,后续单词的首字母大写。 +- **例子**:`firstName`, `userName`, `myVariable` + +**大驼峰命名(UpperCamelCase)**: + +- 每个单词的首字母都大写,通常用于类名或类型名。 +- **例子**:`MyClass`, `EmployeeData`, `OrderDetails` + + + +表中查询的数据封装到实体类中 + +- 实体类属性名和数据库表查询返回的**字段名一致**,mybatis会自动封装。 +- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。 + +![image-20221212103124490](https://pic.bitday.top/i/2025/03/19/u6o894-2.png) + +解决方法: + +1. 起别名 +2. 结果映射 +3. **开启驼峰命名** +4. **属性名和表中字段名保持一致** + + + +**开启驼峰命名(推荐)**:如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射 + +> 驼峰命名规则: abc_xyz => abcXyz +> +> - 表中字段名:abc_xyz +> - 类中属性名:abcXyz + +```java +# 在application.properties中添加: +mybatis.configuration.map-underscore-to-camel-case=true +``` + + + ### 增删改 - **增删改通用!:返回值为int时,表示影响的记录数,一般不需要可以设置为void!** -- **#{} 表示占位符**,执行SQL时,生成预编译SQL,会自动设置参数值 -- ${} 也是占位符,但直接将参数拼接在SQL语句中,存在SQL注入问题 **作用于单个字段** @@ -1431,45 +1506,23 @@ public interface EmpMapper { } ``` -说明:#{...} 里面写的名称是对象的**属性名**!,函数内的参数是Emp对象 +在 **`@Insert`** 注解中使用 `#{}` 来引用 `Emp` 对象的属性,MyBatis 会自动从 `Emp` 对象中提取相应的字段并绑定到 SQL 语句中的占位符。 -useGeneratedKeys = true表示获取返回的主键值,keyProperty = "id"表示主键值存在Emp对象的id属性中,添加这句可以直接获取主键值 +`@Options(useGeneratedKeys = true, keyProperty = "id")` 这行配置表示,插入时自动生成的主键会赋值给 `Emp` 对象的 `id` 属性。 +``` +// 调用 mapper 执行插入操作 +empMapper.insert(emp); - -### 查/驼峰命名法 - -表中查询的数据封装到实体类中 - -- 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。 -- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。 - -![image-20221212103124490](https://pic.bitday.top/i/2025/03/19/u6o894-2.png) - -解决方法: - -1. 起别名 -2. 结果映射 -3. **开启驼峰命名** -4. **属性名和表中字段名保持一致** - -**开启驼峰命名(推荐)**:如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射 - -> 驼峰命名规则: abc_xyz => abcXyz -> -> - 表中字段名:abc_xyz ->- 类中属性名:abcXyz - -```java -# 在application.properties中添加: -mybatis.configuration.map-underscore-to-camel-case=true +// 现在 emp 对象的 id 属性会被自动设置为数据库生成的主键值 +System.out.println("Generated ID: " + emp.getId()); ``` -> 要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。 +### 查 -eg:通过页面原型以及需求描述我们要实现的查询: +查询案例: - **姓名:要求支持模糊匹配** - 性别:要求精确匹配 @@ -1480,6 +1533,12 @@ eg:通过页面原型以及需求描述我们要实现的查询: 解决方案: +使用MySQL提供的字符串拼接函数:`concat('%' , '关键字' , '%')` + +**`CONCAT()`** 如果其中任何一个参数为 **`NULL`**,`CONCAT()` 返回 **`NULL`**,`Like NULL`会导致查询不到任何结果! + +`NULL`和`''`是完全不同的 + ```java @Mapper public interface EmpMapper { @@ -1494,8 +1553,6 @@ public interface EmpMapper { } ``` -**使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%')** - ### XML配置文件规范 @@ -1514,7 +1571,7 @@ public interface EmpMapper { \ select * from emp where name like concat('%',#{name},'%') @@ -1546,15 +1605,17 @@ public interface EmpMapper { ``` - XML映射文件中sql语句的id与Mapper接口中的**方法名**一致,并保持**返回类型一致**(也是**全限定名**!!).里面的查询语句与之前的一模一样,仅仅单独写到一个xml文件中罢了。 - - **注意:**返回类型指的是单挑记录的类型,是Emp,不是list + **`id="list"`**:指定查询方法的名称,应该与 Mapper 接口中的方法名称一致。 + + **`resultType="edu.whut.pojo.Emp"`**:`resultType` 只在 **查询操作** 中需要指定。指定查询结果映射的对象类型,这里是 `Emp` 类。 -**这里有bug!!!concat('%',#{name},'%')这里应该用 标签对name是否为''或null进行判断** +这里有bug!!! + +`concat('%',#{name},'%')`这里应该用`` ``标签对name是否为`NULL`或`''`进行判断 + -`''`和`null`虽然在某些上下文中可能看起来相似,但它们代表了不同的概念:一个是具有明确的(虽然是空的)值,另一个是完全没有值。 ### 动态SQL @@ -1570,8 +1631,6 @@ public interface EmpMapper { ``只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR,**加了总比不加好** - - ```java ``` + + #### SQL-foreach -mapper接口: +Mapper 接口 ```java @Mapper @@ -1603,9 +1664,9 @@ public interface EmpMapper { } ``` -xml: +XML 映射文件 -语法: +`` 标签用于遍历集合,常用于动态生成 SQL 语句中的 IN 子句、批量插入、批量更新等操作。 ```java ``` +`open="("`:这个属性表示,在*生成的 SQL 语句开始*时添加一个 左括号 `(`。 +`close=")"`:这个属性表示,在生成的 SQL 语句结束时添加一个 右括号 `)`。 + +例:批量删除实现 ```java - delete from emp where id in - - #{id} - - + DELETE FROM emp WHERE id IN + + #{id} + + ``` +实现效果类似:`DELETE FROM emp WHERE id IN (1, 2, 3);` + ## 案例实战 @@ -1642,15 +1709,42 @@ xml: 分页插件帮我们完成了以下操作: -1. 先获取到要执行的SQL语句:select * from emp -2. 把SQL语句中的字段列表,变为:count(*) -3. 执行SQL语句:select count(*) from emp //获取到总记录数 -4. 再对要执行的SQL语句:select * from emp 进行改造,在末尾添加 limit ? , ? -5. 执行改造后的SQL语句:select * from emp limit ? , ? +1. 先获取到要执行的SQL语句: + + ``` + select * from emp + ``` + +2. 为了实现分页,第一步是获取符合条件的总记录数。分页插件将原始 SQL 查询中的 `SELECT *` 改成 `SELECT count(*)` + + ```mysql + select count(*) from emp; + ``` + +3. 一旦知道了总记录数,分页插件会将 `SELECT *` 的查询语句进行修改,加入 `LIMIT` 关键字,限制返回的记录数。 + + ```mysql + select * from emp limit ?, ? + ``` + + 第一个参数(`?`)是 起始位置,通常是 `(当前页 - 1) * 每页显示的记录数`,即从哪一行开始查询。 + + 第二个参数(`?`)是 每页显示的记录数,即返回多少条数据。 + +4. 执行分页查询,例如,假设每页显示 10 条记录,你请求第 2 页数据,那么 SQL 语句会变成: + + ```mysql + select * from emp limit 10, 10; + ``` + + **使用方法:** -当使用了PageHelper分页插件进行分页,就**无需再Mapper中进行手动分页**了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前**设置分页参数**,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。 +当使用 **PageHelper** 分页插件时,无需在 Mapper 中手动处理分页。只需在 Mapper 中编写常规的列表查询。 + +- 在 **Service 层**,调用 Mapper 方法之前,**设置分页参数**。 +- 调用 Mapper 查询后,**自动进行分页**,并将结果封装到 `PageBean` 对象中返回。 1、在pom.xml引入依赖 @@ -1674,6 +1768,7 @@ public interface EmpMapper { ``` 3、EmpServiceImpl +当调用 `PageHelper.startPage(page, pageSize)` 时,PageHelper 插件会拦截随后的 SQL 查询,自动修改查询,加入 `LIMIT` 子句来实现分页功能。 ```java @Override @@ -1732,7 +1827,7 @@ public class EmpController { order by create_time desc - +/select> ``` diff --git a/自学/Mysql数据库.md b/自学/Mysql数据库.md index 5ce89f3..cb66d92 100644 --- a/自学/Mysql数据库.md +++ b/自学/Mysql数据库.md @@ -186,11 +186,11 @@ desc tb_tmps; ( tb_tmps为表名) ```mysql create table 表名( - 字段1 字段1类型 [约束] [comment 字段1注释 ], - 字段2 字段2类型 [约束] [comment 字段2注释 ], + 字段1 字段1类型 [约束] [comment '字段1注释' ], + 字段2 字段2类型 [约束] [comment '字段2注释' ], ...... - 字段n 字段n类型 [约束] [comment 字段n注释 ] -) [ comment 表注释 ] ; + 字段n 字段n类型 [约束] [comment '字段n注释' ] +) [ comment '表注释' ] ; ``` > 注意: [ ] 中的内容为可选参数; 最后一个字段后面没有逗号 @@ -215,29 +215,56 @@ create table tb_user ( | ------------ | ------------------------------------------------ | ----------- | | 非空约束 | 限制该字段值不能为null | not null | | 唯一约束 | 保证字段的所有数据都是唯一、不重复的 | unique | -| 主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | primary key | +| 主键约束 | 主键是一行数据的唯一标识,要求**非空且唯一** | primary key | | 默认约束 | 保存数据时,如果未指定该字段值,则采用默认值 | default | | **外键约束** | 让两张表的数据建立连接,保证数据的一致性和完整性 | foreign key | -```text -create table tb_user ( - id int primary key auto_increment comment 'ID,唯一标识', - username varchar(20) not null unique comment '用户名', - name varchar(10) not null comment '姓名', - age int comment '年龄', - gender char(1) default '男' comment '性别' -) comment '用户表'; +```mysql +CREATE TABLE tb_user ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID,唯一标识', + username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名', + name VARCHAR(10) NOT NULL COMMENT '姓名', + age INT COMMENT '年龄', + gender CHAR(1) DEFAULT '男' COMMENT '性别' +) COMMENT '用户表'; + +-- 假设我们有一个 orders 表,它将 tb_user 表的 id 字段作为外键 +CREATE TABLE orders ( + order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', + order_date DATE COMMENT '订单日期', + user_id INT, + FOREIGN KEY (user_id) REFERENCES tb_user(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + COMMENT '订单表' +); ``` -auto_increment: +**foreign key:** + +- 保证数据的一致性和完整性 + +- **`ON DELETE CASCADE`**:如果父表中的某行被删除,那么子表中所有与之关联的行也会被自动删除。 + + **`ON DELETE SET NULL`**:如果父表中的某行被删除,子表中的相关外键列会被设置为 `NULL`。 + + **`ON UPDATE CASCADE`**:如果父表中的外键值被更新,那么子表中的相关外键值也会自动更新。 + +注意:在实际的 Java 项目中,特别是在一些微服务架构或分布式系统中,通常**不直接依赖数据库中的外键约束**。相反,开发者通常会在代码中通过逻辑来确保数据的一致性和完整性。 + + + +**auto_increment:** - 每次插入新的行记录时,数据库自动生成id字段(主键)下的值 -- 具有auto_increment的数据列是一个正数序列开始增长(从1开始自增) +- 具有auto_increment的数据列是一个正数序列且整型(从1开始自增) +- 不能应用于多个字段 + + **设计表的字段时,还应考虑:** id:主键,唯一标志这条记录 - create_time :插入记录的时间 now()函数可以获取当前时间 update_time:最后修改记录的时间 @@ -249,6 +276,8 @@ DML英文全称是Data Manipulation Language(数据操作语言),用来对数 - 修改数据(UPDATE) - 删除数据(DELETE) + + ### INSERT insert语法: @@ -277,6 +306,8 @@ insert语法: insert into 表名 values (值1, 值2, ...), (值1, 值2, ...); ~~~ + + ### UPDATE update语法: @@ -299,6 +330,8 @@ update tb_emp set entrydate='2010-01-01',update_time=now(); **注意!**不带where会更新表中所有记录! + + ### DELETE delete语法: @@ -321,6 +354,8 @@ delete from tb_emp; DELETE 语句不能删除某一个字段的值(可以使用UPDATE,将该字段值置为NULL即可)。 + + ## DQL(查询) DQL英文全称是Data Query Language(数据查询语言),用来查询数据库表中的记录。 @@ -391,7 +426,20 @@ LIMIT | or 或 \|\| | 或者 (多个条件任意一个成立) | | not 或 ! | 非 , 不是 | -案例:查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息 + + +**表数据**: + +| id | name | gender | job | entrydate | +| ---- | ---- | ------ | ---- | ---------- | +| 1 | 张三 | 2 | 2 | 2005-04-15 | +| 2 | 李四 | 1 | 3 | 2007-07-22 | +| 3 | 王五 | 2 | 4 | 2011-09-01 | +| 4 | 赵六 | 1 | 2 | 2008-06-11 | + + + +案例1:查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息 ```text select * @@ -400,7 +448,7 @@ where entrydate between '2000-01-01' and '2010-01-01' and gender = 2; ``` -案例8:查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息 +案例2:查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息 ```text select * @@ -408,7 +456,13 @@ from tb_emp where job in (2,3,4); ``` -案例9:查询 姓名 为两个字的员工信息 +案例3:查询 姓名 为两个字的员工信息 + +常见的 **LIKE** 模式匹配符包括: + +​ **`%`**:表示零个或多个字符。 + +​ **`_`**:表示一个字符。 ~~~mysql select * @@ -420,19 +474,31 @@ where name like '__'; # 通配符 "_" 代表任意1个字符 ### 聚合函数 -之前我们做的查询都是横向查询,就是根据条件一行一行的进行判断,而使用聚合函数查询就是**纵向查询**,它是对一列的值进行计算,然后返回一个结果值。(将一列数据作为一个整体,进行纵向计算) +之前我们做的查询都是**横向查询**,就是根据条件一行一行的进行判断,而使用聚合函数查询就是**纵向查询**,它是对一列的值进行计算,然后**返回一个结果值**。(将一列数据作为一个整体,进行纵向计算) + +**聚合函数:** + +| **函数** | **功能** | +| -------- | -------- | +| count | 统计数量 | +| max | 最大值 | +| min | 最小值 | +| avg | 平均值 | +| sum | 求和 | 语法: ~~~mysql -select 聚合函数(字段列表) from 表名 ; +select 聚合函数(字段名、列名) from 表名 ; ~~~ > 注意 : 聚合函数会忽略空值,对NULL值不作为统计。 -```text +```mysql # count(*) 推荐此写法(MySQL底层进行了优化) -select count(*) from tb_emp; +select count(*) from tb_emp; -- 统计记录数 + +SELECT SUM(amount) FROM tb_sales; -- 统计amount列的总金额 ``` @@ -445,10 +511,19 @@ select count(*) from tb_emp; > > 分组查询通常会使用**聚合函数**进行计算。 -```text +```mysql select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤条件]; ``` +*orders表:* + +| customer_id | amount | +| ----------- | ------ | +| 1 | 100 | +| 1 | 200 | +| 2 | 150 | +| 2 | 300 | + 例如,假设我们有一个名为 `orders` 的表,其中包含 `customer_id` 和 `amount` 列,我们想要计算每个客户的订单总金额,可以这样写查询: ```text @@ -459,8 +534,6 @@ GROUP BY customer_id; 在这个例子中,`GROUP BY customer_id` 将结果按照 `customer_id` 列的值进行分组,并对每个客户的订单金额求和,生成每个客户的总金额。 - - ```text SELECT customer_id, SUM(amount) AS total_amount FROM orders @@ -474,10 +547,12 @@ HAVING total_amount > specified_amount; **注意事项:** -​ • 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义 +​ • 分组之后,查询的字段**一般为聚合函数**和分组字段,查询其他字段无任何意义 ​ • 执行顺序:where > 聚合函数 > having + + ### 排序查询 语法: @@ -496,6 +571,14 @@ order by 字段1 排序方式1 , 字段2 排序方式2 … ; - DESC:降序 +```mysql +select id, username, password, name, gender, image, job, entrydate, create_time, update_time +from tb_emp +order by entrydate ASC; -- 按照entrydate字段下的数据进行升序排序 +``` + + + ### 分页查询 ```text @@ -512,6 +595,8 @@ select 字段列表 from 表名 limit 起始索引, 每页显示记录数 3. 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 条数 + + ## 多表设计 ### 外键约束 @@ -520,15 +605,23 @@ select 字段列表 from 表名 limit 起始索引, 每页显示记录数 ```mysql -- 创建表时指定 -create table 表名( - 字段名 数据类型, - ... - [constraint] [外键名称] foreign key (外键字段名) references 主表 (主键名) +CREATE TABLE child_table ( + id INT PRIMARY KEY, + parent_id INT, -- 外键字段 + FOREIGN KEY (parent_id) + REFERENCES parent_table (id) + ON DELETE CASCADE -- 可选,表示父表数据删除时,子表数据也会删除 + ON UPDATE CASCADE -- 可选,表示父表数据更新时,子表数据会同步更新 ); -- 建完表后,添加外键 -alter table 表名 add constraint 外键名称 foreign key(外键字段名) references 主表(主表列名); +ALTER TABLE child_table +ADD CONSTRAINT fk_parent_id -- 外键约束的名称,可选 +FOREIGN KEY (parent_id) +REFERENCES parent_table (id) +ON DELETE CASCADE +ON UPDATE CASCADE; ``` @@ -537,18 +630,20 @@ alter table 表名 add constraint 外键名称 foreign key(外键字段名) ![image-20221206230156403](https://pic.bitday.top/i/2025/03/19/u7bf4a-2.png) -**一对多关系实现:在数据库表中多的一方,添加外键字段,来关联'一'这方的主键。** +一对多关系实现:在数据库表中**多的一方**,添加外键字段(如dept_id),来关联'一'这方的主键(id)。 ### 一对一 -一对一关系表在实际开发中应用起来比较简单,通常是用来做单表的拆分。一对一的应用场景: 用户表=》基本信息表+身份信息表 +一对一关系表在实际开发中应用起来比较简单,通常是用来做**单表的拆分**。一对一的应用场景: 用户表=》基本信息表+身份信息表,以此来提高数据的操作效率。 + +![image-20221207105632634](https://pic.bitday.top/i/2025/04/01/m9jvnx-0.png) - 基本信息:用户的ID、姓名、性别、手机号、学历 - 身份信息:民族、生日、身份证号、身份证签发机关,身份证的有效期(开始时间、结束时间) -**一对一 :在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)** +一对一 :在**任意一方**加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE) @@ -556,11 +651,13 @@ alter table 表名 add constraint 外键名称 foreign key(外键字段名) 多对多的关系在开发中属于也比较常见的。比如:学生和老师的关系,一个学生可以有多个授课老师,一个授课老师也可以有多个学生。 +![image-20221207113341028](https://pic.bitday.top/i/2025/04/01/mbg8ad-0.png) + 案例:学生与课程的关系 - 关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择 -- 实现关系:建立第三张中间表(选课表),中间表至少包含两个外键,分别关联两方主键 +- 实现关系:**建立第三张中间表**(选课表),中间表至少包含两个外键,**分别关联两方主键** @@ -572,7 +669,7 @@ alter table 表名 add constraint 外键名称 foreign key(外键字段名) 1. 连接查询 - - 内连接:相当于查询A、B交集部分数据 + - 内连接:相当于查询A、B**交集**部分数据 ![image-20221207165446062](https://pic.bitday.top/i/2025/03/19/u7b5ng-2.png) @@ -636,16 +733,22 @@ select 字段列表 from 表1 left [ outer ] join 表2 on 连接条件 select 字段列表 from 表1 right [ outer ] join 表2 on 连接条件 ... ; ``` +> 右外连接相当于查询表2(右表)的**所有**数据,当然也包含表1和表2交集部分的数据。 + +案例:查询部门表中所有部门的名称, 和对应的员工名称 + ```text --- 右外连接 -select dept.name , emp.name -from tb_emp AS emp right join tb_dept AS dept +-- 左外连接:以left join关键字左边的表为主表,查询主表中所有数据,以及和主表匹配的右边表中的数据 +select emp.name , dept.name +from tb_emp AS emp left join tb_dept AS dept on emp.dept_id = dept.id; ``` -![image-20240306190305575](https://pic.bitday.top/i/2025/03/19/u7c5gf-2.png) +![image-20221207181204792](https://pic.bitday.top/i/2025/04/01/mgov75-0.png) + + ### 子查询 @@ -748,6 +851,8 @@ select * from emp where entrydate > '2006-01-01'; select e.*, d.* from (select * from emp where entrydate > '2006-01-01') e left join dept d on e.dept_id = d.id ; ~~~ + + ## 事务 简而言之:事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 @@ -757,9 +862,7 @@ select e.*, d.* from (select * from emp where entrydate > '2006-01-01') e left j - 第1种情况:开启事务 => 执行SQL语句 => 成功 => 提交事务 - 第2种情况:开启事务 => 执行SQL语句 => 失败 => 回滚事务 -` - -```text +```mysql -- 开启事务 start transaction ; @@ -770,8 +873,6 @@ delete from tb_dept where id = 1; delete from tb_emp where dept_id = 1; ``` -` - - 上述的这组SQL语句,如果如果执行成功,则提交事务 ```sql @@ -795,6 +896,8 @@ rollback ; > 事务的四大特性简称为:ACID + + ## 索引 索引(index):是帮助数据库高效获取数据的数据结构 。 @@ -864,8 +967,6 @@ musql默认采用B+树来作索引 最大的问题就是在数据量大的情况下,树的层级比较深,会影响检索速度。因为不管是二叉搜索数还是红黑数,一个节点下面只能有两个子节点。此时在数据量大的情况下,就会造成数的高度比较高,树的高度一旦高了,检索速度就会降低。 - - > 说明:如果数据结构是红黑树,那么查询1000万条数据,根据计算树的高度大概是23左右,这样确实比之前的方式快了很多,但是如果高并发访问,那么一个用户有可能需要23次磁盘IO,那么100万用户,那么会造成效率极其低下。所以为了减少红黑树的高度,那么就得增加树的宽度,就是不再像红黑树一样每个节点只能保存一个数据,可以引入另外一种数据结构,一个节点可以保存多个数据,这样宽度就会增加从而降低树的高度。这种数据结构例如BTree就满足。 下面我们来看看B+Tree(多路平衡搜索树)结构中如何避免这个问题: diff --git a/自学/力扣Hot 100题.md b/自学/力扣Hot 100题.md index 8ce9246..cce8e3f 100644 --- a/自学/力扣Hot 100题.md +++ b/自学/力扣Hot 100题.md @@ -265,31 +265,40 @@ visited[i][j] = true; #### `PriorityQueue` -- **基于优先堆(最小堆或最大堆)实现**,元素按优先级排序。 -- **默认是最小堆**,即队首元素是最小的。 -- **支持自定义排序规则**,通过 `Comparator` 实现。 -- **常用操作的时间复杂度**: +- 基于优先堆(最小堆或最大堆)实现,元素按优先级排序。 +- **默认是最小堆**,即队首元素是最小的。 `new PriorityQueue<>(Comparator.reverseOrder());`定义最大堆 +- 支持自定义排序规则,通过 `Comparator` 实现。 - - 插入元素:`O(log n)` - - 删除队首元素:`O(log n)` - - 查看队首元素:`O(1)` -- **常用方法** +**常用方法:** - 1. **`add(E e)` / `offer(E e)`**: - - 将元素插入队列。 - - `add` 在队列满时会抛出异常,`offer` 返回 `false`。 - 2. **`remove()` / `poll()`**: - - 移除并返回队首元素。 - - `remove` 在队列为空时会抛出异常,`poll` 返回 `null`。 - 3. **`element()` / `peek()`**: - - 查看队首元素,但不移除。 - - `element` 在队列为空时会抛出异常,`peek` 返回 `null`。 - 4. **`size()`**: - - 返回队列中的元素数量。 - 5. **`isEmpty()`**: - - 检查队列是否为空。 - 6. **`clear()`**: - - 清空队列。 +`add(E e)` / `offer(E e)`: + +- 功能:将元素插入队列。 +- 时间复杂度:`O(log n)` +- 区别 + - `add`:当队列满时会抛出异常。 + - `offer`:当队列满时返回 `false`,不会抛出异常。 + +`remove()` / `poll()`: + +- 功能:移除并返回队首元素。 +- 时间复杂度:`O(log n)` +- 区别 + - `remove`:队列为空时抛出异常。 + - `poll`:队列为空时返回 `null`。 + +`element()` / `peek()`: + +- 功能:查看队首元素,但不移除。 +- 时间复杂度:`O(1)` +- 区别 + - `element`:队列为空时抛出异常。 + - `peek`:队列为空时返回 `null`。 + +`clear()`: + +- 功能:清空队列。 +- 时间复杂度:`O(n)`(因为需要删除所有元素) ```java import java.util.PriorityQueue; @@ -303,7 +312,6 @@ public class PriorityQueueExample { // 添加元素 minHeap.add(10); minHeap.add(20); - minHeap.add(30); minHeap.add(5); // 查看队首元素 @@ -317,7 +325,6 @@ public class PriorityQueueExample { // 输出: // 5 // 10 - // 30 // 20 // 移除队首元素 @@ -330,11 +337,10 @@ public class PriorityQueueExample { PriorityQueue maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); maxHeap.add(10); maxHeap.add(20); - maxHeap.add(30); maxHeap.add(5); // 查看队首元素 - System.out.println("最大堆队首元素: " + maxHeap.peek()); // 输出 30 + System.out.println("最大堆队首元素: " + maxHeap.peek()); // 输出 20 // 清空队列 minHeap.clear(); @@ -345,6 +351,51 @@ public class PriorityQueueExample { +自己实现大根堆: + +```java +class Solution { + public int findKthLargest(int[] nums, int k) { + int heapSize = nums.length; + buildMaxHeap(nums, heapSize); + for (int i = nums.length - 1; i >= nums.length - k + 1; --i) { + swap(nums, 0, i); + --heapSize; + maxHeapify(nums, 0, heapSize); + } + return nums[0]; + } + + public void buildMaxHeap(int[] a, int heapSize) { + for (int i = heapSize / 2 - 1; i >= 0; --i) { + maxHeapify(a, i, heapSize); + } + } + + public void maxHeapify(int[] a, int i, int heapSize) { + int l = i * 2 + 1, r = i * 2 + 2, largest = i; + if (l < heapSize && a[l] > a[largest]) { + largest = l; + } + if (r < heapSize && a[r] > a[largest]) { + largest = r; + } + if (largest != i) { + swap(a, i, largest); + maxHeapify(a, largest, heapSize); + } + } + + public void swap(int[] a, int i, int j) { + int temp = a[i]; + a[i] = a[j]; + a[j] = temp; + } +} +``` + + + #### **`ArrayList`** - 基于数组实现,支持动态扩展。 @@ -600,11 +651,16 @@ public class QueueExample { ```java Deque stack = new ArrayDeque<>(); +//Deque stack = new LinkedList<>(); stack.push(1); // 入栈 Integer top1=stack.peek() Integer top = stack.pop(); // 出栈 ``` +- **LinkedList** 是基于双向链表实现的,每个节点存储数据和指向前后节点的引用。 +- **ArrayDeque** 则基于动态数组实现,内部使用循环数组来存储数据。 +- **ArrayDeque** 在大多数情况下性能更好,因为数组在内存中连续,缓存友好,且操作(如 push/pop)开销更小。 + **双端队列** diff --git a/自学/苍穹外卖.md b/自学/苍穹外卖.md index 2d6ac0e..5aef4c4 100644 --- a/自学/苍穹外卖.md +++ b/自学/苍穹外卖.md @@ -31,6 +31,8 @@ | 10 | orders | 订单表 | | 11 | order_detail | 订单明细表 | + + ```java @TableName("user") public class User { diff --git a/自学/草稿.md b/自学/草稿.md index f3e1bc3..96cf316 100644 --- a/自学/草稿.md +++ b/自学/草稿.md @@ -1,1233 +1,33 @@ -产品官网:[智标领航 - 招投标AI解决方案](https://intellibid.cn/home) +# ConcurrentHashMap 不同JDK版本的实现对比 -产品后台:https://intellibid.cn:9091/login?redirect=%2Findex +## 1. 数据结构 -项目地址:[zy123/zbparse - zbparse - 智标领航代码仓库](http://47.98.59.178:3000/zy123/zbparse) - -git clone地址:http://47.98.59.178:3000/zy123/zbparse.git - -选择develop分支,develop-xx 后面的xx越近越新。 - -正式环境:121.41.119.164:5000 - -测试环境:47.98.58.178:5000 - -大解析:指从招标文件解析入口进去,upload.py - -小解析:从投标文件生成入口进去,little_zbparse 和get_deviation,两个接口后端一起调 - -## 项目启动与维护: - -![1](https://pic.bitday.top/i/2025/03/24/qlfepx-0.png) - -.env存放一些密钥(大模型、textin等),它是gitignore忽略了,因此在服务器上git pull项目的时候,这个文件不会更新(因为密钥比较重要),需要手动维护服务器相应位置的.env。 - -### **如何更新服务器上的版本:** - -#### 步骤 - -1. 进入项目文件夹 - -![1](https://pic.bitday.top/i/2025/03/24/qlfi9z-0.png) - -**注意:**需要确认.env是否存在在服务器,默认是隐藏的 -输入cat .env -如果不存在,在项目文件夹下sudo vim .env - -将密钥粘贴进去!!! - -2. git pull - -3. sudo docker-compose up --build -d 更新并重启 - - 或者 sudo docker-compose build 先构建镜像 - - sudo docker-compose up -d 等空间时再重启 - -4. sudo docker-compose logs flask_app --since 1h 查看最近1h的日志(如果重启后报错也能查看,推荐重启后都运行一下这个) - -requirements.txt一般无需变动,除非代码中使用了新的库,也要手动在该文件中添加包名及对应的版本 - - - -#### docker基础知识 - -**docker-compose:** - -![1](https://pic.bitday.top/i/2025/03/24/qlfftc-0.png) - -本项目为**单服务项目**,只有flask_app(服务名) - -build context(`context: .`): -这是在构建镜像时提供给 Docker 的文件集,指明哪些文件可以被 Dockerfile 中的 `COPY` 或 `ADD` 指令使用。它是构建过程中的“资源包”。 - -对于多服务,build下就要针对不同的服务,指定所需的“资源包”和对应的Dockerfile - - - -**dockerfile:** - -![1](https://pic.bitday.top/i/2025/03/24/qlfi4o-0.png) - -COPY . .(在 Dockerfile 中): -这条指令会将构建上下文中的所有内容复制到镜像中的当前工作目录(这里是 `/flask_project`)。 - -```text -docker exec -it zbparse-flask_app-1 sh -``` - -这个命令会直接进入到flask_project目录内部ls之后可以看到: - -```text -Dockerfile README.md docker-compose.yml flask_app md_files requirements.txt - -``` - -如果这个基础上再` cd / `会切换到这个容器的根目录,可以看到flask_project文件夹以及其他基础系统环境。如: - -```text -bin boot dev etc flask_project home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var - -``` - -**数据卷挂载:** - - volumes: --/home/Z/zbparse_output_dev:/flask_project/flask_app/static/output *#* *额外的数据卷挂载* - -本地路径:容器内路径 都从根目录找起。 - - - -**完整的容器名** - -```text -<项目名>-<服务名>-<序号> -``` - -**项目名**:默认是当前目录的名称(这里是 `zbparse`),或者你在启动 Docker Compose 时通过 `-p` 参数指定的项目名称。 - -**服务名**:在 `docker-compose.yml` 文件中定义的服务名称(这里是 `flask_app`)。 - -**序号**:如果同一个服务启动了多个容器,会有数字序号来区分(这里是 `1`)。 - -docker-compose exec flask_app sh - -docker exec -it zbparse-flask_app-1 sh - -这两个是等价的,因为docker-compose 会自动找到对应的完整容器名并执行命令。 - - - -**删除所有悬空镜像**(无容器引用的 `` 镜像) - -```text -docker image prune -``` - - - -### **如何本地启动本项目:** - -**Pycharm启动** - -1. requirements.txt里的环境要配好 - conda create -n zbparse python=3.8 - conda activate zbparse - pip install -r requirements.txt -2. .env环境配好 (一般不需要在电脑环境变量中额外配置了,但是要在Pycharm中**安装插件**,使得项目在**启动时**能将env中的环境变量**自动配置**到系统环境变量中!!!) -3. 点击下拉框,Edit configurations - - ![1](https://pic.bitday.top/i/2025/03/24/qlfg63-0.png) - - 设置run_serve.py为启动脚本![1](https://pic.bitday.top/i/2025/03/24/io729q-2.png) - 注意这里的working directory要设置到最外层文件夹,而不是flask_app!!! - - - -**命令行启动** - -1.编写ps1脚本 - -```text -# 切换到指定目录 -cd D:\PycharmProjects\zbparse - -# 激活 Conda 环境 -conda activate zbparse - -# 检查是否存在 .env 文件 -if (Test-Path .env) { - # 读取 .env 文件并设置环境变量 - Get-Content .env | ForEach-Object { - if ($_ -match '^\s*([^=]+)=(.*)') { - $name = $matches[1].Trim() - $value = $matches[2].Trim() - [System.Environment]::SetEnvironmentVariable($name, $value) - } - } -} else { - Write-Host ".env not find" -} - -# 设置 PYTHONPATH 环境变量 -$env:PYTHONPATH = "D:\flask_project" - -# 运行 Python 脚本 -python flask_app\run_serve.py -``` - -`$env:PYTHONPATH = "D:\flask_project"`,告诉 Python 去 D:\flask_project 查找模块,这样就能让 Python 找到你的 `flask_app` 包。 - - - -2.确保conda已添加到系统环境变量 - -- 打开 Anaconda Prompt,然后输入 `where conda` 来查看 conda 的路径。 - -- ```text - 打开系统环境变量Path,添加一条:C:\ProgramData\anaconda3\condabin +- **JDK1.7**: + - 使用 `Segment(分段锁) + HashEntry数组 + 链表` 的数据结构 - 或者 CMD 中 set PATH=%PATH%;新添加的路径 - ```text - ``` +- **JDK1.8及之后**: + - 使用 `数组 + 链表/红黑树` 的数据结构(与HashMap类似) -- 重启终端可以刷新环境变量 +## 2. 锁的类型与宽度 -3.如果你尚未在 PowerShell 中初始化 conda,可以在 Anaconda Prompt 中运行: +- **JDK1.7**: + - 分段锁(Segment)继承了 `ReentrantLock` + - Segment容量默认16,不会扩容 → 默认支持16个线程并发访问 -```text -conda init powershell -``` +- **JDK1.8**: + - 使用 `synchronized + CAS` 保证线程安全 + - 空节点:通过CAS添加 + - 非空节点:通过synchronized加锁 -4.进入到存放run.ps1文件的目录,在搜索栏中输入powershell +## 3. 渐进式扩容(JDK1.8+) -5.默认情况下,PowerShell 可能会阻止运行脚本。你可以调整执行策略: +- **触发条件**:元素数量 ≥ `数组容量 × 负载因子(默认0.75)` +- **扩容过程**: + 1. 创建2倍大小的新数组 + 2. 线程操作数据时,逐步迁移旧数组数据到新数组 + 3. 使用 `transferIndex` 标记迁移进度 + 4. 直到旧数组数据完全迁移完成 -```text -Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -``` - - - -6.运行脚本 - -```text -.\run.ps1 -``` - -**注意!!!** - -Windows 控制台存在QuickEdit 模式,在 QuickEdit 模式下,当你在终端窗口中点击(尤其是拖动或选中内容)时,控制台会进入文本选择状态,从而暂停正在运行的程序!! - -**禁用 QuickEdit 模式** - -- 在 PowerShell 窗口标题栏上点击右键,选择“属性”。 -- 在“选项”选项卡中,取消勾选“快速编辑模式”。 -- 点击“确定”,重启 PowerShell 窗口后再试。 - - - -### 模拟用户请求 - -postman打**post**请求测试: - -http://127.0.0.1:5000/upload - -body: - -{ - - "file_url":"xxxx", - - "zb_type":2 - -} -file_url如何获取:[OSS管理控制台](https://oss.console.aliyun.com/bucket/oss-cn-wuhan-lr/bid-assistance/object?path=test%2F) - -bid-assistance/test 里面找个文件的url,推荐'094定稿-湖北工业大学xxx' -注意这里的url地址有时效性,要经常重新获取新的url - - - -### 清理服务器上的文件夹 - -**1.编写shell文件**,sudo vim clean_dir.sh - -清理/home/Z/zbparse_output_dev下的output1这些二级目录下的c8d2140d-9e9a-4a49-9a30-b53ba565db56这种uuid的三级目录(只保留最近7天)。 - -```text -#!/bin/bash - -# 需要清理的 output 目录路径 -ROOT_DIR="/home/Z/zbparse_output_dev" - -# 检查目标目录是否存在 -if [ ! -d "$ROOT_DIR" ]; then - echo "目录 $ROOT_DIR 不存在!" - exit 1 -fi - -echo "开始清理 $ROOT_DIR 下超过 7 天的目录..." -echo "以下目录将被删除:" - -# -mindepth 2 表示从第二层目录开始查找,防止删除 output 下的直接子目录(如 output1、output2) -# -depth 采用深度优先遍历,确保先处理子目录再处理父目录 -find "$ROOT_DIR" -mindepth 2 -depth -type d -mtime +7 -print -exec rm -rf {} \; - -echo "清理完成。" - -``` - -**2.添加权限。** - -```text -sudo chmod +x ./clean_dir.sh -``` - -**3.执行** - -```text -sudo ./clean_dir.sh -``` - -**以 root 用户的身份编辑 crontab 文件**,从而设置或修改系统定时任务(cron jobs)。每天零点10分清理 - -```text -sudo crontab -e -在里面添加: - -10 0 * * * /home/Z/clean_dir.sh -``` - -**目前测试服务器和正式服务器都写上了!无需变动** - - - -### 内存泄漏问题 - -#### 问题定位 - -**查看容器运行时占用的文件FD套接字FD等**(排查内存泄漏,长期运行这三个值不会很大) - -```text -[Z@iZbp13rxxvm0y7yz7l02hbZ zbparse]$ docker exec -it zbparse-flask_app-1 sh - -ls -l /proc/1/fd | awk ' -BEGIN { - file=0; socket=0; pipe=0; other=0 -} -{ - if(/socket:/) socket++ - else if(/pipe:/) pipe++ - else if(/\/|tmp/) file++ # 识别文件路径特征 - else other++ -} -END { - print "文件FD:", file - print "套接字FD:", socket - print "管道FD:", pipe - print "其他FD:", other -}' -``` - -**可以发现文件FD很大,基本上发送一个请求文件FD就加一,且不会衰减:** - -经排查,@validate_and_setup_logger注解会为每次请求都创建一个logger,需要在@app.teardown_request中获取与本次请求有关的logger并释放。 - -```text -def create_logger(app, subfolder): - """ - 创建一个唯一的 logger 和对应的输出文件夹。 - - 参数: - subfolder (str): 子文件夹名称,如 'output1', 'output2', 'output3' - """ - unique_id = str(uuid.uuid4()) - g.unique_id = unique_id - output_folder = os.path.join("flask_app", "static", "output", subfolder, unique_id) - os.makedirs(output_folder, exist_ok=True) - log_filename = "log.txt" - log_path = os.path.join(output_folder, log_filename) - logger = logging.getLogger(unique_id) - if not logger.handlers: - file_handler = logging.FileHandler(log_path) - file_formatter = CSTFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler.setFormatter(file_formatter) - logger.addHandler(file_handler) - stream_handler = logging.StreamHandler() - stream_handler.setFormatter(logging.Formatter('%(message)s')) - logger.addHandler(stream_handler) - logger.setLevel(logging.INFO) - logger.propagate = False - g.logger = logger - g.output_folder = output_folder #输出文件夹路径 -``` - -handler:每当 logger 生成一条日志信息时,这条信息会被传递给所有关联的 handler,由 handler 决定如何输出这条日志。例如,`FileHandler` 会把日志写入文件,而 `StreamHandler` 会将日志输出到控制台。 - -`logger.setLevel(logging.INFO)` :它设置了 logger 的日志级别阈值。Logger **只会处理大于或等于 INFO 级别**的日志消息(例如 INFO、WARNING、ERROR、CRITICAL),而 DEBUG 级别的消息会被忽略。 - - - -**解决这个文件句柄问题后内存泄漏仍未解决,考虑分模块排查。** - -本项目结构大致是**1.**预处理(文件读取切分) **2.**并发调用5个函数分别调用大模型获取结果。 - -因此排查思路: - -先将**预处理模块单独拎出来**作为接口,上传文件测试。 - -文件一般几MB,首先会读到内存,再处理,必然会占用很多内存,且它是调用每个接口都会经历的环节(little_zbparse/upload等) - - - -**内存泄漏排查工具** - -pip install **memory_profiler** - -```text -from memory_profiler import memory_usage -import time -@profile -def my_function(): - a = [i for i in range(100000)] - time.sleep(1) # 模拟耗时操作 - b = {i: i*i for i in range(100000)} - time.sleep(1) - return a, b - -# 监控函数“运行前”和“运行后”的内存快照 -mem_before = memory_usage()[0] -result=my_function() -mem_after = memory_usage()[0] - print(f"Memory before: {mem_before} MiB, Memory after: {mem_after} MiB") -``` - -@profile注解加在函数上,可以逐行分析内存增减情况。 - -memory_usage()[0] 可以获取当前程序所占内存的**快照** - -![1](https://pic.bitday.top/i/2025/03/24/qlfgiy-0.png) - -产生的数据都存到result变量-》内存中,这是正常的,因此my_function没有内存泄漏问题。 -**但是** - -```text -@profile -def extract_text_by_page(file_path): - result = "" - with open(file_path, 'rb') as file: - reader =PdfReader(file) - num_pages = len(reader.pages) - # print(f"Total pages: {num_pages}") - for page_num in range(num_pages): - page = reader.pages[page_num] - text = page.extract_text() - return "" -``` - -![1](https://pic.bitday.top/i/2025/03/24/qlfhow-0.png) - -可以发现尽管我返回"",内存仍然没有释放!因为就是读取pdf这块发生了内存泄漏! - - - -**tracemalloc** - -```text -def extract_text_by_page(file_path): - result = "" - with open(file_path, 'rb') as file: - reader =PdfReader(file) - num_pages = len(reader.pages) - # print(f"Total pages: {num_pages}") - for page_num in range(num_pages): - page = reader.pages[page_num] - text = page.extract_text() - return result - -# 开始跟踪内存分配 -tracemalloc.start() -# 捕捉函数调用前的内存快照 -snapshot_before = tracemalloc.take_snapshot() -# 调用函数 -file_path=r'C:\Users\Administrator\Desktop\fsdownload\00550cfc-fd33-469e-8272-9215291b175c\ztbfile.pdf' -result = extract_text_by_page(file_path) -# 捕捉函数调用后的内存快照 -snapshot_after = tracemalloc.take_snapshot() -# 比较两个快照,获取内存分配差异信息 -stats = snapshot_after.compare_to(snapshot_before, 'lineno') -print("[ Top 10 内存变化 ]") -for stat in stats[:10]: - print(stat) -# 停止内存分配跟踪 -tracemalloc.stop() -``` - -![1](https://pic.bitday.top/i/2025/03/24/qlffi7-0.png) - -tracemalloc能更深入的分析,不仅是自己写的代码,**调用的库函数**产生的内存也能分析出来。在这个例子中就是PyPDF2中的各个函数占用了大部分内存。 - -**综上,定位到问题,就是读取PDF,使用PyPDF2库的地方** - - - -#### **如何解决:** - -1. 首先尝试用with open打开文件,代替直接使用 - -```text -reader =PdfReader(file_path) -``` - -能够确保文件正常关闭。但是没有效果。 - -2. 考虑为**每次请求开子进程**处理,有效**隔离内存泄漏**导致的资源占用,这样子进程运行结束后会释放资源。 - -3. 但是解析流程是流式/分段返回的,因此还需处理: - -**_child_target** 是一个“桥梁”: - -- 它在子进程内调用 `goods_bid_main(...)` (你的生成器) 并把每一次 `yield` 得到的数据放进队列。 -- 结束时放一个 `None` 表示没有更多数据。 - -**run_in_subprocess** 是主进程使用的接口,开启子进程: - -- 它启动子进程并实时 `get()` 队列数据,然后 `yield` 给外界调用者。 -- 当队列里读到 `None`,说明子进程运行完毕,就 `break` 循环并 `p.join()`。 - -**main_func**是真正执行的函数!!! - -```text -def _child_target(main_func, queue, output_folder, file_path, file_type, unique_id): - """ - 子进程中调用 `main_func`(它是一个生成器函数), - 将其 yield 出的数据逐条放进队列,最后放一个 None 表示结束。 - """ - try: - for data in main_func(output_folder, file_path, file_type, unique_id): - queue.put(data) - except Exception as e: - # 如果要把异常也传给父进程,以便父进程可感知 - queue.put(json.dumps({'error': str(e)}, ensure_ascii=False)) - finally: - queue.put(None) -def run_in_subprocess(main_func, output_folder, file_path, file_type, unique_id): - """ - 启动子进程调用 `main_func(...)`,并在父进程流式获取其输出(通过 Queue)。 - 子进程结束时,操作系统回收其内存;父进程则保持实时输出。 - """ - queue = multiprocessing.Queue() - p = multiprocessing.Process( - target=_child_target, - args=(main_func, queue, output_folder, file_path, file_type, unique_id) - ) - p.start() - - while True: - item = queue.get() # 阻塞等待子进程产出的数据 - if item is None: - break - yield item - - p.join() -``` - -如果开子线程,线程共享同一进程的内存空间,所以如果发生内存泄漏,泄漏的内存会累积在整个进程中,影响所有线程。 - -开子进程的缺点:多进程通常消耗的系统资源(如内存、启动开销)比多线程要大,因为每个进程都需要独立的资源和上下文切换开销。 - - - -**进程池** - -在判断上传的文件是否为招标文件时,需要快速准确地响应。因此既保证**内存不泄漏**,又**保证速度**的方案就是在项目启动时创建进程池。(因为**创建进程需要耗时2到3秒**!) - -如果是Waitress服务器启动,这里的进程池是全局共享的;但如果Gunicorn启动,每个请求分配一个worker进程,进程池是在worker里面共享的!!! - -```text -#创建app,启动时 -def create_app(): - # 创建全局日志记录器 - app = Flask(__name__) - app.process_pool = Pool(processes=10, maxtasksperchild=3) - app.global_logger = create_logger_main('model_log') # 全局日志记录器 - -#调用时 -pool = current_app.process_pool # 使用全局的进程池 -def judge_zbfile_exec_sub(file_path): - result = pool.apply( - judge_zbfile_exec, # 你的实际执行函数 - args=(file_path,) - ) - return result -``` - -但是存在一个问题:**第一次发送请求执行时间较慢!** - -![1](https://pic.bitday.top/i/2025/03/24/qlfgkw-0.png) - -可以发现实际执行只需7.7s,但是接口实际耗时10.23秒,主要是因**懒加载或按需初始化**:有些模块或资源在子进程启动时并不会马上加载,而是在子进程首次真正执行任务时才进行初始化。 - -**解决思路:提前热身(warm up)进程池** - -在应用启动后、还没正式接受请求之前,可以提交一个简单的“空任务”或非常小的任务给进程池,让子进程先**完成相关的初始化**。这种“预热”方式能在正式请求到来之前就完成大部分初始化,减少首次请求的延迟。 - -**还可以快速验证服务是否正常启动** - -```text -def warmup_request(): - # 等待服务器完全启动,例如等待 1-2 秒 - time.sleep(5) - try: - url = "http://127.0.0.1:5000/judge_zbfile" - #url必须为永久地址,完成热启动,创建进程池 - payload = {"file_url": "xxx"} # 根据实际情况设置 file_url - headers = {"Content-Type": "application/json"} - response = requests.post(url, json=payload, headers=headers) - print(f"Warm-up 请求发送成功,状态码:{response.status_code}") - except Exception as e: - print(f"Warm-up 请求出错:{e}") -``` - -threading.Thread(target=warmup_request, daemon=True).start() - - - -## flask_app结构介绍 - -1 - - - -### 项目中做限制的地方 - -#### **账号、服务器分流** - -服务器分流:目前linux服务器和windows服务器主要是硬件上的分流(文件切分需要消耗CPU资源),大模型基底还是调用阿里,共用的tpm qpm。 - -账号分流:qianwen_plus下的 - -```text -api_keys = cycle([ - os.getenv("DASHSCOPE_API_KEY"), - # os.getenv("DASHSCOPE_API_KEY_BACKUP1"), - # os.getenv("DASHSCOPE_API_KEY_BACKUP2") -]) -api_keys_lock = threading.Lock() -def get_next_api_key(): - with api_keys_lock: - return next(api_keys) - -api_key = get_next_api_key() -``` - -只需轮流使用不同的api_key即可。目前没有启用。 - - - -#### **大模型的限制** - -general/llm下的doubao.py 和通义千问long_plus.py -**目前是linux和windows各部署一套,因此项目中的qps是对半的,即calls=?** - -1. 这是qianwen-long的限制(针对阿里qpm为1200,每秒就是20,又linux和windows服务器对半,就是10;TPM无上限) - -```text -@sleep_and_retry -@limits(calls=10, period=1) # 每秒最多调用10次 -def rate_limiter(): - pass # 这个函数本身不执行任何操作,只用于限流 -``` - -2. 这是qianwen-plus的限制(针对tpm为1000万,每个请求2万tokens,那么linux和windows总的qps为8时,8x60x2=960<1000。单个为4) - **经过2.11号测试,calls=4时最高TPM为800,因此把目前稳定版把calls设为5** - - **2.12,用turbo作为超限后的承载,目前把calls设为7** - -```text -@sleep_and_retry -@limits(calls=7, period=1) # 每秒最多调用7次 -def qianwen_plus(user_query, need_extra=False): - logger = logging.getLogger('model_log') # 通过日志名字获取记录器 -``` - -3. qianwen_turbo的限制(TPM为500万,由于它是plus后的手段,稳妥一点,qps设为6,两个服务器分流即calls=3) - -```text -@sleep_and_retry -@limits(calls=3, period=1) # 500万tpm,每秒最多调用6次,两个服务器分流就是3次 (plus超限后的保底手段,稳妥一点) -``` - -**重点!!**后续阿里扩容之后成倍修改这块**calls=?** - -如果不用linux和windows负载均衡,这里的calls也要乘2!! - - - -#### **接口的限制** - -1. start_up.py的def create_app()函数,限制了对每个接口同时100次请求。这里事实上不再限制了(因为100已经足够大了),默认限制做到大模型限制这块。 - -```text -app.connection_limiters['upload'] = ConnectionLimiter(max_connections=100) - app.connection_limiters['get_deviation'] = ConnectionLimiter(max_connections=100) - app.connection_limiters['default'] = ConnectionLimiter(max_connections=100) - app.connection_limiters['judge_zbfile'] = ConnectionLimiter(max_connections=100) -``` - -2. ConnectionLimiter.py以及每个接口上的装饰器,如 - - ```text - @require_connection_limit(timeout=1800) - - def zbparse(): - ``` - - 这里限制了每个接口内部执行的时间,暂时设置到了30分钟!(不包括排队时间)超时就是解析失败 - -#### **后端的限制:** - -目前后端发起招标请求,如果发送超过100(max_connections=100)个请求,我这边会排队后面的请求,这时后端的计时器会将这些请求也视作正在解析中,事实上它们还在排队等待中,这样会导致在极端情况下,新进的解析文件速度大于解析的速度,排队越来越长,后面的文件会因为等待时间过长而直接失败,而不是'解析失败'。 - -​ - -### general - -是公共函数存放的文件夹,llm下是各类大模型,读取文件下是docx pdf文件的读取以及文档清理clean_pdf,去页眉页脚页码 - -![1](https://pic.bitday.top/i/2025/03/24/qlfj98-0.png) - -general下的llm下的清除file_id.py 需要**每周运行至少一次**,防止file_id数量超出(我这边对每次请求结束都有file_id记录并清理,向应该还没加) - -llm下的model_continue_query是'模型继续回答'脚本,应对超长文本模型一次无法输出完的情况,继续提问,拼接成完整的内容。 - - - -general下的file2markdown是textin 文件--》markdown - -general下的format_change是pdf-》docx 或doc/docx->pdf - -general下的merge_pdfs.py是拼接文件的:1.拼接招标公告+投标人须知 2.拼接评标细则章节+资格审查章节 - - - -**general中比较重要的!!!** - -**后处理:** - -general下的**post_processing**,解析后的后处理部分,包括extract_info、 资格审查、技术偏离 商务偏离 所需提交的证明材料,都在这块生成。 - -post_processing中的**inner_post_processing**专门提取*extracted_info* - -post_processing中的**process_functions_in_parallel**提取 - -资格审查、技术偏离、 商务偏离、 所需提交的证明材料 - -![1](https://pic.bitday.top/i/2025/03/24/qlfj26-0.png) - -大解析upload用了post_processing完整版, - -little_zbparse.py、小解析main.py用了inner_post_processing - -get_deviation.py、偏离表数据解析main.py用了process_functions_in_parallel - - - -**截取pdf:** - -*截取pdf_main.py*是顶级函数, - -二级是*截取pdf货物标版*.py和*截取pdf工程标版.py* (非general下) - -三级是*截取pdf通用函数.py* - -如何判断截取位置是否正确?根据output文件夹中的切分情况(打开各个文件查看是否切分准确,目前的逻辑主要是按大章切分,即'招标公告'章节) - - - -**如果切分不准确,如何定位正则表达式?** - -首先判断当前是工程标解析还是货物标解析,即zb_type=1还是2 - -如果是2,那么是货物标解析,那么就是*截取pdf_main.py*调用*截取pdf货物标版*.py,如下图,selection=1代表截取'招标公告',那么如果招标公告没有切准,就在这块修改。这里可以发现get_notice是通用函数,即*截取pdf通用函数.py*中的get_notice函数,那么继续往内部跳转。 - -若开头没截准,就改begin_pattern,末尾没截准,就改end_pattern - -![1](https://pic.bitday.top/i/2025/03/24/qlfjrn-0.png) - -![1](https://pic.bitday.top/i/2025/03/24/qlffvh-0.png) - -另外:在*截取pdf货物标版*.py中,还有extract_pages_twice函数,即第一次没有切分到之后,会运行该函数,这边又有一套begin_pattern和end_pattern,即二次提取 - - - -**如何测试?** - -![1](https://pic.bitday.top/i/2025/03/24/qlfkm6-0.png) - -输入pdf_path,和你要切分的序号,selection=1代表切公告,依次类推,可以看切出来的效果如何。 - - - -**无效标和废标公共代码** - -获取无效标与废标项的主要执行代码。对docx文件进行预处理=》正则=》temp.txt=》大模型筛选 -如果提的不全,可能是正则没涵盖到位,也可能是大模型提示词漏选了。 - -这里:如果段落中既被正则匹配,又被follow_up_keywords中的任意一个匹配,那么不会添加到temp中(即不会被大模型筛选),它会**直接添加**到最后的返回中! - -![1](https://pic.bitday.top/i/2025/03/24/qlfk83-0.png) - - - -**投标人须知正文条款提取成json文件** - -将截取到的ztbfile_tobidders_notice_part2.pdf ,即须知正文,转为clause1.json 文件,便于后续提取**开评定标流程**、**投标文件要求**、**重新招标、不再招标和终止招标** - -这块的主要逻辑就是匹配形如'一、总则'这样的大章节 - -然后匹配形如'1.1' '1.1.1'这样的序号,由于是按行读取pdf,一个序号后面的内容可能有好几行,因此遇到下一个序号(如'2.1')开头,之前的内容都视为上一个序号的。 - - - -### old_version - -都是废弃文件代码,未在正式、测试环境中使用的,不用管 - -![1](https://pic.bitday.top/i/2025/03/24/qlfl37-0.png) - - - -### routes - -是接口以及主要实现部分,一一对应 - -![1](https://pic.bitday.top/i/2025/03/24/qlfjt6-0.png) - -get_deviation对应偏离表数据解析main,获得偏离表数据 - -judge_zbfile对应判断是否是招标文件 - -little_zbparse对应小解析main,负责解析extract_info - -test_zbparse是测试接口,无对应 - -upload对应工程标解析和货物标解析,即大解析 - -**混淆澄清**:小解析可以指代一个过程,即从'投标文件生成'这个入口进去的解析,后端会同时调用little_zbparse和get_deviation。这个过程称为'小解析'。 - -但是little_zbparse也叫小解析,命名如此因为最初只需返回这些数据(extract_info),后续才陆续返回商务、技术偏离... - - - -utils是接口这块的公共功能函数。其中validate_and_setup_logger函数对不同的接口请求对应到不同的output文件夹,如upload->output1。后续增加接口也可直接在这里写映射关系。 - -![1](https://pic.bitday.top/i/2025/03/24/qlfsl5-0.png) - -重点关注大解析:**upload.py**和**货物标解析main.py** - - - -### static - -存放解析的输出和提示词 - -其中output用gitignore了,git push不会推送这块内容。 - -各个文件夹(output1 output2..)对应不同的接口请求 - -![1](https://pic.bitday.top/i/2025/03/24/qlfqqb-0.png) - - - -### test_case&testdir - -test_case是测试用例,是对一些函数的测试。好久没更新了 - -testdir是平时写代码的测试的地方 - -它们都不影响正式和测试环境的解析 - -![1](https://pic.bitday.top/i/2025/03/24/qlfo67-0.png) - - - -### 工程标&货物标 - -是两个解析流程中不一样的地方(一样的都写在**general**中了) - -![1](https://pic.bitday.top/i/2025/03/24/qlfpg9-0.png) - -主要是货物标额外解析了采购要求(提取采购需求main+技术参数要求提取+商务服务其他要求提取) - - - -### 最后: - -ConnectionLimiter.py定义了接口超时时间->超时后断开与后端的连接 - -![1](https://pic.bitday.top/i/2025/03/24/qlfs2b-0.png) - -logger_setup.py 为每个请求创建单独的log,每个log对应一个log.txt - -start_up.py是启动脚本,run_serve也是启动脚本,是对start_up.py的简单封装,目前dockerfile定义的直接使用run_serve启动 - - - -## 持续关注 - -```text - yield sse_format(tech_deviation_response) - yield sse_format(tech_deviation_star_response) - yield sse_format(zigefuhe_deviation_response) - yield sse_format(shangwu_deviation_response) - yield sse_format(shangwu_star_deviation_response) - yield sse_format(proof_materials_response) -``` - -1. 工程标解析目前仍没有解析采购要求这一块,因此后处理返回的只有'资格审查'和''证明材料"和"extracted_info",没有''商务偏离''及'商务带星偏离',也没有'技术偏离'和'技术带星偏离',而货物标解析是完全版。 - - 其中''证明材料"和"extracted_info"是直接返给后端保存的 - -2. 大解析中返回了技术评分,后端接收后不仅显示给前端,还会返给向,用于生成技术偏离表 -3. 小解析时,get_deviation.py其实也可以返回技术评分,但是没有返回,因为没人和我对接,暂时注释了。 - -![1](https://pic.bitday.top/i/2025/03/24/qlfqdw-0.png) - - - -4.商务评议和技术评议偏离表,即评分细则的偏离表,暂时没做,但是**商务评分、技术评分**无论大解析还是小解析都解析了,稍微对该数据处理一下返回给后端就行。 - -![1](https://pic.bitday.top/i/2025/03/24/qlft09-0.png) - -这个是解析得来的结果,适合给前端展示,但是要生成商务技术评议偏离表的话,需要再调一次大模型,对该数据进行重新归纳,以字符串列表为佳。再传给后端。(未做) - - - -### 如何定位问题 - -1. 查看static下的output文件夹 (upload大解析对应output1) -2. docker-compose文件中规定了数据卷挂载的路径:- /home/Z/zbparse_output_dev:/flask_project/flask_app/static/output - 也就是说static/output映射到了服务器的Z/zbparse_output_dev文件夹 -3. 根据时间查找哪个子文件夹(uuid作为子文件名) -4. 查看是否有final_result.json文件,如果有,说明解析流程正常结束了,问题可能出在后端(a.后端接口请求超限30分钟 b.后处理存在解析数据的时候出错) - - 也可能出现在自身解析,可以查看子文件内的log.txt,查看日志。 - -5. 若解析正常(有final_result)但解析不准,可以根据以下定位: - - a.查看子文件夹下的文件切分是否准确,例如:如果评标办法不准确,那么查看ztbfile_evaluation_methon,是否正确切到了评分细则。如果切到了,那就改general/商务技术评分提取里的提示词;否则修改截取pdf那块关于'评标办法'的正则表达式。 - - b.总之是**先看切的准不准,再看提示词能否优化**,都要定位到对应的代码中! - - - -## 学习总结 - -### Flask + Waitress : - -Flask 和 Waitress 是两个不同层级的工具,在 Python Web 开发中扮演互补角色。它们的协作关系可以概括为:**Flask 负责构建 Web 应用逻辑,而 Waitress 作为生产级服务器承载 Flask 应用**。 - -```text -# Flask 开发服务器(仅用于开发) -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) - -# 使用 Waitress 启动(生产环境) -from waitress import serve -serve(app, host='0.0.0.0', port=8080) -``` - -**Waitress 的工作方式** - -- 作为 WSGI 服务器:Waitress 作为一个 WSGI 服务器,负责监听指定端口上的网络请求,并将请求传递给 WSGI 应用(如 Flask 应用)。 - -- 多线程处理:默认情况下,waitress 在**单个进程**内启用线程池。当请求到达时,waitress 会从线程池中分配一个线程来处理这个请求。由于 GIL 限制,同一时间只有一个线程在执行 Python 代码(只能使用一个核心,CPU利用率只能到100%)。 - -**Flask 与 waitress 的协同工作** - -- **WSGI 接口**:Flask 应用实现了 WSGI 接口。waitress 接收到请求后,会调用 Flask 应用对应的视图函数来处理请求,生成响应。 -- **请求处理流程** - - 请求进入 waitress - - waitress 分配一个线程并调用 Flask 应用 - - Flask 根据路由匹配并执行对应的处理函数 - - 处理函数返回响应,waitress 将响应发送给客户端 - -**Waitress 的典型使用场景** - -1. **跨平台部署**:尤其适合 Windows 环境(Gunicorn 等服务器不支持)。 -2. **简单配置**:无需复杂设置即可获得比开发服务器(Flask自带)更强的性能。 -3. **中小型应用**:对并发要求不极高的场景,Waitress 的轻量级特性优势明显。 - -**Waitress的不足与处理** - -由于 waitress 是在单进程下工作,所有线程共享进程内存,如果业务逻辑简单且无复杂资源共享问题,这种方式是足够的。 - -**引入子进程**:如果需要每个请求实现内存隔离或者绕过 GIL 来利用多核 CPU,有时会在 Flask 视图函数内部启动子进程来处理实际任务。 - -**直接采用多进程部署方案**:使用 Gunicorn 的多 worker 模式 - - - -### Gunicorn - -**Gunicorn 的工作方式** - -- **预启动 Worker 进程**。Gunicorn 启动时,会按照配置数量(例如 4 个 worker)创建多个 worker 进程。这些 worker 进程会一直运行,并监听同一个端口上的请求。不会针对每个请求单独创建新进程。 -- **共享 socket**:所有 worker 进程共享同一个监听 socket,当有请求到来时,操作系统会将请求分发给某个空闲的 worker。 - -推荐worker 数量 = (2 * CPU 核心数) + 1 - -**如何启动:** - -要使用异步 worker,你需要: - -```text -pip install gevent -``` - -启动 Gunicorn 时指定 worker 类型和数量,例如: - -```text -gunicorn -k gevent -w 4 --max-requests 100 flask_app.start_up:create_app --bind 0.0.0.0:5000 - -``` - -使用 `-k gevent`(或者 `-k eventlet`)就可以使用异步 worker,单个 worker 能够处理多个 I/O 密集型请求。 - -使用--max-requests 100 。每个 worker 在处理完 100 个请求后会自动重启,从而释放可能累积的内存。 - - - -### 本项目的执行流程: - -1. 调用CPU进行PDF文件的读取与切分,CPU密集型,耗时半分钟 -2. 针对切分之后的不同部分,分别调用大模型,得到回答,IO密集型,耗时2分钟。 - -解决方案: - -1.使用flask+waitress,waitress会为每个用户请求开新的线程处理,然后我的代码逻辑会在这个线程内**开子进程**来执行具体的代码,以绕过GIL限制,且正确释放内存资源。 - -**后续可以开一个共享的进程池代替为每个请求开子进程。以避免高并发下竞争多核导致的频繁CPU切换问题。 - -2.使用Gunicorn的异步worker,gunicorn为固定创建worker(进程),处理用户请求,一个异步 worker 可以同时处理多个用户请求,因为当一个请求在等待外部响应(例如调用大模型接口)时,worker 可以切换去处理其他请求。 - - - -### 全局解释器锁(GIL): - -Python(特别是 CPython 实现)中有一个叫做全局解释器锁(Global Interpreter Lock,简称 GIL)的机制,这个锁确保在任何时刻**只有一个线程**在执行 **Python 字节码。** - -这意味着,即使你启动了多个线程,它们在执行 Python 代码时实际上是串行执行的,而不是并行利用多核 CPU。 - -在 Java 中,多线程通常能充分利用多核,因为 **Java 的线程是真正的系统级线程**,不存在类似 CPython 中的 GIL 限制。 - -**影响**: - -- **CPU密集型任务**:由于 GIL 的存在,在 CPU 密集型任务中,多线程往往不能提高性能,因为同时只有一个线程在执行 Python 代码。 -- **I/O密集型任务**:如果任务主要等待 I/O(例如**网络**、**磁盘读写**),线程在等待时会释放 GIL,此时多线程可以提高程序的响应性和吞吐量。 - - - -**NumPy**能够在一定程度上绕过 Python 的 GIL 限制。许多 NumPy 的数值计算操作(如矩阵乘法、向量化运算等)是由高度优化的 C 或 Fortran 库(如 BLAS、LAPACK)实现的。这些库通常在执行计算密集型任务时会释放 GIL。**C 扩展模块的方式将 C 代码嵌入到 Python 中,从而利用底层 C 库的高性能优势** - - - -### 进程与线程 - -1、进程是操作系统分配任务的基本单位,进程是python中正在运行的程序;当我们打开了1个浏览器时就是开始了一个浏览器进程; -线程是进程中执行任务的基本单元(执行指令集),一个进程中至少有一个线程、当只有一个线程时,称为**主线程** -2、线程的创建和销毁耗费资源少,进程的创建和销毁耗费资源多;线程很容易创建,进程不容易创建 -3、线程的切换速度快,进程慢 -4、一个进程中有多个线程时:线程之间可以进行通信;一个进程中有多个子进程时,进程与进程之间不可以相互通信,如果需要通信时,就必须通过一个中间代理实现,Queue、Pipe。 -5、多进程可以利用多核cpu,**多线程不可以利用多核cpu** -6、一个新的线程很容易被创建,一个新的进程创建需要对父进程进行一次克隆 -7、多进程的主要目的是充分使用CPU的多核机制,**多线程的主要目的是充分利用某一个单核** -——————————————— - -**每个进程有自己的独立 GIL** - -**多线程适用于 I/O 密集型任务** - -**多进程适用于CPU密集型任务** - -**因此,多进程用于充分利用多核,进程内开多线程以充分利用单核。** - - - -#### 进程池 - -**multiprocessing.Pool库:**,通过 `maxtasksperchild` 指定每个子进程在退出前最多执行的任务数,这有助于防止某些任务中可能存在的内存泄漏问题 - -```text -pool =Pool(processes=10, maxtasksperchild=3) -``` - -**concurrent.futures.ProcessPoolExecutor**更高级、更统一,没有类似 `maxtasksperchild` 的参数,意味着进程在整个执行期内会一直存活,适合任务本身**比较稳定**的场景。 - -pool =ProcessPoolExecutor(max_workers=10) - -最好创建的进程数**等同于**CPU核心数,如果大于,且每个进程都是CPU密集型(高负债一直用到CPU),那么进程之间会竞争CPU,导致上下文切换增加,反而会降低性质。 - -设置的工作进程数接近 CPU 核心数,以便每个进程能**独占一个核**运行。 - - - -#### 进程、线程间通信 - -**线程间通信**: - -- 线程之间可以直接共享全局变量、对象或数据结构,不需要额外的序列化过程,但这也带来了同步的复杂性(如竞态条件)。 - -```text -import threading -num=0 -def work(): - global num - for i in range(1000000): - num+=1 - print('work',num) - - -def work1(): - global num - for i in range(1000000): - num+=1 - print('work1',num) - -if __name__ == '__main__': - t1=threading.Thread(target=work) - t2=threading.Thread(target=work1) - t1.start() - t2.start() - t1.join() - t2.join() - print('主线程执行结果',num) -``` - -运行结果: - -```text -work 1551626 - -work1 1615783 - -主线程执行结果 1615783 -``` - -这些数值都小于预期的 2000000,因为: - -即使存在 GIL,`num += 1` 这样的操作实际上**并不是原子**的。GIL 确保同一时刻只有一个线程执行 Python 字节码,但在执行 `num += 1` 时,实际上会发生下面几步操作: - -1. 从内存中读取 `num` 的当前值 -2. 对读取到的值进行加 1 操作 -3. 将新的值写回到内存 - -**由多个字节码组成!!!** - -因此会导致: - -线程 A 读取到 `num` 的值 - -切换到线程 B,线程 B 也读取同样的 `num` 值并进行加 1,然后写回 - -当线程 A 恢复时,它依然基于之前读取的旧值进行加 1,最后写回,从而覆盖了线程 B 的更新 - -**解决:** - -```text -from threading import Lock - -import threading -num=0 -def work(): - global num - for i in range(1000000): - with lock: - num+=1 - print('work',num) - -def work1(): - global num - for i in range(1000000): - with lock: - num+=1 - print('work1',num) - -if __name__ == '__main__': - lock=Lock() - t1=threading.Thread(target=work) - t2=threading.Thread(target=work1) - t1.start() - t2.start() - t1.join() - t2.join() - print('主线程执行结果',num) - -``` - - - -**进程间通信(IPC)**: - -- 进程之间默认不共享内存,因此如果需要传递数据,就必须使用专门的通信机制。 -- 在 Python 中,可以使用 `multiprocessing.Queue`、`multiprocessing.Pipe`、共享内存(如 `multiprocessing.Value` 和 `multiprocessing.Array`)等方式实现进程间通信。 - -```text -from multiprocessing import Process, Queue - -def worker(process_id, q): - # 每个进程将数据放入队列 - q.put(f"data_from_process_{process_id}") - print(f"Process {process_id} finished.") - -if __name__ == '__main__': - q = Queue() - processes = [] - for i in range(5): - p = Process(target=worker, args=(i, q)) - processes.append(p) - p.start() - - for p in processes: - p.join() - - # 从队列中收集数据 - results = [] - while not q.empty(): - results.append(q.get()) - - print("Collected data:", results) -``` - -- 当你在主进程中创建了一个 `Queue` 对象,然后将它作为参数传递给子进程时,子进程会获得一个能够与主进程通信的“句柄”。 - -- 子进程中的 `q.put(...)` 操作会将数据通过这个管道传送到主进程,而主进程可以通过 `q.get()` 来获取这些数据。 - -- 这种机制虽然看起来像是“共享”,但实际上是通过 IPC(进程间通信)实现的,而不是直接共享内存中的变量。 - - - -### 项目贡献 - -![1](https://pic.bitday.top/i/2025/03/24/qlfmwr-0.png) - -![1](https://pic.bitday.top/i/2025/03/24/qlgebb-0.png) - -![1](https://pic.bitday.top/i/2025/03/24/qlfrmu-0.png) - -![1](https://pic.bitday.top/i/2025/03/24/qlfsml-0.png) - -### 效果图 - -![1](https://pic.bitday.top/i/2025/03/24/qlfm43-0.png) - -![](https://pic.bitday.top/i/2025/03/24/qlhcva-0.gif) - -![1](https://pic.bitday.top/i/2025/03/24/qlhexw-0.png) - -![1](https://pic.bitday.top/i/2025/03/24/qlhmzd-0.png) - -![1](https://pic.bitday.top/i/2025/03/24/qlgydj-0.png) \ No newline at end of file +### 关键改进点: +- 降低大数据量扩容时的性能开销 +- 允许读写操作与扩容并发进行 \ No newline at end of file