首页
关于
Search
1
图神经网络
8 阅读
2
数学基础
7 阅读
3
欢迎使用 Typecho
6 阅读
4
线性代数
4 阅读
5
linux服务器
3 阅读
默认分类
科研
自学
登录
最新发布
2025-03-21
数学基础
数学基础 求解一阶非齐线性微分方程 考虑方程 $$ y' + y = x $$ 第一步:求齐次方程的通解 先求对应的齐次方程 $$ y' + y = 0 $$ 其解为 $$ y_h = Ce^{-x} $$ 其中 $C$ 为任意常数。 第二步:设特解形式 利用常数变易法,令特解取形式 $$ y_p = u(x) e^{-x} $$ 其中 $u(x)$ 为待定函数。 第三步:求导并代入原方程 计算 $y_p$ 的导数: $$ y_p' = u'(x)e^{-x} - u(x)e^{-x} $$ 将 $y_p$ 和 $y_p'$ 代入原方程 $y' + y = x$: $$ \bigl[u'(x)e^{-x} - u(x)e^{-x}\bigr] + u(x)e^{-x} = u'(x)e^{-x} = x $$ 因此有: $$ u'(x) = x e^ $$ 第四步:求 $u(x)$ 对 $u'(x)$ 积分: $$ u(x) = \int x e^ dx $$ 计算积分,可以用分部积分法:令 $$ \begin{cases} u = x, \quad dv = e^x dx,\\[1mm] du = dx, \quad v = e^x, \end{cases} $$ 得: $$ \int x e^x dx = x e^x - \int e^x dx = x e^x - e^x + C_1 = e^x (x-1) + C_1 $$ 注意这里求得的常数 $C_1$可以忽略,因为它会与齐次解合并。故我们取 $$ u(x) = e^x (x-1) $$ 第五步:构造特解并给出通解 将 $u(x)$ 带回特解形式: $$ y_p = u(x)e^{-x} = e^x (x-1) e^{-x} = x-1 $$ 因此,原方程的通解为齐次解与特解的和: $$ y = y_h + y_p = Ce^{-x} + (x-1) $$ 梯度下降 我们可以用一个简单的线性层作为例子,展示如何利用向量和矩阵计算梯度并更新参数。假设有一个全连接层,其计算公式为 $$ y = W x + b $$ 其中 $x \in \mathbb{R}^2$ 是输入向量 $W \in \mathbb{R}^{2\times2}$ 是权重矩阵 $b \in \mathbb{R}^2$ 是偏置向量 $y \in \mathbb{R}^2$ 是输出向量 我们使用均方误差(MSE)作为损失函数,定义为 $$ L = \frac{1}{2} \|y - y_{\text{true}}\|^2 = \frac{1}{2} \sum_{i=1}^{2}(y_i - y_{\text{true}, i})^2 $$ 设定具体数值 输入向量: $$ x = \begin{pmatrix} 1 \\ 2 \end{pmatrix} $$ 权重矩阵: $$ W = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} $$ 偏置向量: $$ b = \begin{pmatrix} 1 \\ 1 \end{pmatrix} $$ 真实输出: $$ y_{\text{true}} = \begin{pmatrix} 7 \\ 13 \end{pmatrix} $$ 步骤 1:前向传播 计算输出 $y$: $$ y = W x + b = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \begin{pmatrix} 1 \\ 2 \end{pmatrix} + \begin{pmatrix} 1 \\ 1 \end{pmatrix} $$ 首先计算矩阵乘法: $$ W x = \begin{pmatrix} 1\cdot1 + 2\cdot2 \\ 3\cdot1 + 4\cdot2 \end{pmatrix} = \begin{pmatrix} 1+4 \\ 3+8 \end{pmatrix} = \begin{pmatrix} 5 \\ 11 \end{pmatrix} $$ 再加上偏置 $b$ 得到 $$ y = \begin{pmatrix} 5+1 \\ 11+1 \end{pmatrix} = \begin{pmatrix} 6 \\ 12 \end{pmatrix} $$ 计算损失 $L$: $$ L = \frac{1}{2} \left[(6-7)^2 + (12-13)^2\right] = \frac{1}{2} \left[(-1)^2 + (-1)^2\right] = \frac{1}{2} (1+1) = 1 $$ 步骤 2:反向传播,计算梯度 首先,我们定义误差向量为 $$ e = y - y_{\text{true}} = \begin{pmatrix} 6-7 \\ 12-13 \end{pmatrix} = \begin{pmatrix} -1 \\ -1 \end{pmatrix} $$ 由于损失函数 $$ L = \frac{1}{2}\|y - y_{\text{true}}\|^2 $$ 对 $y$ 的偏导数为 $$ \frac{\partial L}{\partial y} = y - y_{\text{true}} = e = \begin{pmatrix} -1 \\ -1 \end{pmatrix} $$ 接下来,我们利用链式法则将梯度传递到 $W$ 和 $b$。 1. 梯度对 $W$ 的求导 对于输出层有 $$ y = W x + b $$ 每个元素 $y_i$ 对 $W_{ij}$ 的偏导数为 $$ \frac{\partial y_i}{\partial W_{ij}} = x_j $$ 利用链式法则,损失对 $W_{ij}$ 的梯度为 $$ \frac{\partial L}{\partial W_{ij}} = \frac{\partial L}{\partial y_i} \cdot \frac{\partial y_i}{\partial W_{ij}} = e_i \, x_j $$ 用矩阵形式写就是: $$ \frac{\partial L}{\partial W} = e \cdot x^\top $$ 将数值代入: $$ e = \begin{pmatrix} -1 \\ -1 \end{pmatrix}, \quad x^\top = \begin{pmatrix} 1 & 2 \end{pmatrix} $$ 所以, $$ \frac{\partial L}{\partial W} = \begin{pmatrix} -1 \\ -1 \end{pmatrix} \begin{pmatrix} 1 & 2 \end{pmatrix} = \begin{pmatrix} -1\cdot1 & -1\cdot2 \\ -1\cdot1 & -1\cdot2 \end{pmatrix} = \begin{pmatrix} -1 & -2 \\ -1 & -2 \end{pmatrix} $$ 2.梯度对 $b$ 的求导 由于 $y = W x + b$,且对 $b$ 的偏导数为 1, $$ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial b} = e \cdot 1 = e = \begin{pmatrix} -1 \\ -1 \end{pmatrix} $$ 步骤 3:使用梯度下降更新参数 设定学习率 $\eta = 0.1$,更新公式为 $$ W_{\text{new}} = W - \eta \frac{\partial L}{\partial W}, \quad b_{\text{new}} = b - \eta \frac{\partial L}{\partial b} $$ 更新 $W$ $$ W_{\text{new}} = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} - 0.1 \cdot \begin{pmatrix} -1 & -2 \\ -1 & -2 \end{pmatrix} = \begin{pmatrix} 1 + 0.1 & 2 + 0.2 \\ 3 + 0.1 & 4 + 0.2 \end{pmatrix} = \begin{pmatrix} 1.1 & 2.2 \\ 3.1 & 4.2 \end{pmatrix} $$ 更新 $b$ $$ b_{\text{new}} = \begin{pmatrix} 1 \\ 1 \end{pmatrix} - 0.1 \cdot \begin{pmatrix} -1 \\ -1 \end{pmatrix} = \begin{pmatrix} 1 + 0.1 \\ 1 + 0.1 \end{pmatrix} = \begin{pmatrix} 1.1 \\ 1.1 \end{pmatrix} $$ 总结 在这个例子中,我们展示了如何用向量和矩阵的形式计算一个简单全连接层的前向传播、损失以及对参数 $W$ 和 $b$ 的梯度。关键步骤如下: 前向传播:计算 $y = W x + b$ 得到输出,再计算损失 $L = \frac{1}{2}|y - y_{\text{true}}|^2$ 反向传播: 计算误差向量 $e = y - y_{\text{true}}$ 利用链式法则得出梯度: $\frac{\partial L}{\partial W} = e \cdot x^\top$ $\frac{\partial L}{\partial b} = e$ 参数更新:通过梯度下降将参数沿负梯度方向调整 这样,我们就得到了更新后的参数 $W_{\text{new}}$ 和 $b_{\text{new}}$。这种向量或矩阵形式的梯度计算方法在真实神经网络中是普遍应用的,能够有效处理高维数据和大规模参数。 期望、方差、协方差 期望 $$ E(X) = \sum_{i} x_i \cdot P(x_i) $$ 其中: $x_i$ 是随机变量$X$ 的取值, $P(x_i)$ 是 $x_i$ 发生的概率。 性质 线性性: $$ E(aX + bY) = aE(X) + bE(Y) $$ 独立变量: $$ E(XY) = E(X)E(Y) \quad (\text{当}X,Y\text{独立时}) $$ 常数处理: $$ E(c) = c $$ 方差 标准差 $$ \sigma =\sqrt{\frac{\textstyle\sum_{i=1}^{n}{( _{i}-\overline )}^{2}}{n}} $$ 方差 它是一个标量,表示一个单一随机变量的变动程度。 $$ Var(X)=\mathrm{E}[{(X-\mu) }^{2}]= {\sigma}^{2} $$ 性质 $$ Var(X)=\mathrm{E}({X}^{2})-{[\mathrm{E}(X)]}^{2} \\ Var(kX)={k}^{2}Var(X) $$ 若X和Y是独立的随机变量 $$ Var(X+Y)=Var(X)+Var(Y) $$ 协方差 给定两个随机变量 $X$ 和 $Y$,其协方差计算公式为: $$ \text{Cov}(X,Y) = \sum_{i=1}^n (x_i - \mu_X)(y_i - \mu_Y) $$ 其中: $x_i, y_i$ 为观测值 $\mu_X, \mu_Y$ 分别为 $X$ 和 $Y$ 的样本均值 直观理解:如果有$X$,$Y$两个变量,$X$增大,$Y$也倾向于增大,$Cov(X,Y)>0$,正相关;$X$增加,$Y$倾向于减小->负相关;否则不相关。 推广:概率分布中的协方差 $\text{Cov}(X,Y) =\sum_{i=1}^n {p}{i}( {i}-{\mu }{\mathrm{X}})({\mathcal{y}}{i}-{\mu }_{Y})=E\left[(X-\mu_X)(Y-\mu_Y)\right]$ 性质 对称性 $$ \text{Cov}(X, Y) = \text{Cov}(Y, X) $$ 协方差的计算与变量顺序无关 线性性 $$ \text{Cov}(aX + b, cY + d) = ac \cdot \text{Cov}(X, Y) $$ 零自协方差 $$ \text{Cov}(X, X) = \text{Var}(X) $$ 分解性 $$ \text{Cov}(X_1 + X_2, Y) = \text{Cov}(X_1, Y) + \text{Cov}(X_2, Y) $$ 标量倍数 $$ \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$) 协方差矩阵的元素是通过计算每对随机变量之间的协方差来获得的。协方差矩阵 $\Sigma$ 的元素可以表示为: $$ \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} $$ 其中: 对角线上的元素 $\text{Cov}(X_i, X_i)$ 是每个变量的方差,即 $\text{Var}(X_i)$。 非对角线上的元素 $\text{Cov}(X_i, X_j)$ 是变量 $X_i$ 和 $X_j$ 之间的协方差。 计算举例 假设我们有 3 个特征($n=3$)和 4 个样本($m=4$),则数据矩阵 $X$ 的构造如下: $$ 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} $$ 中心化数据(每行减去均值): 计算每行均值: $$ \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} $$ 计算协方差矩阵: $$ \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的噪声? 生成标准正态随机变量 $$ \mathbf{Z} \sim \mathcal{N}(0, \mathbf{I}) $$ 进行线性变换 $$ \mathbf{w}_k = \sqrt{\mathbf{Q}} \cdot \mathbf{Z} $$ 其中 $\sqrt{\mathbf{Q}}$ 是 $\mathbf{Q}$ 的矩阵平方根。 验证其协方差确实为Q: $$ \begin{aligned} \text{Cov}(\mathbf{w}_k) &= \mathbb{E}[\mathbf{w}_k\mathbf{w}_k^T] \ &= \sqrt{\mathbf{Q}} \cdot \mathbb{E}[\mathbf{Z}\mathbf{Z}^T] \cdot \sqrt{\mathbf{Q}}^T \ &= \sqrt{\mathbf{Q}} \cdot \mathbf{I} \cdot \sqrt{\mathbf{Q}}^T \ &= \mathbf{Q} \end{aligned} $$ Python代码示例 import numpy as np # 定义协方差矩阵 Q = np.array([[0.1, 0.05], [0.05, 0.2]]) # Cholesky分解 L = np.linalg.cholesky(Q) # L @ L.T = Q # 生成标准正态随机数 Z = np.random.randn(2) # 生成目标噪声 w = L @ Z # 等价于 np.dot(L, Z) 概率密度函数 定义: 概率密度函数是描述连续型随机变量在某个取值点附近的可能性"密度"的函数。注意: PDF在某一点的值不是概率,而是概率的"密度"。 实际概率是通过对PDF在某个区间内积分得到的(比如 $P(a \leq X \leq b) = \int_a^b f(x)dx$)。 PDF的全域积分必须等于1(即所有可能性的总和为100%)。 例子: 假设某人的每日通勤时间 $X$ 是一个连续随机变量,其PDF可能是一个钟形曲线(如正态分布)。PDF在 $x=30$ 分钟处的值 $f(30)$ 表示"30分钟附近"的概率密度,而 $P(25 \leq X \leq 35)=0.4$ 表示约有40%的概率通勤时间会落在这个区间。 指数分布 定义: 指数分布是一种常见的连续型概率分布,通常用于描述"事件间隔时间"或"无记忆性"的过程。比如: 客服电话的间隔时间。 灯泡的寿命。 地震发生的间隔时间。 概率密度函数(PDF): 指数分布的PDF公式为: $$ f(x) = \lambda e^{-\lambda x} \quad (x \geq 0) $$ 其中: $\lambda$ 是率参数(单位时间内事件发生的平均次数)。 $1/\lambda$ 是事件的平均间隔时间。 无记忆性:已经等待了时间 $t$,再等待额外时间 $s$ 的概率与从头开始等待 $s$ 的概率相同(即 $P(X > t+s \mid X > t) = P(X > s)$)。 例子: 假设某网站用户访问的间隔时间服从 $\lambda = 0.5$(平均每2分钟1次访问),则: PDF为 $f(x) = 0.5 e^{-0.5x}$。 用户在接下来1分钟内访问的概率是 $P(0 \leq X \leq 1) = \int_0^1 0.5 e^{-0.5x} dx \approx 0.393$。 高斯分布 高斯分布的概率密度函数: $$ \mathcal{f}(\mathcal )=\frac{1}{\sqrt{2\pi }\sigma }\exp \begin{pmatrix}-\frac{{(x-u)}^{2}}{2{\sigma }^{2}} \end{pmatrix} $$ x 在 μ-σ 和 μ+σ 之间的样本数量占到整个样本数量的 68.2%; x 在 μ-2σ 和 μ+2σ 之间的样本数量占到整个样本数量的 95.4%; x 在 μ-3σ 和 μ+3σ 之间的样本数量占到整个样本数量的99.6%; 数据融合 当前最优值=当前的先验估计值和观测值进行融合 我们通常会尝试最小化方差,以尽可能减小状态估计的不确定性,从而获得更可靠和准确的估计结果 拉普拉斯变换 拉普拉斯变换的定义 对于一个给定的时间域函数 ( f(t) ),其拉普拉斯变换 ( F(s) ) 定义为: $$ F(s) = \int_{0}^{\infty} e^{-st}f(t) \, dt $$ 这里的 ( s ) 是一个复数,通常写作 $ s = \sigma + j\omega $,其中 $\sigma$ 和 $ \omega $ 分别是实部和虚部。 拉普拉斯变换的作用 简化微分方程:拉普拉斯变换可以将微分方程转换为代数方程,从而简化求解过程。 系统分析:在控制理论中,拉普拉斯变换用来分析系统的稳定性和频率响应。 信号处理:在信号处理中,拉普拉斯变换帮助分析信号的频谱和系统的滤波特性。 例子:单一指数函数的拉普拉斯变换 假设有一个函数 $f(t) = e^{-at} $(其中 ( a ) 是一个正常数),我们想计算它的拉普拉斯变换。根据拉普拉斯变换的定义: $$ F(s) = \int_{0}^{\infty} e^{-st}e^{-at} \, dt = \int_{0}^{\infty} e^{-(s+a)t} \, dt $$ 这个积分可以解为: $$ F(s) = \begin{bmatrix} \frac{e^{-(s+a)t}}{-(s+a)} \end{bmatrix}_{0}^{\infty} = \frac{1}{s+a} $$ 因为当 $ t \to \infty $ 时,$ e^{-(s+a)t} $ 趋向于 0,前提是 $ Re(s+a) > 0 $(即 $s $ 的实部加 $ a $ 必须是正的)。 拉普拉斯矩阵 拉普拉斯矩阵及其性质 对于一个无向图 $G = (V, E)$,其拉普拉斯矩阵 $L$ 通常定义为 $$ L = D - A, $$ 其中: $D$是度矩阵,一个对角矩阵,其对角元 ($d_i$) 为顶点 $i$ 的度数; $A$是邻接矩阵,反映了图中各顶点之间的连接关系。 示例: 考虑一个简单的无向图,该图包含三个顶点:1, 2, 3,以及两条边: - 边 (1, 2) - 边 (2, 3) 邻接矩阵 (A) $$ A = \begin{pmatrix} 0 & 1 & 0 \\ 1 & 0 & 1 \\ 0 & 1 & 0 \end{pmatrix}. $$ 度矩阵 (D) $$ D = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}. $$ 拉普拉斯矩阵 (L) 将上面两个矩阵相减得到 $$ L = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix} - \begin{pmatrix} 0 & 1 & 0 \\ 1 & 0 & 1 \\ 0 & 1 & 0 \end{pmatrix} = \begin{pmatrix} 1 & -1 & 0 \\ -1 & 2 & -1 \\ 0 & -1 & 1 \end{pmatrix}. $$ 令常数向量 $$ \mathbf{1} = \begin{pmatrix} 1 \\ 1 \\ 1 \end{pmatrix}, $$ 则有 $$ L\mathbf{1} = \begin{pmatrix} 1 \cdot 1 + (-1) \cdot 1 + 0 \cdot 1 \\ -1 \cdot 1 + 2 \cdot 1 + (-1) \cdot 1 \\ 0 \cdot 1 + (-1) \cdot 1 + 1 \cdot 1 \end{pmatrix} = \begin{pmatrix} 1 - 1 + 0 \\ -1 + 2 - 1 \\ 0 - 1 + 1 \end{pmatrix} = \begin{pmatrix} 0 \\ 0 \\ 0 \end{pmatrix}. $$ 这说明常数向量 $\mathbf{1}$ 是 $L$ 的零空间中的一个向量,即零特征值对应的特征向量。 主要性质 对称性 由于对于无向图,邻接矩阵 (A) 是对称的,而度矩阵 (D) 本身也是对称的(因为它是对角矩阵),所以拉普拉斯矩阵 $L$ 也是对称矩阵。 正半定性 对于任意实向量 $x$,都有: $$ x^T L x = \sum_{(i,j) \in E} (x_i - x_j)^2 \ge 0. $$ 这说明 $L$ 是正半定矩阵,即其所有特征值均非负。 零特征值与连通分量 对于任意图,都有 $$ L \mathbf{1} = \mathbf{0}, $$ 其中 $\mathbf{1} = (1, 1, \ldots, 1)^T$,因此 $0$ 一定是 $L$ 的一个特征值。 因为拉普拉斯矩阵的定义为 $L = D - A$,其中每一行的元素之和为零,所以当向量所有分量都相等时,每一行的加权求和自然等于零。 更进一步,零特征值的重数等于图的连通分量(独立的子图)个数。也就是说,如果图 $G$ 有 $k$ 个连通分量,则 $L$ 的零特征值重数为 $k$ 。 简单证明思路 考虑图中每个连通分量,对于某个连通分量内的所有顶点,可以构造一个特征向量,使得在该连通分量中所有分量取相同常数,而在其他部分取零。由于该连通分量内部的任意两个顶点都是连通的,该特征向量满足 $Lx = 0$。这样,对于每个连通分量都可以构造出一个线性无关的零特征值特征向量,从而零特征值的重数至少为连通分量的数量;进一步证明可以证明重数不会超过这个数量。 谱分解及应用 由于 $L$ 是对称正半定矩阵,其可以进行谱分解: $$ L = U \Lambda U^T, $$ 其中$U$ 是**正交矩阵**,$\Lambda$ 是包含 $L$ 所有非负特征值的**对角矩阵**。 这一性质使得拉普拉斯矩阵在谱聚类、图分割等应用中非常有用。 总结 拉普拉斯矩阵 $L = D - A$是描述图结构的重要工具,具有如下主要性质: 对称性:$L$是对称矩阵; 正半定性:任意向量 $x$ 有 $x^T L x \ge 0$; 零特征值:$L$ 总有零特征值,且其重数与图的连通分量个数相等; 谱分解:$L$ 可进行正交谱分解,广泛应用于图的聚类与分割等领域。 这些性质不仅在理论上非常重要,而且在图论和数据分析等实际问题中有广泛的应用。 平均拉普拉斯矩阵: 归一化拉普拉斯矩阵 为了在某些应用中(例如谱聚类、图卷积网络等)获得更好的数值性质和归一化效果,我们可以构造 对称归一化拉普拉斯矩阵,记为 $L_{sym}$,定义为 $$ L_{sym} = D^{-1/2} L D^{-1/2} = I - D^{-1/2} A D^{-1/2}, $$ 其中 $D^{-1/2}$ 表示度矩阵的逆平方根, $I$ 为单位矩阵。 $$ D = \begin{pmatrix} 4 & 0 & 0 \\ 0 & 9 & 0 \\ 0 & 0 & 16 \end{pmatrix}. D^{-1/2} = \begin{pmatrix} \frac{1}{2} & 0 & 0 \ 0 & \frac{1}{3} & 0 \ 0 & 0 & \frac{1}{4} \end{pmatrix}. $$ 主要特点 归一化: 通过 $D^{-1/2}$ 的两侧预处理,将不同顶点的度数影响消除,使得矩阵在谱分解时能更好地反映图的结构。 对称性: $L_{sym}$ 是对称矩阵,这意味着它可以进行正交谱分解,其特征值均为实数。 谱性质: $L_{sym}$ 的特征值都位于区间 $[0, 2]$ 内。这一性质对于很多图论算法的稳定性和收敛性分析都非常重要。 Fiedler向量 根据谱分解理论,$L$ 的特征值满足 $$ x 0 = \lambda_1 \le \lambda_2 \le \cdots \le \lambda_n. $$ 其中,$\lambda_1 = 0$ 对应的特征向量通常为所有分量相同的常数向量。而 **Fiedler 向量** 就是对应于 $\lambda_2$ (第二小的特征值)的特征向量。 图的谱划分 构建图的拉普拉斯矩阵 根据给定的图结构,构建图的拉普拉斯矩阵 $L$。 计算 Fiedler 向量 求解拉普拉斯矩阵 $L$ 的第二小特征值对应的特征向量,即 Fiedler 向量。 根据 Fiedler 向量进行图划分 将 Fiedler 向量的元素按大小排序。 找到 Fiedler 向量元素值为 0 附近的分界点,将图划分为两个子图。 Fiedler 向量在连接紧密的顶点上的取值往往比较接近 $$ Fiedler 向量 :xv = \begin{pmatrix}0.8 \\0.7 \\0.6 \\-0.5 \\-0.6 \\-0.7\end{pmatrix}. $$ 正值部分:对应顶点 1, 2, 3; 负值部分:对应顶点 4, 5, 6。 经过这种划分后,通常会发现: 子图内部:顶点之间的连接较为紧密(边较多), 子图之间:连接较弱(边较少或只有一两条边)。 递归划分子图(可选) 对划分得到的两个子图,分别递归应用上述步骤(1-3步),进一步将其划分为更小的子图。 这样可以将原图层层划分为多个子图。 确定最终聚类结果 根据上述划分过程得到的多个子图,就对应了图的最终聚类结果。 每个子图内的节点被认为属于同一个聚类。 谱聚类 谱聚类的基本思想是通过图的特征向量将数据点映射到低维空间中,然后在这个低维空间中使用传统的聚类技术。 1.构造相似性图 数据表示: 给定数据点 ${x_1, x_2, \ldots, x_n}$。 相似性矩阵 $W$: 根据数据点之间的距离或相似性构造矩阵 $W$。常见方法包括: Gaussian 核函数: $$ W_{ij} = \exp\Bigl(-\frac{\|x_i - x_j\|^2}{2\sigma^2}\Bigr), $$ 只有当 $x_i$ 与 $x_j$ 彼此接近时, $W_{ij}$ 才较大;衡量数据点之间的距离并将其映射为一个 [0, 1] 之间的相似性值。 其中 $\sigma$ 为尺度参数,当 $\sigma$ 较小时,只有非常接近的数据点才会被认为是相似的 K近邻图:仅连接每个点与其 $k$ 个最近邻之间的边,其余 $W_{ij} = 0$。 2.构造图拉普拉斯矩阵 - 对称归一化拉普拉斯矩阵 - 未归一化的拉普拉斯矩阵 3.计算特征向量 对选定的拉普拉斯矩阵(例如 $L_{sym}$)进行特征分解,求出前 $k$ 个最小特征值对应的特征向量。 注意:对于未归一化的拉普拉斯矩阵,零特征值对应的特征向量通常是常数向量,所以在分解时忽略这个解,选择第二小开始的 $k$ 个特征向量。 4.构造嵌入空间 形成矩阵 $U$: 将求得的 $k$ 个特征向量作为列组成矩阵 $$ U = \begin{pmatrix} u_1(1) & u_2(1) & \cdots & u_k(1) \\ u_1(2) & u_2(2) & \cdots & u_k(2) \\ \vdots & \vdots & \ddots & \vdots \\ u_1(n) & u_2(n) & \cdots & u_k(n) \end{pmatrix}. $$ 其中,每一行对应原数据点在低维空间中的表示。 归一化(可选): 对于对称归一化的情况,可以对 $U$ 的每一行做归一化处理,使得每一行变为单位向量,这一步有助于后续聚类的稳定性。 5.聚类 使用 k-means 等传统聚类算法: 在低维嵌入空间中,每一行表示一个数据点的低维表示,然后对这些点进行聚类。 得到每个数据点对应的簇标签。 谱聚类示例(6个数据点分成3类) 假设数据点为 $$ x_1=1,\quad x_2=2,\quad x_3=5,\quad x_4=6,\quad x_5=10,\quad x_6=11. $$ 直观上我们希望将它们分为3类: 类1:靠近 1、2 类2:靠近 5、6 类3:靠近 10、11 1. 构造相似性矩阵 $W$ 采用 Gaussian 核函数 $$ W_{ij}=\exp\Bigl(-\frac{(x_i-x_j)^2}{2\sigma^2}\Bigr). $$ 取 $\sigma=2$(参数可调),则分母为 $2\sigma^2=8$。 计算部分相似性(近似值): $x_1,x_2: ; |1-2|^2=1,\quad W_{12}=\exp(-1/8)\approx0.8825.$ $x_1,x_3: ; |1-5|^2=16,\quad W_{13}=\exp(-16/8)=\exp(-2)\approx0.1353.$ $x_1,x_4: ; |1-6|^2=25,\quad W_{14}=\exp(-25/8)\approx0.0439.$ $x_1,x_5: ; |1-10|^2=81,\quad W_{15}=\exp(-81/8)\approx0.00004.$ $x_1,x_6: ; |1-11|^2=100,\quad W_{16}=\exp(-100/8)\approx0.00001.$ $x_2,x_3: ; |2-5|^2=9,\quad W_{23}=\exp(-9/8)\approx0.3247.$ $x_2,x_4: ; |2-6|^2=16,\quad W_{24}=\exp(-16/8)=\exp(-2)\approx0.1353.$ $x_2,x_5: ; |2-10|^2=64,\quad W_{25}=\exp(-64/8)=\exp(-8)\approx0.000335.$ $x_2,x_6: ; |2-11|^2=81,\quad W_{26}=\exp(-81/8)\approx0.00004.$ $x_3,x_4: ; |5-6|^2=1,\quad W_{34}=\exp(-1/8)\approx0.8825.$ $x_3,x_5: ; |5-10|^2=25,\quad W_{35}=\exp(-25/8)\approx0.0439.$ $x_3,x_6: ; |5-11|^2=36,\quad W_{36}=\exp(-36/8)=\exp(-4.5)\approx0.0111.$ $x_4,x_5: ; |6-10|^2=16,\quad W_{45}=\exp(-16/8)=\exp(-2)\approx0.1353.$ $x_4,x_6: ; |6-11|^2=25,\quad W_{46}=\exp(-25/8)\approx0.0439.$ $x_5,x_6: ; |10-11|^2=1,\quad W_{56}=\exp(-1/8)\approx0.8825.$ 由于 $W$ 是对称矩阵,对角元一般取 0(或1,根据需求),我们构造相似性矩阵 $W$ 为 $$ W=\begin{pmatrix} 0 & 0.8825 & 0.1353 & 0.0439 & 0.00004 & 0.00001 \\ 0.8825 & 0 & 0.3247 & 0.1353 & 0.000335& 0.00004 \\ 0.1353 & 0.3247 & 0 & 0.8825 & 0.0439 & 0.0111 \\ 0.0439 & 0.1353 & 0.8825 & 0 & 0.1353 & 0.0439 \\ 0.00004& 0.000335&0.0439 & 0.1353 & 0 & 0.8825 \\ 0.00001& 0.00004 & 0.0111 & 0.0439 & 0.8825 & 0 \end{pmatrix}. $$ 构造度矩阵 $D$ $$ D_{ii}=\sum_{j=1}^6 W_{ij}. $$ 近似计算: 对于 $x_1$: $D_{11}\approx0.8825+0.1353+0.0439+0.00004+0.00001\approx1.0617.$ 对于 $x_2$: $D_{22}\approx0.8825+0.3247+0.1353+0.000335+0.00004\approx1.3429.$ 对于 $x_3$: $D_{33}\approx0.1353+0.3247+0.8825+0.0439+0.0111\approx1.3975.$ 对于 $x_4$: $D_{44}\approx0.0439+0.1353+0.8825+0.1353+0.0439\approx1.241.$ 对于 $x_5$: $D_{55}\approx0.00004+0.000335+0.0439+0.1353+0.8825\approx1.0617.$ 对于 $x_6$: $D_{66}\approx0.00001+0.00004+0.0111+0.0439+0.8825\approx0.9375.$ 构造度矩阵: $$ D=\begin{pmatrix} 1.0617 & 0 & 0 & 0 & 0 & 0\\[0.5em] 0 & 1.3429 & 0 & 0 & 0 & 0\\[0.5em] 0 & 0 & 1.3975 & 0 & 0 & 0\\[0.5em] 0 & 0 & 0 & 1.2410 & 0 & 0\\[0.5em] 0 & 0 & 0 & 0 & 1.0617 & 0\\[0.5em] 0 & 0 & 0 & 0 & 0 & 0.9375 \end{pmatrix}. $$ 3. 构造拉普拉斯矩阵 $L$ 未归一化拉普拉斯矩阵定义为 $$ L = D - W. $$ 例如,矩阵的第 1 行为: $$ L_{1\cdot}=(1.0617,\ -0.8825,\ -0.1353,\ -0.0439,\ -0.00004,\ -0.00001), $$ 其它行类似。 4. 特征分解与构造低维嵌入 为了分成 3 类,通常我们取图拉普拉斯矩阵(或归一化拉普拉斯矩阵)的前 $k=3$ 个最小特征值对应的特征向量。 (注意:对于未归一化拉普拉斯矩阵,第一个特征值为 0,对应常数向量;但在归一化方法中,所有 3 个特征向量通常都有实际意义。) 假设经过特征分解后,我们得到了三个特征向量 $$ u_1,\; u_2,\; u_3, $$ 每个都是 6 维向量。将它们按列排列构成矩阵 $$ U=\begin{pmatrix} u_1(1) & u_2(1) & u_3(1) \\[0.3em] u_1(2) & u_2(2) & u_3(2) \\[0.3em] u_1(3) & u_2(3) & u_3(3) \\[0.3em] u_1(4) & u_2(4) & u_3(4) \\[0.3em] u_1(5) & u_2(5) & u_3(5) \\[0.3em] u_1(6) & u_2(6) & u_3(6) \end{pmatrix}. $$ 每一行 $i$ 表示数据点 $x_i$ 在 3 维低维嵌入空间中的表示。 假设得到的低维表示(示例数值): $x_1: ; (0.9,\ 0.2,\ 0.1)$ $x_2: ; (0.8,\ 0.3,\ 0.2)$ $x_3: ; (-0.1,\ 0.8,\ 0.1)$ $x_4: ; (-0.2,\ 0.7,\ 0.0)$ $x_5: ; (0.1,\ -0.2,\ 0.9)$ $x_6: ; (0.0,\ -0.1,\ 1.0)$ 5. 在低维空间上使用 k-means 聚类 利用 k-means 算法对 6 个数据点的 3 维向量进行聚类。 在本例中,k-means 会尝试将点分为 3 类。 根据上述低维表示,很容易看到: 数据点 $x_1$ 和 $x_2$ 聚在一起; 数据点 $x_3$ 和 $x_4$ 聚在一起; 数据点 $x_5$ 和 $x_6$ 聚在一起。 最终得到的聚类结果: 类1:${x_1, x_2}$ 类2:${x_3, x_4}$ 类3:${x_5, x_6}$ 谱分解与网络重构 一个对称矩阵可以通过其特征值和特征向量进行分解。对于一个 $n \times n$ 的对称矩阵 $A$,其谱分解可以表示为: $$ A = Q \Lambda Q^T \\ A = \sum_{i=1}^{n} \lambda_i x_i x_i^T $$ 其中,$\lambda_i$ 是矩阵 $A$ 的第 $i$ 个特征值,$x_i$ 是对应的特征向量。 推导过程 特征值和特征向量的定义 对于一个对称矩阵 $A$,其特征值和特征向量满足: $$ A x_i = \lambda_i x_i $$ 其中,$\lambda_i$ 是特征值,$x_i$ 是对应的特征向量。 谱分解 将这些特征向量组成一个正交矩阵 $Q$ $A = Q \Lambda Q^T$ $$ Q = \begin{bmatrix} x_1 & x_2 & \cdots & x_n \end{bmatrix}, $$ $$ Q \Lambda = \begin{bmatrix} \lambda_1 x_1 & \lambda_2 x_2 & \cdots & \lambda_n x_n \end{bmatrix}. $$ $$ Q \Lambda Q^T = \begin{bmatrix} \lambda_1 x_1 & \lambda_2 x_2 & \cdots & \lambda_n x_n \end{bmatrix} \begin{bmatrix} x_1^T \\ x_2^T \\ \vdots \\ x_n^T \end{bmatrix}. $$ $$ Q \Lambda Q^T = \lambda_1 x_1 x_1^T + \lambda_2 x_2 x_2^T + \cdots + \lambda_n x_n x_n^T. $$ 可以写为 $$ A = \sum_{i=1}^{n} \lambda_i x_i x_i^T. $$ 网络重构 在随机网络中,网络的邻接矩阵 $A$ 通常是对称的。利用预测算法得到的谱参数 ${\lambda_i, x_i}$ 后,就可以用以下公式重构网络矩阵: $$ A(G) = \sum_{i=1}^{n} \lambda_i x_i x_i^T $$ 幂迭代 幂迭代方法是一种常用的数值迭代算法,主要用于计算矩阵的主特征值(即具有最大模长的特征值)及其对应的特征向量。 收敛原理 在多数实际问题中,矩阵的特征值中存在一个绝对值最大的特征值。根据线性代数理论: 任取一个非零初始向量(且在主特征向量方向上的分量不为0) 通过不断与矩阵相乘并归一化,该向量会逐渐趋近于主特征向量方向 其他较小特征值对应的分量会被逐渐"削弱" 算法步骤 选取初始向量 选择非零初始向量 $$x^{(0)}$$ 迭代更新 每次迭代计算: $$ x^{(k+1)} = A x^{(k)} $$ 并进行二范数归一化以保持数值稳定性 收敛判断 当相邻迭代向量足够接近时停止,此时: 归一化向量 ≈ 主特征向量 特征值估计(瑞利商(Rayleigh Quotient)): $$ \lambda^{(k)} = \frac{(x^{(k)})^T A x^{(k)}}{(x^{(k)})^T x^{(k)}} $$ 瑞利商(Rayleigh Quotient)推导: 假设 $x$ 是 $A$ 的一个近似特征向量(比如幂迭代法得到的 $x^{(k)}$),我们希望找到一个标量 $\lambda$ 使得 $A x \approx \lambda x$。 为了找到最优的 $\lambda$,可以最小化残差 $| A x - \lambda x |^2$: $$ \| A x - \lambda x \|^2 = (A x - \lambda x)^T (A x - \lambda x) $$ 展开后: $$ = x^T A^T A x - 2 \lambda x^T A x + \lambda^2 x^T x $$ 对 $\lambda$ 求导并令导数为零: $$ \frac{d}{d\lambda} \| A x - \lambda x \|^2 = -2 x^T A x + 2 \lambda x^T x = 0 $$ 解得: $$ \lambda = \frac{x^T A x}{x^T x} $$ 这就是 **瑞利商** 的表达式: $$ \lambda^{(k)} = \frac{(x^{(k)})^T A x^{(k)}}{(x^{(k)})^T x^{(k)}} $$
科研
zy123
3月21日
0
7
0
2025-03-21
jupyter笔记本
jupyter笔记本 如何打开jupyter? 打开anaconda navigator图形界面,lauch jupyter 打开cmd 敲 jupyter notebook %matplotlib inline 使matplotlib绘制的图像嵌入在jupyter notebook单元格中 常用快捷键! esc进入命令模式 回车进入编辑模式 在命令模式下输入M可以进行markdown格式编写,输入Y可以进行python代码编写 shift+回车:运行当前代码块并跳转到下一块 ctrl+回车:只运行当前代码块 不跳转 B:往下增加一行代码块 A:往上新加一行代码块 DD:删除一行 L:标一个代码块内的行数 公式撰写 $$ x=\frac{-b\pm \sqrt{b^2-4_ac}}{2a} $$
自学
zy123
3月21日
0
0
0
2025-03-21
苍穹外卖
苍穹外卖 踩坑总结 项目简介 整体介绍 本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 小程序端应用 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。 1). 管理端功能 员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。 2). 用户端功能 微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。 技术选型 1). 用户层 本项目中在构建系统管理后台的前端页面,我们会用到H5、Vue.js、ElementUI、apache echarts(展示图表)等技术。而在构建移动端应用时,我们会使用到微信小程序。 2). 网关层 Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。 3). 应用层 SpringBoot: 快速构建Spring项目, 采用 "约定优于配置" 的思想, 简化Spring项目的配置开发。 SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。 Spring Task: 由Spring提供的定时任务框架。 httpclient: 主要实现了对http请求的发送。 Spring Cache: 由Spring提供的数据缓存框架 JWT: 用于对应用程序上的用户进行身份验证的标记。 阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。 Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。 POI: 封装了对Excel表格的常用操作。 WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。 4). 数据层 MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。 Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。 Mybatis: 本项目持久层将会使用Mybatis开发。 pagehelper: 分页插件。 spring data redis: 简化java代码操作Redis的API。 5). 工具 git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。 maven: 项目构建工具。 junit:单元测试工具,开发人员功能实现完毕后,需要通过junit对功能进行单元测试。 postman: 接口测工具,模拟用户发起的各类HTTP请求,获取对应的响应结果。 准备工作 //待完善,最后写一套本地java开发、nginx部署前端;服务器docker部署的方案!!! 前端环境搭建 1.构建和打包前端项目 npm run build 2.将构建文件复制到指定目录 Nginx 默认的静态文件根目录通常是 /usr/share/nginx/html,你可以选择将打包好的静态文件拷贝到该目录 或者使用自定义目录/var/www/my-frontend,并修改 Nginx 配置文件来指向这个目录。 3.配置 Nginx 打开 Nginx 的配置文件,通常位于 /etc/nginx/nginx.conf 以下是一个使用自定义目录 /var/www/my-frontend 作为站点根目录的示例配置: server { listen 80; server_name your-domain.com; # 如果没有域名可以使用 _ 或 localhost root /var/www/my-frontend; index index.html; location / { try_files $uri $uri/ /index.html; } } 4.启动或重启 Nginx sudo nginx -t # 检查配置是否正确 sudo systemctl restart nginx # 重启 Nginx 服务 5.访问前端项目 在浏览器中输入你配置的域名或服务器 IP 地址 后端环境搭建 工程的每个模块作用说明: 序号 名称 说明 1 sky-take-out maven父工程,统一管理依赖版本,聚合其他子模块 2 sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等 3 sky-pojo 子模块,存放实体类、VO、DTO等 4 sky-server 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 分析sky-common模块的每个包的作用: 名称 说明 constant 存放相关常量类 context 存放上下文类 enumeration 项目的枚举类存储 exception 存放自定义异常类 json 处理json转换的类 properties 存放SpringBoot相关的配置属性类 result 返回结果类的封装 utils 常用工具类 分析sky-pojo模块的每个包的作用: 名称 说明 Entity 实体,通常和数据库中的表对应 DTO 数据传输对象,通常用于程序中各层之间传递数据(接收从web来的数据) VO 视图对象,为前端展示数据提供的对象(响应给web) POJO 普通Java对象,只有属性和对应的getter和setter 分析sky-server模块的每个包的作用: 名称 说明 config 存放配置类 controller 存放controller类 interceptor 存放拦截器类 mapper 存放mapper接口 service 存放service类 SkyApplication 启动类 数据库初始化 执行sky.sql文件 序号 数据表名 中文名称 1 employee 员工表 2 category 分类表 3 dish 菜品表 4 dish_flavor 菜品口味表 5 setmeal 套餐表 6 setmeal_dish 套餐菜品关系表 7 user 用户表 8 address_book 地址表 9 shopping_cart 购物车表 10 orders 订单表 11 order_detail 订单明细表 Nginx 1.静态资源托管 直接高效地托管前端静态文件(HTML/CSS/JS/图片等)。 server { root /var/www/html; index index.html; location / { try_files $uri $uri/ /index.html; # 支持前端路由(如 React/Vue) } } try_files:按顺序尝试多个文件或路径,直到找到第一个可用的为止。 $uri:尝试直接访问请求的路径对应的文件(例如 /css/style.css)。 $uri/:尝试将路径视为目录(例如 /blog/ 会查找 /blog/index.html)。 /index.html:如果前两者均未找到,最终返回前端入口文件 index.html。 2.nginx 反向代理: 反向代理的好处: 提高访问速度 因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。 保证后端服务安全 因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。 统一入口解决跨域问题(无需后端配置 CORS)。 nginx 反向代理的配置方式: server{ listen 80; server_name localhost; location /api/{ proxy_pass http://localhost:8080/admin/; #反向代理 } } 监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。 3.负载均衡配置(默认是轮询) 将流量分发到多个后端服务器,提升系统吞吐量和容错能力。 upstream webservers{ server 192.168.100.128:8080; server 192.168.100.129:8080; } server{ listen 80; server_name localhost; location /api/{ proxy_pass http://webservers/admin;#负载均衡 } } 完整流程示例 用户访问:浏览器打开 http://yourdomain.com。 Nginx 返回静态文件:返回 index.html 和前端资源。 前端发起 API 请求:前端代码调用 /api/data。 Nginx 代理请求:将 /api/data 转发到 http://backend_server:3000/api/data。 后端响应:处理请求并返回数据,Nginx 将结果传回前端。 windows下更新nginx配置:先cd到nginx安装目录 nginx -t nginx -s reload APIFox 使用APIFox管理、测试接口、导出接口文档... 优势: 1.多格式支持 APIFox 能够导入包括 YApi 格式在内的多种接口文档,同时支持导出为 OpenAPI、Postman Collection、Markdown 和 HTML 等格式,使得接口文档在不同工具间无缝迁移和使用。 2.接口调试与 Mock 内置接口调试工具可以直接发送请求、查看返回结果,同时内置 Mock 服务功能,方便前后端联调和接口数据模拟,提升开发效率。 3.易用性与团队协作 界面直观、操作便捷,支持多人协作,通过分支管理和版本控制,团队成员可以并行开发并进行变更管理,确保接口维护有序。 迭代分支功能: 新建迭代分支,新增的待测试的接口在这里充分测试,没问题之后合并回主分支。 导出接口文档: 推荐导出数据格式为OpenAPI Spec,它是一种通用的 API 描述标准,Postman和APIFox都支持。 Swagger 使得前后端分离开发更加方便,有利于团队协作 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担 功能测试 使用: 1.导入 knife4j 的maven坐标 在pom.xml中添加依赖 <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> 2.在配置类中加入 knife4j 相关配置 WebMvcConfiguration.java /** * 通过knife4j生成接口文档 * @return */ @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; } 3.设置静态资源映射,否则接口文档页面无法访问 WebMvcConfiguration.java /** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } 4.访问测试 接口文档访问路径为 http://ip:port/doc.html ---> http://localhost:8080/doc.html 这是根据后端 Java 代码(通常是注解)自动生成接口文档,访问是通过后端服务的端口,这些文档最终会以静态文件的形式存在于 jar 包内,通常存放在 META-INF/resources/ 常用注解 通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下: 注解 说明 @Api 用在类上,例如Controller,表示对类的说明 @ApiModel 用在类上,例如entity、DTO、VO @ApiModelProperty 用在属性上,描述属性信息 @ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用 EmployeeLoginDTO.java @Data @ApiModel(description = "员工登录时传递的数据模型") public class EmployeeLoginDTO implements Serializable { @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; } EmployeeController.java @Api(tags = "员工相关接口") public class EmployeeController { @Autowired private EmployeeService employeeService; @Autowired private JwtProperties jwtProperties; /** * 登录 * * @param employeeLoginDTO * @return */ @PostMapping("/login") @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) { //.............. } } 实战开发 分页查询 传统员工分页查询分析: 采用分页插件PageHelper: 在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢? 分页插件帮我们完成了以下操作: 先获取到要执行的SQL语句: select * from emp 为了实现分页,第一步是获取符合条件的总记录数。分页插件将原始 SQL 查询中的 SELECT * 改成 SELECT count(*) select count(*) from emp; 一旦知道了总记录数,分页插件会将 SELECT * 的查询语句进行修改,加入 LIMIT 关键字,限制返回的记录数。 select * from emp limit ?, ? 第一个参数(?)是 起始位置,通常是 (当前页 - 1) * 每页显示的记录数,即从哪一行开始查询。 第二个参数(?)是 每页显示的记录数,即返回多少条数据。 执行分页查询,例如,假设每页显示 10 条记录,你请求第 2 页数据,那么 SQL 语句会变成: select * from emp limit 10, 10; 使用方法: 当使用 PageHelper 分页插件时,无需在 Mapper 中手动处理分页。只需在 Mapper 中编写常规的列表查询。 在 Service 层,调用 Mapper 方法之前,设置分页参数。 调用 Mapper 查询后,自动进行分页,并将结果封装到 PageBean 对象中返回。 1、在pom.xml引入依赖 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency> 2、EmpMapper @Mapper public interface EmpMapper { //获取当前页的结果列表 @Select("select * from emp") public List<Emp> list(); } 3、EmpServiceImpl 当调用 PageHelper.startPage(page, pageSize) 时,PageHelper 插件会拦截随后的 SQL 查询,自动修改查询,加入 LIMIT 子句来实现分页功能。 @Override public PageBean page(Integer page, Integer pageSize) { // 设置分页参数 PageHelper.startPage(page, pageSize); //page是页号,不是起始索引 // 执行分页查询 List<Emp> empList = empMapper.list(); // 获取分页结果 Page<Emp> p = (Page<Emp>) empList; //封装PageBean PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); return pageBean; } 4、Controller @Slf4j @RestController @RequestMapping("/emps") public class EmpController { @Autowired private EmpService empService; //条件分页查询 @GetMapping public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize) { //记录日志 log.info("分页查询,参数:{},{}", page, pageSize); //调用业务层分页查询功能 PageBean pageBean = empService.page(page, pageSize); //响应 return Result.success(pageBean); } } 条件分页查询 思路分析: <select id="pageQuery" resultType="com.sky.entity.Employee"> select * from employee <where> <if test="name != null and name != ''"> and name like concat('%',#{name},'%') </if> </where> order by create_time desc /select> 文件上传 阿里云OSS存储 pom文件中添加如下依赖: <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- no more than 2.3.3--> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency> 上传文件的工具类 /** * 阿里云 OSS 工具类 */ public class AliOssUtil { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; /** * 文件上传 * * @param bytes * @param objectName * @return */ public String upload(byte[] bytes, String objectName) { // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { // 创建PutObject请求。 ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes)); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } //文件访问路径规则 https://BucketName.Endpoint/ObjectName StringBuilder stringBuilder = new StringBuilder("https://"); stringBuilder .append(bucketName) .append(".") .append(endpoint) .append("/") .append(objectName); log.info("文件上传到:{}", stringBuilder.toString()); return stringBuilder.toString(); } } 自己搭建FileBrowser存储 public class FileBrowserUtil { private String domain; private String username; private String password; /** * —— 第一步:登录拿 token —— * 调用 /api/login 接口,返回纯 JWT 字符串,或 {"token":"..."} 结构 */ public String login() throws IOException { String url = domain + "/api/login"; try (CloseableHttpClient client = HttpClients.createDefault()) { HttpPost post = new HttpPost(url); post.setHeader("Content-Type", "application/json"); // 构造登录参数 JSONObject cred = new JSONObject(); cred.put("username", username); cred.put("password", password); post.setEntity(new StringEntity(cred.toString(), StandardCharsets.UTF_8)); try (CloseableHttpResponse resp = client.execute(post)) { int status = resp.getStatusLine().getStatusCode(); String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8).trim(); if (status >= 200 && status < 300) { // 如果返回 JSON 对象,则解析出 token 字段 if (body.startsWith("{") && body.endsWith("}")) { JSONObject obj = JSONObject.parseObject(body); String token = obj.getString("token"); log.info("登录成功,token={}", token); return token; } // 否则直接当成原始 JWT 返回 log.info("登录成功,token={}", body); return body; } else { log.error("Login failed: HTTP {} - {}", status, body); throw new IOException("Login failed: HTTP " + status); } } } } /** * —— 第二步:上传文件 —— * POST {domain}/api/resources/{encodedPath}?override=true * Header: X-Auth: token */ public String uploadFile(byte[] fileBytes, String fileName) throws IOException { String token = login(); String remotePath = "store/" + fileName; String encodedPath = URLEncoder .encode(remotePath, StandardCharsets.UTF_8) .replace("%2F", "/"); // 根据后缀猜 MIME 类型 String mimeType = URLConnection.guessContentTypeFromName(fileName); if (mimeType == null) { mimeType = "application/octet-stream"; } String uploadUrl = domain + "/api/resources/" + encodedPath + "?override=true"; try (CloseableHttpClient client = HttpClients.createDefault()) { HttpPost post = new HttpPost(uploadUrl); post.setHeader("X-Auth", token); post.setEntity(new ByteArrayEntity(fileBytes, ContentType.create(mimeType))); try (CloseableHttpResponse resp = client.execute(post)) { int status = resp.getStatusLine().getStatusCode(); String respBody = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8); if (status < 200 || status >= 300) { log.error("文件上传失败: HTTP {} - {}", status, respBody); throw new IOException("Upload failed: HTTP " + status); } log.info("文件上传成功,remotePath={}", remotePath); return remotePath; } } } /** * 第三步:生成公开分享链接 * 模拟浏览器的 POST /api/share/{encodedPath} 请求,body 为 "{}" */ public String createShareLink(String remotePath) throws IOException { String token = login(); // URL encode 并保留斜杠 String encodedPath = URLEncoder .encode(remotePath, StandardCharsets.UTF_8) .replace("%2F", "/"); String shareUrl = domain + "/api/share/" + encodedPath; log.info("准备创建分享链接,POST {}", shareUrl); try (CloseableHttpClient client = HttpClients.createDefault()) { HttpPost post = new HttpPost(shareUrl); post.setHeader("Cookie", "auth=" + token); post.setHeader("X-Auth", token); post.setHeader("Content-Type", "text/plain;charset=UTF-8"); post.setEntity(new StringEntity("{}", StandardCharsets.UTF_8)); try (CloseableHttpResponse resp = client.execute(post)) { int status = resp.getStatusLine().getStatusCode(); String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8).trim(); if (status < 200 || status >= 300) { log.error("创建分享失败 HTTP {} - {}", status, body); throw new IOException("Share failed: HTTP " + status); } // ========= 这里改为先检测是对象还是数组 ========= JSONObject json; if (body.startsWith("[")) { // 如果真返回数组(老版本可能是 [{...}]) json = JSONArray.parseArray(body).getJSONObject(0); } else if (body.startsWith("{")) { // 当前版本直接返回对象 json = JSONObject.parseObject(body); } else { throw new IOException("Unexpected share response: " + body); } String hash = json.getString("hash"); String publicUrl = domain + "/api/public/dl/" + hash + "/" + remotePath; log.info("创建分享链接成功:{}", publicUrl); return publicUrl; } } } /** * —— 整合示例:上传并立即返回分享链接 —— */ public String uploadAndGetUrl(byte[] fileBytes, String fileName) throws IOException { String remotePath = uploadFile(fileBytes, fileName); return createShareLink(remotePath); } } 数据库密码加密 加密存储确保即使数据库泄露,攻击者也不能轻易获取用户原始密码。 spring security中提供了一个加密类BCryptPasswordEncoder。 它采用哈希算法 SHA-256 +随机盐+密钥对密码进行加密。加密算法是一种可逆的算法,而哈希算法是一种不可逆的算法。 因为有随机盐的存在,所以相同的明文密码经过加密后的密码是不一样的,盐在加密的密码中是有记录的,所以需要对比的时候,springSecurity是可以从中获取到盐的 添加 spring-security-crypto 依赖,无需引入Spring Security 的认证、授权、过滤器链等其它安全组件! <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency> 添加配置 @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { // 参数 strength 为工作因子,默认为 10,这里可以根据需要进行调整 return new BCryptPasswordEncoder(10); } } 用户注册、加密 encode @Autowired private PasswordEncoder passwordEncoder; // 对密码进行加密 String encodedPassword = passwordEncoder.encode(rawPassword); 验证密码 matches // 使用 matches 方法来对比明文密码和存储的哈希密码 boolean judge= passwordEncoder.matches(rawPassword, user.getPassword()); BaseContext 如何获得当前登录的用户id? 方法:ThreadLocal ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。 每次请求代表一个线程!!!注:请求可以先经过拦截器,再经过controller=>service=>mapper,都是在一个线程里。而即使同一个用户,先用两次请求/login、 /upload,它们也不处于同一线程中! public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } } 实现方式:登录成功 -> 生成jwt令牌 (claims中存userId)->前端浏览器保存 后续每次请求携带jwt -> 拦截器检查jwt令牌 -> BaseContext.setCurrentId(jwt中取出的userId); -> BaseContext.getCurrentId(); //service层中获取当前userId 全局异常处理 新增员工时的问题 录入的用户名已存,抛出的异常后没有处理。 法一:每次新增员工前查询一遍数据库,保证无重复username再插入。 法二:插入后系统报“Duplicate entry”再处理。 「乐观策略」减少不必要的查询,只有在冲突时才抛错。 解决方法:定义全局异常处理器 @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获业务异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(BaseException ex){ log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ //Duplicate entry 'zhangsan' for key 'employee.idx_username' String message = ex.getMessage(); log.info(message); if(message.contains("Duplicate entry")){ String[] split = message.split(" "); String username = split[2]; String msg = username + MessageConstant.ALREADY_EXISTS; return Result.error(msg); }else{ return Result.error(MessageConstant.UNKNOWN_ERROR); } } } SQLIntegrityConstraintViolationException用来捕获各种 完整性约束冲突,如唯一/主键约束冲突(向一个设置了 UNIQUE 或者 PRIMARY KEY 的字段重复插入相同的值。)。可以捕捉username重复异常。并以Result.error(msg)返回通用响应。 另外,自定义一个异常BaseException与若干个业务层异常, public class BaseException extends RuntimeException { public BaseException() { } public BaseException(String msg) { super(msg); } } 业务层抛异常: throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED); 这样抛出异常之后可以被全局异常处理器 exceptionHandler(BaseException ex) 捕获。 SpringMVC的消息转换器(处理日期) Jackson 是一个用于处理 JSON 数据 的流行 Java 库,主要用于: 序列化:将 Java 对象转换为 JSON 字符串(例如:Java对象 → {"name":"Alice"})。Controller 返回值上带有 @ResponseBody 或者使用 @RestController 反序列化:将 JSON 字符串解析为 Java 对象(例如:{"name":"Alice"} → Java对象)。方法参数上标注了 @RequestBody Spring Boot默认集成了Jackson 1). 方式一 在属性上加上注解,对日期进行格式化 但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。 2). 方式二(推荐 ) 在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对LocalDateTime、LocalDate、LocalTime进行格式处理 /** * 扩展Spring MVC框架的消息转化器 * @param converters */ protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..."); //创建一个消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据 converter.setObjectMapper(new JacksonObjectMapper()); //将自己的消息转化器加入容器中,确保覆盖默认的 Jackson 行为 converters.add(0,converter); } JacksonObjectMapper()文件: //直接复用 Jackson 的核心功能,仅覆盖或扩展特定行为。 public class JacksonObjectMapper extends ObjectMapper { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; //LocalDate public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; //LocalDateTime // public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; //LocalTime public JacksonObjectMapper() { super(); //收到未知属性时不报异常 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时,属性不存在的兼容处理 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); //注册功能模块 例如,可以添加自定义序列化器和反序列化器 this.registerModule(simpleModule); } } 数据库操作代码复用 为提高代码复用率,在 Mapper 层统一定义一个通用的 update 方法,利用 MyBatis 的动态 SQL,根据传入的 Employee 对象中非空字段生成对应的 SET 子句。这样: 启用/禁用员工:只需在业务层调用(如 startOrStop),传入带有 id 和 status 的 Employee 实例,底层自动只更新 status 字段。 更新员工信息:调用(如 updateEmployee)时,可传入包含多个属性的 Employee 实例,自动更新那些非空字段。 Controller 层和 Service 层的方法命名可根据不同业务场景进行区分,底层均复用同一个 update方法 在 EmployeeMapper 接口中声明 update 方法: /** * 根据主键动态修改属性 * @param employee */ void update(Employee employee); 在 EmployeeMapper.xml 中编写SQL: <update id="update" parameterType="Employee"> update employee <set> <if test="name != null">name = #{name},</if> <if test="username != null">username = #{username},</if> <if test="password != null">password = #{password},</if> <if test="phone != null">phone = #{phone},</if> <if test="sex != null">sex = #{sex},</if> <if test="idNumber != null">id_Number = #{idNumber},</if> <if test="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update> 操作多表时的规范操作 功能:实现批量删除套餐操作,只能删除'非起售中'的套餐,关联表有套餐表和套餐菜品表。 代码1: @Transactional public void deleteBatch(Long[] ids) { for(Long id:ids){ Setmeal setmeal = setmealMapper.getById(id); if(StatusConstant.ENABLE == setmeal.getStatus()){ //起售中的套餐不能删除 throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE); } else{ Long setmealId=id; setmealMapper.deleteById(id); setmeal_dishMapper.deleteByDishId(id); } } } 代码2: @Transactional public void deleteBatch(List<Long> ids) { ids.forEach(id -> { Setmeal setmeal = setmealMapper.getById(id); if (StatusConstant.ENABLE == setmeal.getStatus()) { //起售中的套餐不能删除 throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE); } }); ids.forEach(setmealId -> { //删除套餐表中的数据 setmealMapper.deleteById(setmealId); //删除套餐菜品关系表中的数据 setmealDishMapper.deleteBySetmealId(setmealId); }); } 代码2更好,因为: 1.把「验证」逻辑和「删除」逻辑分成了两段,职责更单一,读代码的时候一目了然 2.避免不必要的删除操作,第一轮只做 getById 校验,碰到 起售中 马上抛异常,从不执行任何删除 SQL,效率更高。 @Transactional 最典型的场景就是:在同一个业务方法里要执行多条数据库操作(增删改),而且这些操作必须保证“要么都成功、要么都失败” 时,用它来把这些 SQL 语句包裹在同一个事务里,遇到运行时异常就回滚,避免出现“删到一半、中途抛错”导致的数据不一致。也就是说,同时操作多表时,都在方法上加下这个注解! 公共字段自动填充——AOP编程 在数据库操作中,通常需要为某些公共字段(如创建时间、更新时间等)自动赋值。采用AOP: 统一管理这些字段的赋值逻辑 避免在业务代码中重复设置 确保数据一致性 序号 字段名 含义 数据类型 操作类型 1 create_time 创建时间 datetime insert 2 create_user 创建人id bigint insert 3 update_time 修改时间 datetime insert、update 4 update_user 修改人id bigint insert、update 实现步骤: 1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法 2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值 客户端 → Service → Mapper接口方法(带@AutoFill) ↓ 切面触发 Before 通知(AutoFillAspect.autoFill) [1] 读取注解,确定 INSERT/UPDATE [2] 从 BaseContext 拿到 currentId [3] 反射调用 entity.setXxx() ↓ 切面执行完毕,回到原方法 Mapper 执行动态 SQL,将已填充的字段写入数据库 3). 在 需要统一填充的Mapper 的方法上加入 AutoFill 注解 **技术点:**枚举、注解、AOP、反射 HttpClient HttpClient作用: 在Java程序中发送HTTP请求 接收响应数据 HttpClient的maven坐标: <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency> HttpClient的核心API: HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。 HttpClients:可认为是构建器,可创建HttpClient对象。 CloseableHttpClient:实现类,实现了HttpClient接口。 HttpGet:Get方式请求类型。 HttpPost:Post方式请求类型。 HttpClient发送请求步骤: 创建HttpClient对象 创建Http请求对象 调用HttpClient的execute方法发送请求 测试用例 @SpringBootTest public class HttpClientTest { /** * 测试通过httpclient发送GET方式的请求 */ @Test public void testGET() throws Exception{ //创建httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); //创建请求对象 HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status"); //发送请求,接受响应结果 CloseableHttpResponse response = httpClient.execute(httpGet); //获取服务端返回的状态码 int statusCode = response.getStatusLine().getStatusCode(); System.out.println("服务端返回的状态码为:" + statusCode); HttpEntity entity = response.getEntity(); String body = EntityUtils.toString(entity); System.out.println("服务端返回的数据为:" + body); //关闭资源 response.close(); httpClient.close(); } @Test public void testPOST() throws Exception{ // 创建httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); //创建请求对象 HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login"); JSONObject jsonObject = new JSONObject(); jsonObject.put("username","admin"); jsonObject.put("password","123456"); StringEntity entity = new StringEntity(jsonObject.toString()); //指定请求编码方式 entity.setContentEncoding("utf-8"); //数据格式 entity.setContentType("application/json"); httpPost.setEntity(entity); //发送请求 CloseableHttpResponse response = httpClient.execute(httpPost); //解析返回结果 int statusCode = response.getStatusLine().getStatusCode(); System.out.println("响应码为:" + statusCode); HttpEntity entity1 = response.getEntity(); String body = EntityUtils.toString(entity1); System.out.println("响应数据为:" + body); //关闭资源 response.close(); httpClient.close(); } } 微信小程序 步骤分析: 小程序端,调用wx.login()获取code,就是授权码。 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。 小程序端,收到自定义登录态,存储storage。 小程序端,后绪通过wx.request()发起业务请求时,携带token。 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id(无需获取openai,因为token中存了userid可以确认用户身份)。 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。 缓存功能 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。 实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 经典缓存实现代码 缓存逻辑分析: 每个分类下的菜品保存一份缓存数据 数据库中菜品数据有变更时清理缓存数据 @Autowired private RedisTemplate redisTemplate; /** * 根据分类id查询菜品 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list(Long categoryId) { //构造redis中的key,规则:dish_分类id String key = "dish_" + categoryId; //查询redis中是否存在菜品数据 List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if(list != null && list.size() > 0){ //如果存在,直接返回,无须查询数据库 return Result.success(list); } //////////////////////////////////////////////////////// Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 //如果不存在,查询数据库,将查询到的数据放入redis中 list = dishService.listWithFlavor(dish); //////////////////////////////////////////////////////// redisTemplate.opsForValue().set(key, list); return Result.success(list); } 为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。 需要改造的方法: 新增菜品 修改菜品 批量删除菜品 起售、停售菜品 清理缓冲方法: private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); } Spring Cache框架实现缓存 Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version> </dependency> 在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个: 注解 说明 @EnableCaching 开启缓存注解功能,通常加在启动类上 @Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据(取);如果没有缓存数据,调用方法并将方法返回值放到缓存中 @CachePut 将方法的返回值放到缓存中 @CacheEvict 将一条或多条数据从缓存中删除 1)@CachePut 说明: 作用: 将方法返回值,放入缓存 value: 缓存的名称, 每个缓存名称下面可以有很多key key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法 value 和 cacheNames 属性在用法上是等效的。它们都用来指定缓存区的名称 在Redis中并没有直接的“缓存名”概念,而是通过键(key)来访问数据。Spring Cache通过cacheNames属性来模拟不同的“缓存区”,实际上这是通过将这些名称作为键的一部分来实现的。例如,如果你有一个缓存名为 userCache,那么所有相关的缓存条目的键可能以 "userCache::" 开头。 在save方法上加注解@CachePut @PostMapping @CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1 public User save(@RequestBody User user){ userMapper.insert(user); return user; } **说明:**key的写法如下 #user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ; #result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ; 2)@Cacheable 说明: 作用: 在方法执行前,spring先查看缓存中是否有指定的key的数据,如果有数据,则直接返回缓存数据,不执行后续sql操作;若没有数据,调用方法并将方法返回值放到缓存中。 在getById上加注解@Cacheable @GetMapping @Cacheable(cacheNames = "userCache",key="#id") public User getById(Long id){ User user = userMapper.getById(id); return user; } 3)@CacheEvict 说明: 作用: 清理指定缓存 在 delete 方法上加注解@CacheEvict @DeleteMapping @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据 public void deleteById(Long id){ userMapper.deleteById(id); } @DeleteMapping("/delAll") @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据 public void deleteAll(){ userMapper.deleteAll(); } **总结:**新增数据的时候->添加缓存@CachePut ; 查询的时候->判断有无缓存@Cacheable; 删除的时候->删除缓存@CacheEvict。 Spring Cache是经典缓存的上位替代!!! 注意,如果缓存的是套餐分类,即一个套餐分类中含有多个套餐,那么在新增套餐的时候,需要清除相应的套餐分类缓存,因为当你新增一个属于分类 5 的套餐时,原来缓存里那份「分类 5 的列表」已经不再完整──它少了新加的那个套餐。 但是如果缓存的就是套餐本身,新增套餐的时候就可以直接缓存套餐。不要混淆两者! 下单支付 下单 表名 含义 说明 orders 订单表 主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等) order_detail 订单明细表 主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息) 微信支付 官方文档:https://pay.weixin.qq.com/static/product/product_index.shtml 商户系统调用微信后台: **JSAPI下单:**商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步) 微信小程序调起支付: 通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步) 内网穿透 微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。 目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。 解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。 1)下载地址:https://dashboard.cpolar.com/get-started 2). cpolar指定authtoken 复制authtoken: 执行命令: 注意,cd到cpolar.exe所在的目录打开cmd 输入代码: cpolar.exe authtoken ZmIwMmQzZDYtZDE2ZS00ZGVjLWE2MTUtOGQ0YTdhOWI2M2Q1 3)获取临时域名 cpolar.exe http 8080 这里的 https://52ac2ecb.r18.cpolar.top 就是与http://localhost:8080对应的临时域名。 原理: 客户端向 cpolar 的中转节点发起 出站(outbound)连接,完成身份认证(authtoken),并在连接上报出要映射的本地端口,比如 HTTP 的 8080 中转节点分配一个公网端点,如abcd1234.cpolar.com 外部用户 访问 http://abcd1234.cpolar.com,落到 cpolar 的中转节点,中转节点 通过先前建立好的持久隧道,把流量转发到你本地运行的客户端。 Spring Task Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。 **定位:**定时任务框架 **作用:**定时自动执行某段Java代码 1.2 cron表达式 cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间 **构成规则:**分为6或7个域,由空格分隔开,每个域代表一个含义 每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选) cron表达式在线生成器:https://cron.qqe2.com/ 1.3 入门案例 1.3.1 Spring Task使用步骤 1). 导入maven坐标 spring-context(已存在) 2). 启动类添加注解 @EnableScheduling 开启任务调度 3). 自定义定时任务类 2.订单状态定时处理 2.1 需求分析 用户下单后可能存在的情况: 下单后未支付,订单一直处于**“待支付”**状态 用户收货后管理端未点击完成按钮,订单一直处于**“派送中”**状态 对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为: 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消” 通过定时任务每天凌晨1点(打烊后)检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成” @Component @Slf4j public class OrderTask { /** * 处理下单之后未15分组内支付的超时订单 */ @Autowired private OrderMapper orderMapper; @Scheduled(cron = "0 * * * * ? ") public void processTimeoutOrder(){ log.info("定时处理支付超时订单:{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); // select * from orders where status = 1 and order_time < 当前时间-15分钟 List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time); if(ordersList != null && ordersList.size() > 0){ ordersList.forEach(order -> { order.setStatus(Orders.CANCELLED); order.setCancelReason("支付超时,自动取消"); order.setCancelTime(LocalDateTime.now()); orderMapper.update(order); }); } } @Scheduled(cron = "0 0 1 * * ?") public void processDeliveryOrder() { log.info("处理派送中订单:{}", new Date()); // select * from orders where status = 4 and order_time < 当前时间-1小时 LocalDateTime time = LocalDateTime.now().plusMinutes(-60); List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time); if (ordersList != null && ordersList.size() > 0) { ordersList.forEach(order -> { order.setStatus(Orders.COMPLETED); orderMapper.update(order); }); } } } Websocket WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。 HTTP协议和WebSocket协议对比: HTTP是短连接 WebSocket是长连接 HTTP通信是单向的,基于请求响应模式 WebSocket支持双向通信 HTTP和WebSocket底层都是TCP连接 入门案例 实现步骤: 1). 直接使用websocket.html页面作为WebSocket客户端 2). 导入WebSocket的maven坐标 3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信(比较固定,建立连接、接收消息、关闭连接、发送消息) 4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件 它通过Spring的 ServerEndpointExporter 将使用 @ServerEndpoint 注解的类自动注册为WebSocket端点。这样,当应用程序启动时,所有带有 @ServerEndpoint 注解的类就会被Spring容器自动扫描并注册为WebSocket服务器端点,使得它们能够接受和处理WebSocket连接。 5). 导入定时任务类WebSocketTask,定时向客户端推送数据 来单提醒 设计思路: 通过WebSocket实现管理端页面和服务端保持长连接状态 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content type 为消息类型,1为来单提醒 2为客户催单 orderId 为订单id content 为消息内容
自学
zy123
3月21日
0
1
0
2025-03-21
安卓开发
安卓开发 导入功能模块心得 最近想在我的书城中开发一下阅读器的功能,难度颇高,因此在github上找到了一个封装了阅读器功能的项目,仅需获得文件本地存储地址,调用其提供的函数即可进行阅读。 但是,github介绍的使用方法并不总是有效,比如我就经常无法正确添加依赖 因此,我将其项目代码拷贝到本地,手动集成。 依据项目结构,可以发现app是主项目,hwtxtreaderlib是功能模块,根据是这张图: build.gradle(:app)中引入了hwtxtreaderlib的依赖,而app只是个demo测试模块,相当于演示了如果在自己的项目中引用hwtxtreaderlib。因此,手动步骤如下: 将hwtxtreaderlib复制到自己的项目文件夹中 在app的build.gradle中,添加依赖 implementation project(':hwtxtreaderlib') 在settings.gradle中,设置项目包括的模块 include ':app', ':hwtxtreaderlib' syn now! 同步一下,然后android studio中项目结构变成如下图 build没报错基本就稳了,然后就运行试试 这里可能AndroidManifest.xml报错,需要查看原项目中app模块如何编写的,做些适当的修改!我这里卡了很久. **非常重要!!!**有时候github项目会将项目的详细信息写在wiki中!!!
自学
zy123
3月21日
0
0
0
2025-03-21
Redis
Redis Redis基本定义 Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件。 Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。它存储的value类型比较丰富,也被称为结构化的NoSql数据库(非关系型)。 典型场景: 热点数据缓存(商品/资讯/秒杀) 会话管理(Session) 排行榜/计数器 消息队列(Stream) NoSQL数据库: 键值型(Redis) 文档型(MongoDB) 列存储(HBase) 图数据库(Neo4j) 下载与使用 Redis安装包分为windows版和Linux版: Windows版下载地址:https://github.com/microsoftarchive/redis/releases Linux版下载地址: https://download.redis.io/releases/ 角色 作用 典型示例 Redis 服务端 数据存储的核心,负责接收/执行命令、管理内存、持久化数据等。需先启动服务端才能使用。 redis-server(默认监听 6379 端口) Redis 客户端 连接服务端并发送命令(如 GET/SET),获取返回结果。可以是命令行工具或代码库。 redis-cli、Java 的 Jedis 库 windows下服务启动/停止: 启动: redis-server.exe redis.windows.conf 这种方式关闭命令行后Redis服务又停止了! 解决方法:安装为 Windows 服务 redis-server --service-install redis.windows.conf --service-name Redis redis-server --service-start 停止: ctrl+c 客户端连接: 直接运行 redis-cli.exe 时,它会尝试连接 本机(127.0.0.1)的 Redis 服务端,并使用默认端口 6379。 等价于手动指定参数: redis-cli -h 127.0.0.1 -p 6379 指定连接: redis-cli -h <IP> -p <端口> -a <密码> redis-cli -h 192.168.1.100 -p 6379 -a yourpassword 退出连接:exit 修改Redis配置文件 设置Redis服务密码,修改redis.windows.conf(windows) redis.conf(linux) requirepass 123456 修改redis服务端口 port 6379 Redis客户端图形工具 默认提供的客户端连接工具界面不太友好,可以使用Another Redis Desktop Manager.exe ,类似Navicat连mysql。 Redis数据类型 Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型: 字符串(string):普通字符串,Redis中最简单的数据类型 哈希(hash):也叫散列,类似于Java中的HashMap结构。(套娃) 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList 集合(set):无序集合,没有重复元素,类似于Java中的HashSet(求交集) 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素(排行榜) Redis常用命令 通用命令 Redis的通用命令是不分数据类型的,都可以使用的命令: KEYS pattern 查找所有符合给定模式( pattern)的 key pattern:匹配模式,支持通配符: *:匹配任意多个字符(包括空字符) ?:匹配单个字符 [abc]:匹配 a、b 或 c 中的任意一个字符 [a-z]:匹配 a 到 z 之间的任意一个字符 KEYS user:* #可以返回 形如 "user:1" "user:2" 的 key KEYS * #查找所有key EXISTS key 检查给定 key 是否存在 TYPE key 返回 key 所储存的值的类型 DEL key 该命令用于在 key 存在是删除 key 字符串 Redis 中字符串类型常用命令: SET key value 设置指定key的值 GET key 获取指定key的值 SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒(验证码) SETNX key value 只有在 key不存在时设置 key 的值 哈希操作 Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令: HSET key field value 将哈希表 key 中的字段 field 的值设为 value HGET key field 获取存储在哈希表中指定字段的值 HDEL key field 删除存储在哈希表中的指定字段 HKEYS key 获取哈希表中所有字段 HVALS key 获取哈希表中所有值 列表操作 Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令: LPUSH key value1 将一个值插入到列表头部 RPUSH key value1 [value2] 将一个或多个值插入到列表尾部 RPUSH mylist "world" "redis" "rpush" #多个值插入 LRANGE key start stop 获取列表指定范围内的元素(这里L代表List 不是Left) LRANGE mylist 0 -1 #获取整个列表 LLEN key 获取列表长度 RPOP key 移除并获取列表最后一个元素 BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止 集合操作 Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令: SADD key member1 [member2] 向集合添加一个或多个成员 # 添加单个成员 SADD fruits "apple" # 添加多个成员(自动去重) SADD fruits "banana" "orange" "apple" # "apple" 已存在,不会重复添加 SMEMBERS key 返回集合中的所有成员 SCARD key 获取集合的成员数(基数Cardinality) SINTER key1 [key2] 返回给定所有集合的交集 SUNION key1 [key2] 返回所有给定集合的并集 SREM key member1 [member2] 移除集合中一个或多个成员 有序集合 Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令: 常用命令: ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员(score代表分数) ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员 ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment ZREM key member [member ...] 移除有序集合中的一个或多个成员 Java中操作Redis Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,就如同我们使用JDBC操作MySQL数据库一样。 网址:https://spring.io/projects/spring-data-redis 环境搭建 进入到sky-server模块 1). 导入Spring Data Redis的maven坐标(已完成) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 2). 配置Redis数据源 在application-dev.yml中添加 sky: redis: host: localhost port: 6379 password: 123456 database: 10 解释说明: database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。 可以通过修改Redis配置文件来指定数据库的数量。 在application.yml中添加读取application-dev.yml中的相关Redis配置 spring: profiles: active: dev redis: host: ${sky.redis.host} port: ${sky.redis.port} password: ${sky.redis.password} database: ${sky.redis.database} 3). 编写配置类,创建RedisTemplate对象 @Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象..."); RedisTemplate redisTemplate = new RedisTemplate(); //设置redis的连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } } 解释说明: 当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为 JdkSerializationRedisSerializer,存到 Redis 里会是二进制格式,这里把 key 的序列化器指定为 StringRedisSerializer,就会把所有的 key 都以 UTF‑8 字符串形式写入 Redis,方便观测和调试 功能测试 通过RedisTemplate对象操作Redis,注意要Another Redis Desktop中刷新一下数据库,才能看到数据。 字符串测试 @DataRedisTest @Import(com.sky.config.RedisConfiguration.class) public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate(){ System.out.println(redisTemplate); } @Test public void testString(){ // SET city "北京" redisTemplate.opsForValue().set("city", "北京"); // GET city String city = (String) redisTemplate.opsForValue().get("city"); System.out.println(city); // SETEX code 180 "1234" (3 分钟 = 180 秒) redisTemplate.opsForValue().set("code", "1234", 3, TimeUnit.MINUTES); // SETNX lock "1" redisTemplate.opsForValue().setIfAbsent("lock", "1"); // (由于上一步 lock 已存在,这里相当于不执行) // SETNX lock "2" redisTemplate.opsForValue().setIfAbsent("lock", "2"); } } 对于非String类型: 写入时序列化 调用 opsForValue().set(key, value) 时,value(比如 List<DishVO>)会先走一遍 ValueSerializer。 默认是 JdkSerializationRedisSerializer,它用 Java 的 ObjectOutputStream 把整个对象图打包成一个 byte[]。 这个 byte[] 直接就存成了 Redis 的 String 值。 读取时反序列化 调用 opsForValue().get(key) 时,RedisTemplate 拿回那段 byte[],再用同一个 JDK 序列化器(ObjectInputStream)把它变回 List<DishVO>。 哈希测试 @Test public void testHash(){ HashOperations hashOperations = redisTemplate.opsForHash(); // HSET 100 "name" "tom" hashOperations.put("100", "name", "tom"); // HSET 100 "age" "20" hashOperations.put("100", "age", "20"); // HGET 100 "name" String name = (String) hashOperations.get("100", "name"); System.out.println(name); // HKEYS 100 Set keys = hashOperations.keys("100"); System.out.println(keys); // HVALS 100 List values = hashOperations.values("100"); System.out.println(values); // HDEL 100 "age" hashOperations.delete("100", "age"); } get获得的是Object类型,keys获得的是set类型,values获得的是List 3). 操作列表类型数据 @Test public void testList(){ ListOperations listOperations = redisTemplate.opsForList(); // LPUSH mylist "a" "b" "c" listOperations.leftPushAll("mylist", "a", "b", "c"); // LPUSH mylist "d" listOperations.leftPush("mylist", "d"); // LRANGE mylist 0 -1 List mylist = listOperations.range("mylist", 0, -1); System.out.println(mylist); // RPOP mylist listOperations.rightPop("mylist"); // LLEN mylist Long size = listOperations.size("mylist"); System.out.println(size); } 4). 操作集合类型数据 @Test public void testSet(){ SetOperations setOperations = redisTemplate.opsForSet(); // SADD set1 "a" "b" "c" "d" setOperations.add("set1", "a", "b", "c", "d"); // SADD set2 "a" "b" "x" "y" setOperations.add("set2", "a", "b", "x", "y"); // SMEMBERS set1 Set members = setOperations.members("set1"); System.out.println(members); // SCARD set1 Long size = setOperations.size("set1"); System.out.println(size); // SINTER set1 set2 Set intersect = setOperations.intersect("set1", "set2"); System.out.println(intersect); // SUNION set1 set2 Set union = setOperations.union("set1", "set2"); System.out.println(union); // SREM set1 "a" "b" setOperations.remove("set1", "a", "b"); } 5). 操作有序集合类型数据 @Test public void testZset(){ ZSetOperations zSetOperations = redisTemplate.opsForZSet(); // ZADD zset1 10 "a" zSetOperations.add("zset1", "a", 10); // ZADD zset1 12 "b" zSetOperations.add("zset1", "b", 12); // ZADD zset1 9 "c" zSetOperations.add("zset1", "c", 9); // ZRANGE zset1 0 -1 Set zset1 = zSetOperations.range("zset1", 0, -1); System.out.println(zset1); // ZINCRBY zset1 10 "c" zSetOperations.incrementScore("zset1", "c", 10); // ZREM zset1 "a" "b" zSetOperations.remove("zset1", "a", "b"); } 6). 通用命令操作 * 匹配零个或多个字符。 ? 匹配任何单个字符。 [abc] 匹配方括号内的任一字符(本例中为 'a'、'b' 或 'c')。 [^abc] 或 [!abc] 匹配任何不在方括号中的单个字符。 @Test public void testCommon(){ // KEYS * Set keys = redisTemplate.keys("*"); System.out.println(keys); // EXISTS name Boolean existsName = redisTemplate.hasKey("name"); System.out.println("name exists? " + existsName); // EXISTS set1 Boolean existsSet1 = redisTemplate.hasKey("set1"); System.out.println("set1 exists? " + existsSet1); for (Object key : keys) { // TYPE <key> DataType type = redisTemplate.type(key); System.out.println(key + " -> " + type.name()); } // DEL mylist redisTemplate.delete("mylist"); }
自学
zy123
3月21日
0
0
0
上一页
1
...
5
6
7
...
10
下一页