优雅使用Windows 10
PriorityQueue与Comparator
PriorityQueue的使用场景
1 | Queue<Integer> queue1 = new PriorityQueue<>(); |
Java中的PriorityQueue常用于实现小顶堆、大顶堆。由上述代码可见,通常有默认无参构造函数、传入Comparator对象等方式的实现。
困惑
从构造函数层面,完全不清楚默认构造函数的排序规则,也完全不明白Comparator实现的comparingInt方法对应的排序规则。
根据网上绝大多数文章的描述,都没有解释或者只是说明了对应的排序规则,但是不知道为什么是对应升序或降序。通过阅读源码,才得到准确的认知。
阅读源码
PriorityQueue构造函数、增删元素
可以看到,无参构造函数和指定了Comparator的构造函数实际都是指向PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
构造函数。而comparator发挥作用的时候,体现在PriorityQueue增删元素的方法中,以offer方法为例:
根据构造函数是否传入了Comparator对象,元素的比较分别执行siftUpUsingComparator
和siftUpComparable
方法,两个方法的逻辑一致,只是Comparator
通常作为外部排序工具,Comparable
作为对象的内部比较方式。
- 修改范围:
Comparable
通常作为对象的内部比较方式,适合单一的自然排序;而Comparator
作为外部工具,可以定义多种排序方式。- 灵活性:
Comparator
更加灵活,允许不同的排序策略,且可以在不修改原有类的情况下实现。- 应用场景:如果排序方式只有一种,或者是对象的自然顺序,推荐使用
Comparable
;如果需要多种排序方式,或者不希望修改原有类的结构,推荐使用Comparator
。
1 | private static <T> void siftUpUsingComparator( |
可知,cmp.compare()
方法的返回值< 0时,当前元素与父节点交换位置。siftUpComparable
方法也是相同,是在不传入Comparator对象也能进行大小比较的前提下,通过Comparable
的compareTo()
方法进行比较。
1 | private static <T> void siftUpComparable(int k, T x, Object[] es) { |
Comparator.comparingInt()
1 | public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) { |
由Comparator.comparingInt()
的代码可知,它接收一个ToIntFunction
作为参数。o -> o
是一个lambda表达式,将元素本身作为整数值返回,实现了小顶堆的效果。
1 |
|
总结
PriorityQueue的排序规则,依赖于cmp.compare()
的具体实现。cmp.compare()
方法的返回值< 0时,当前元素与父节点交换位置。
以数值大小进行直接比较的自然排序为例,数值较小的元素总是会成为父节点,最终形成小顶堆的效果。
动手深度学习
数据操作 + 数据预处理
N维数组
N维数组是机器学习和神经网络的主要数据结构。
0维 标量
1.0
表示一个类别
维 向量
[1.0, 2.7, 3.4]
表示一个特征向量
2维 矩阵
[[1.0, 2.7, 3.4]
[5.0, 0.2, 4.6]
[4.3, 8.5, 0.2]]
表示一个样本—特征矩阵
3维
[ [ [0.1, 2.7, 3.4]
[5.0, 0.2, 4.6]
[4.3, 8.5, 0.2] ]
[ [3.2, 5.7, 3.4]
[5.4, 6.2, 3.2]
[4.1, 3.5, 6.2] ] ]
如RGB图片(w * H * channels )
4维
一个RGB图片的批量 (批量大小 * 宽 * 高 * 通道)
5维
一个视频批量 (批量大小 * 时间 * 宽 * 高 * 通道)
数据操作
1 | import torch |
张量(tensor)表示一个数值组成的数组,这个数据可能有多个维度。
shape
属性:访问张量的形状。numel()
函数:访问张量中元素的总数。reshape()
函数:改变张量的形状而不改变元素数量和元素值。
1 | x = torch.arange(12) |
torch.zeros()
使用全0填充指定形状的张量torch.ones()
使用全1填充指定形状的张量torch.tensor()
为张量中的每个元素赋予确定值
1 | torch.zeros((2, 3, 4)) |
常见的标准运算符(
+
、-
、*
、/
和求幂**
)都可以被升级为按元素运算。1
2
3x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x**y
torch.cat()
连结张量
通过逻辑运算符构建二元张量
x.sum()
对张量中所有元素进行求和,会产生只有一个元素的张量。即使形状不同,仍然可以通过调用广播机制(维度的尺寸要么相等,要么其中一个维度为1)来执行按元素操作。
可以用
[-1]
选择最后一个元素可以用
[1:3]
选择第二个和第三个元素可以指定索引将元素写入矩阵
为多个元素赋值相同的元素,只需要索引所有元素,为其赋值。
将Numpy类型的张量转换为pytorch类型的张量
1
2A = X.numpy()
B = torch.tensor(A)将大小为1的张量转换为Python标量
1
2a = torch.tensor([3.5])
a.item(), float(a), int(a)
数据预处理
创建人工数据集,并存储在csv文件
1
2
3
4
5
6
7
8
9
10
11
12import os
# 创建 ../data文件夹
os.makedirs(os.path.join('..', 'data'), exist_ok=True)
# CSV 文件的路径 ../data/house_tiny.csv
data_file = os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:
f.write('NumRooms,Alley,Price\n') # 列名
f.write('NA,Pave,127500\n') # 每行表示一个数据样本
f.write('2,NA,106000\n')
f.write('4,NA,178100\n')
f.write('NA,NA,140000\n')从csv文件中加载原始数据集
为了处理缺失的数据,典型的方法包括插值和删除。
下例通过插入数值,消除缺失的数值。
1
2
3
4inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
# 用 非空数据的平均值 填充NaN
inputs = inputs.fillna(inputs.mean(numeric_only=True))
inputs对于类别值或离散值,将
NaN
视为一个类别。1
2
3
4
5
6
7# 独热编码(将分类变量转换为二进制向量)
# 每个分类变量的每个可能取值都被编码为一个二进制特征
# 当分类变量有多个不同取值时,会生成更多的列。这可能会导致维度灾难(curse of dimensionality)的问题
inputs = pd.get_dummies(inputs, dummy_na=True)
# 将inputs中的所有值转换为数值类型(True/False --> 1/0)
inputs *= 1
inputs将dataframe转换为张量
1
2
3
4
5
6
7import torch
# 把所有条目都是数值类型的 dataframe格式的inputs和outputs
# 转换为张量格式
X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
X, y
线性代数
标量
- 简单操作
- $c = a + b$
- $c = a \cdot b$
- $c = \sin a$
- 长度
- $|a|=\begin{cases} a, & \text{if } a > 0 \ -a, & \text{if } x \leq 0 \end{cases}$
- $|a+b| \leq |a| + |b|$
- $|a\cdot b|=|a| \cdot |b|$
标量由只有一个元素的张量表示。
1 | import torch |
向量
简单操作
- $c = a + b \text{ where } c_i = a_i + b_i$
- $c = \alpha \cdot b \text{ where } c_i = \alpha b_i$ (α是标量)
- $c = \sin a \text{ where } c_i = \sin a_i$
长度
- $||a||2=\sqrt{\sum\limits{i=1}^ma_i^2}$
- $||a||\geq 0 \text{ for all } a$
- $||a+b|| \leq ||a|| + ||b||$
- $||\alpha\cdot b||=|\alpha| \cdot ||b||$ (α是标量)
点乘
$a^Tb = \sum\limits_ia_ib_i$
如果a与b正交,则$a^Tb = 0$
1 | # 可以把向量视为标量值组成的列表 |
矩阵
简单操作
- $C = A + B \text{ where }C_{ij}=A_{ij}+B_{ij}$
- $C=\alpha\cdot B \text{ where } C_{ij}=\alpha B_{ij}$
- $C=\sin A \text{ where } C_{ij} = \sin A_{ij}$
乘法(矩阵 x 向量)
$C = Ab \text{ where } C_i=\sum\limits_jA_{ij}b_j$ (矩阵A的每一行和向量b做内积)
乘法(矩阵 x 矩阵)
$C = AB \text{ where } C_{ik}=\sum\limits_jA_{ij}B_{jk}$
范数(norm)
F范数:把所有元素的模取平方和,再开方。
$||A||2=\sqrt{|A{11}|^2+… + |A_{nn}|^2}=\sqrt{\sum\limits_{i,j}|a_{ij}|^2}$
1
torch.norm(torch.ones((4, 9)))
平方范数$L_2$,
特殊矩阵
对称和反对称矩阵
- $A_{ij}=A_{ji}$
- $A_{ij}=-A_{ji}$
正定矩阵
$x$为任意向量,$x^TAx \geq 0$ ,就称A为正定矩阵。
正交矩阵
- 所有行/列都是单位向量,且两两正交
- 可以写成$AA^T=E$ (对角线全为1的单位矩阵)
置换矩阵
特征向量
不被矩阵改变方向的向量,称为特征向量。对称矩阵总能找到特征向量。
$Ax = \lambda x$
代码举例
通过指定两个分量$m$和$n$来创建一个形状为$m\times n$的矩阵。
1 | A = torch.arange(20).reshape(5, 4) |
哈达玛积 $\odot$
两个矩阵的按元素乘法称为哈达玛积$\odot$,不常用。
1 | import torch |
1 | a = 2 |
按指定轴求和
1 | A = torch.arange(20 * 2).reshape(2, 5, 4) |
针对第1个维度求和
1
2A_sum_axis0 = A.sum(axis = 0)
A.shape, A_sum_axis0.shape, A_sum_axis0针对第2个维度求和
1
2A_sum_axis1 = A.sum(axis = 1)
A.shape, A_sum_axis1.shape, A_sum_axis1针对1、2两个维度求和
1
A.sum(axis = [0, 1])
针对第1个维度计算累加和
1
A.cumsum(axis = 0)
平均值
A.mean()
A.mean(axis=0)
计算总和或均值时 保持轴数不变
1 | A = torch.arange(20 * 2.).reshape(2, 5, 4) |
点积 $\cdot$ (点乘、内积)
点积是相同位置的按元素乘积的和
1 | x = torch.arange(4.) |
可以通过执行按元素乘法(哈达玛积$\odot$),然后求和来表示两个向量的点积。
1 | torch.sum(x * y) |
矩阵向量积
1 | A.shape, x.shape, torch.mv(A, x) |
矩阵乘法
1 | B = torch.ones(4, 3) |
梯度
梯度是导数在向量上的拓展,指向值变化最大的方向。
y是标量,x是向量
x是列向量,求导之后,会变成行向量。
y是向量,x是标量
y是列向量,求导之后,还是列向量。
y和x都是向量
求导之后,是一个矩阵。
自动求导
自动求导计算一个函数在指定值上的导数。自动求导有两种模式:
正向累积
$\frac{\partial y}{\partial x}=\frac{\partial y}{\partial u_n}(\frac{\partial u_n}{\partial u_{n-1}}(\dots(\frac{\partial u_2}{\partial u_1}\frac{\partial u_1}{\partial x})))$
反向累积(反向传递)
$\frac{\partial y}{\partial x}=(((\frac{\partial y}{\partial u_n}\frac{\partial u_n}{\partial u_{n-1}})\dots)\frac{\partial u_2}{\partial u_1})\frac{\partial u_1}{\partial x}$
反向累积求导的复杂度
时间复杂度 $O(n)$,n是操作子个数
通常和正向累积代价类似。
空间复杂度$O(n)$
需要存储正向计算的所有中间结果。正向累积的空间复杂度是$O(1)$
对函数$y = 2x^Tx$ 关于列向量x求导
1 | import torch |
重新计算梯度时,要清除之前的值
如继续对函数$y = \sum\limits_{i=1}^nx_i$关于列向量x求导。
1 | # 默认情况下,pytorch会累积梯度,需要清除之前的值 |
将某些计算移动到记录的计算图之外
1 | x.grad.zero_() |
1 | x.grad.zero_() |
即使构建函数的计算图需要通过Python控制流,仍然可以计算得到变量的梯度
1 | def f(a): |
线性回归 + 基础优化算法
线性回归模型
给定n维输入$\mathbf{x} = [x_1, x_2, \dots, x_n]^T$
线性回归模型有一个n维权重$\mathbf{w}$和一个标量偏差b
$\mathbf{w}=[w_1, w_2, \dots, w_n]^T, \ b$
输出是输入的加权和
$y = w_1x_1 +w_2x_2 +\dots+w_nx_n+b$
向量版本:$y=\langle\mathbf{w},\mathbf{x}\rangle+b$
线性回归模型可以看做单层神经网络,有显式解。
衡量预估质量
使用平方损失(均方损失)衡量预测值和真实值的差异。
$\mathscr{l}(y,\hat{y})=\frac{1}{2}(y-\hat{y})^2$ ($\frac{1}{2}$是为了求导时,方便消去)
参数学习
训练损失
$\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)=\frac{1}{2n}\sum\limits_{i=1}^{n}(y_i-\langle\mathbf{x}_i,\mathbf{w}\rangle-b)^2$
通过最小化损失函数来学习参数
$\mathbf{w}^*,\mathbf{b}^*=\arg\min\limits_{\mathbf{w},b}\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)$
均方误差(Mean Squared Error, MSE)是一种常用的损失函数,用于衡量预测值与真实值之间的差异:
$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$
1 | from torch import nn |
基础优化方法
梯度下降
当一个模型没有显式解时,如何操作?
挑选一个初始值$\mathbf{w}_0$
重复迭代参数 t = 1, 2, 3
$\mathbf{w}t= \mathbf{w}{t-1} -\eta \frac{\partial \ell}{\partial \mathbf{w}_{t-1}}$
$\frac{\partial \ell}{\partial \mathbf{w}{t-1}}$:损失函数$\ell$关于$\mathbf{w}{t-1}$的梯度
- 沿梯度方向将增加损失函数值
- 学习率$\eta$:步长的超参数(需要人为指定的值)。学习率不能太小,也不能太大。
在实际中,很少直接使用梯度下降,最长使用的形式是小批量随机梯度下降。
在整个训练集上算梯度成本高昂
一个深度神经网络模型可能需要数分钟至数小时
可以随机采样 b 个样本$i_1, i_2, …, i_b$来近似损失
$\frac{1}{b}\sum\limits_{i\in I_b}\ell(\mathbf{x}_i, y_i, \mathbf{w})$
b是批量大小,另一个重要的超参数。
- 梯度下降通过不断沿着反梯度方向更新参数求解
- 小批量随机梯度下降是深度学习默认的求解算法
- 两个重要的超参数是批量大小和学习率
pytorch实现
1 | import numpy as np |
调用框架中现有的api来读取数据
1 | def load_array(data_arrays, batch_size, is_train=True): |
使用框架的预定义好的层
1 | from torch import nn |
初始化模型参数
1 | # normal_ 对权重参数的数据进行正态分布初始化,均值为 0,标准差为 0.01 |
计算均方误差使用MSELoss
类
$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$
1 | loss = nn.MSELoss() |
实例化SGD实例
随机梯度下降(SGD, Stochastic Gradient Descent)
1 | # torch.optim.SGD是一个优化器类 |
训练
1 | num_epochs = 3 |
Softmax回归
Softmax回归其实是一个分类问题。
- 回归估计一个连续值
- 跟真实值的区别作为损失
- 分类预测一个离散类别
- 通常多个输出
- 输出i是预测为第i类的置信度
Softmax函数
$softmax(o)=\hat{y}$
$softmax(o)_i = \hat{y}_i=\frac{\exp(o_i)}{\sum_j\exp(o_j)}$
- $softmax(o)_i$ 是模型输出
o
经过 softmax 函数后的概率分布的第i
个元素。 - $\sum_j\exp(o_j)$:所有向量指数的和
使用Softmax函数得到每个类的预测置信度。
交叉熵
交叉熵常用来衡量两个概率的区别:$H(p,q)=\sum\limits_i -p_i\log (q_i)$
将它作为损失:
$\ell(y,\hat{y})= -\sum\limits_i y_i\log \hat{y}_i = -\log \hat{y}_y$
将每个类别的真实标签的概率 $y_i$ 乘以预测概率 $\hat{y}_i$ 的负对数,并将所有类别的结果求和。交叉熵衡量了预测概率分布 $\hat{y}_i$ 在真实概率分布 $y_i$ 上的不确定性和差异。
交叉熵损失函数对模型输出 o
求梯度,即真实概率和预测概率的区别
$\partial_{o_i}\ell(y, \hat{y}) = softmax(o)_i - y_i$
损失函数
均方损失 $L_2$ Loss
$\ell(y,y^{‘})=\frac{1}{2}(y-y^{‘})^2$
绝对值损失 $L_1$ Loss
$\ell(y, y^{‘})=|y-y^{‘}|$
优化到末期时,就不那么稳定
Softmax回归的从零开始实现
1 | import torch |
- softmax的输入需要是一个向量,展平每个图像,将它们视为长度为784(1*28*28)的向量。
- 因为我们的数据集有10个类别,所以网络输出维度为10
1 | num_inputs = 1 * 28 * 28 |
实现Softmax回归模型
对于一个矩阵来说,就是按行来做softmax。
$softmax(X){ij}=\frac{\exp (X{ij})}{\sum_{k}\exp (X_{ik})}$
1 | def softmax(X): |
实现交叉熵损失函数
1 | def cross_entropy(y_hat, y): |
将预测类别与真实y
元素进行比较
1 | def accuracy(y_hat, y): |
评估准确率
1 | class Accumulator: |
训练的一个迭代周期
1 | # 训练模型一个迭代周期 |
定义一个在动画中绘制数据的类
1 | from matplotlib_inline.backend_inline import set_matplotlib_formats |
训练函数
1 | def train(net, train_iter, test_iter, loss, num_epochs, updater): |
小批量随机梯度下降来优化模型的损失函数
1 | def sgd(params, lr, batch_size): |
训练10个迭代周期,对图像进行分类预测
1 | num_epochs = 10 |
多层感知机
感知机
给定输入$\mathbf{x}$,权重$\mathbf{w}$,偏移$b$,感知机输出:
$o=\sigma(\langle\mathbf{w},\mathbf{x}\rangle+b)$
$\sigma(x)=\begin{cases}1 \ \ \text{if}\ x > 0\0 \ \ \text{otherwise}\end{cases}$
感知机其实就是二分类的问题。
- 和线性回归相比,线性回归输出实数。
- 和Softmax回归相比,Softmax是多分类。
模型选择
模型容量
模型容量指的是拟合各种函数的能力。
- 低容量的模型难以拟合训练数据
- 高容量的模型可以记住所有的训练数据。
网络中的网络 NiN
全连接层的问题
卷积层只需要较少的参数,但卷积层之后的第一个全连接层需要的参数量很大。
- 卷积层的参数量: $c_i\times c_o \times k^2$ (输入通道数 x 输出通道数 x 卷积核的高 x 卷积核的宽)
- 全连接层的参数量:$c_i\times w_i \times h_i \times c_o \times w_o \times h_i$ (输入通道数 x 输入宽度 x 输入高度 x 输出通道数 x 输出宽度 x 输出高度)
参数量大,会占用很多内存、计算带宽、容易过拟合。
NiN块
1个卷积层后跟2个1 * 1的卷积层,对每个像素增加了非线性:
- 步幅1,无填充,输出形状跟卷积层输出一样
- 起到全连接层的作用
NiN架构
- 无全连接层
- 交替使用NiN块和步幅为2的最大池化层,逐步减小宽高、增大通道数。
- 最后使用全局平均池化层得到输出,不容易过拟合,更少的参数个数。
- 输入通道数是类别数
含并行连结的网络 GoogLeNet / Inception V3
Inception块用4条有不同超参数的卷积层和池化层的路来抽取不同的信息,它的一个主要优点是模型参数小,计算复杂度低。
GoogLeNet使用了9个lnception块,是第一个达到上百层的网络,后续有一系列改进。
批量归一化
损失出现在最后,后面的层训练较快。
数据在最底部
- 底部的层训练较慢
- 底部层一变化,所有都得跟着变
- 最后的那些层需要重新学习多次
- 导致收敛变慢
我们可以在学习底部层的时候避免变化顶部层吗?
固定小批量B里的样本x的均值
$\mu_B=\frac {1}{|B|}\sum\limits_{x\in B}x_i$
固定小批量B里的样本x的方差
$\sigma^2_B=\frac{1}{|B|}\sum\limits_{i\in B}(x_i-\mu_B)^2+\epsilon$
$\epsilon$是为了防止方差为0的一个很小的数。
批量归一化就是通过可学习的参数$\gamma、\beta$得到的结果:
$x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta$
批量归一化层
- 含有可学习的参数为$\gamma$和$\beta$
- 作用范围:
- 全连接层和卷积层输出上,激活函数前
- 全连接层和卷积层输入上
- 对全连接层,作用在特征维
- 对于卷积层,作用在通道维
批量归一化在做什么
后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度,因此没必要跟丢弃法混合使用。
$x_{i+1}=\gamma\frac{x_i-\hat{\mu}_B}{\hat{\sigma}_B}+\beta$
把$\hat{\mu}_B$和$\hat{\sigma}_B$看作随机偏移和随机缩放。
批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放。可以加速收敛速度,但一般不改变模型精度。
残差网络 ResNet
残差块
- 串联一个层改变函数类,希望能扩大函数类。
- 残差块加入快速通道,来得到$f(x)=x+g(x)$的结构
有很多调整变形的排列组合。
有两种残差块:
- 高宽减半、通道数翻倍的残差块(步幅stride=2)
- 接多个高宽不变的残差块(步幅stride=1)
残差块使得很深的网络更加容易训练,甚至可以训练一千层的网络。
数据增广
数据增强就是在一个已有的数据集中,通过变形数据,使得数据集有更多的多样性,模型繁华性能更好。例如:
- 在语言里加入各种不同的背景噪音
- 改变图片的形状和颜色
常见的图片的数据增强的形式:
- 翻转
- 切割
- 变色
微调
- 微调通过使用在大数据上得到的预训练好的模型,来初始化模型权重,来完成提升精度。
- 预训练模型质量很重要。
- 微调通常速度更快、精度更高。
序列模型
- 时序模型中,当前数据跟之前观察到的数据相关
- 自回归模型使用自身过去数据来预测未来
- 马尔科夫模型假设当前只跟最近少数数据相关,从而简化模型
- 潜变量模型使用潜变量来概括历史信息
Redis
Redis基础
什么是Redis
Redis是基于C语言开发的NoSQL数据库,它是一种存储KV键值对数据的内存数据库,因此读写速度非常快,被广泛应用于分布式缓存方向。
Redis内置了多种数据类型实现:
- 5种基础数据类型
- String
- List
- Set
- Hash
- Zset (有序集合)
- 3种特殊数据类型
- HyperLogLog(基数统计)
- Bitmap(位图)
- Geospatial (地理位置)
Reids还支持事务、持久化(将内存数据保存在磁盘中,重启时可以再次加载使用)、Lua脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。
Redis为什么速度快?
Redis基于内存,内存的访问速度是磁盘的上千倍。
Redis基于Reactor模式,设计开发了一套高效的事件处理模型,主要是单线程事件循环和IO多路复用。
Redis内置了多种优化过后的数据类型/结构实现,性能非常高。
Redis持久化机制
使用缓存的时候,我们经常需要对内存中的数据进行持久化,也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。
Redis 支持 3 种持久化方式:
- 快照(snapshotting,RDB)
- 只追加文件(append-only file, AOF)
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)
RDB 持久化
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式,在 redis.conf
配置文件中默认有此下配置:
1 | save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。 |
AOF持久化
与快照持久化相比,AOF (append only file)持久化的实时性更好。Redis 6.0 之后默认开启了AOF持久化,可以通过 appendonly
参数开启:
1 | appendonly yes |
开启 AOF 持久化后,每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf
中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync
策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。
只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。
AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir
参数设置的,默认的文件名是 appendonly.aof
。
Redis线程模型
对于读写命令来说,Redis 一直是单线程模型。在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
Redis 中除了字符串类型有自己独有设置过期时间的命令
setex
外,其他方法都需要依靠expire
命令来设置过期时间 。另外,persist
命令可以移除一个键的过期时间。很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
常见的缓存更新策略
Cache Aside Pattern 旁路缓存模式
比较适合读比较多的场景。
Cache Aside Pattern中,服务端需要同时维系数据库和缓存,并且以db为准。
写步骤:
更新db
直接删除cache
Cache Aside Pattern 旁路缓存模式下,写数据的时候,为什么选择删除cache,而不是更新cache?
对服务端造成资源浪费
删除cache更直接,如果频繁修改db,就会导致需要频繁更新cache,而cache中的数据可能都没有被访问到。
产生数据不一致问题
并发场景下,更新cache产生数据不一致问题的概率会更大。
在写数据的过程中,可以先删除cache,后更新db吗?
不行,可能会造成数据库和缓存数据不一致。
- 请求1先把cache中的A数据删除;
- 请求2从db中读取数据;
- 请求1再把db中的A数据更新。
在写数据的过程中,先更新db,后删除cache就没问题了吗?
还是可能出现数据不一致的问题,不过概率很小,因为缓存写入速度比数据库写入速度快很多。
- 请求1从db中读取数据 (说明此时cache没有缓存该数据)
- 请求2更新db中的数据,删除缓存
- 请求1将读取的数据写入cache (步骤2很难比步骤3快,因为写数据库一般会先加锁)
读步骤:
- 从cache中读,读取到直接返回
- cache中读取不到,就从db读
- 把db中读取到的数据放到cache中
Cache Aside Pattern 旁路缓存模式 的缺陷:
首次请求的数据一定不在cache中。
可以将热点数据提前放入cache。
如果写操作比较频繁,会导致cache中的数据被频繁删除,影响缓存命中率。
数据库和缓存数据强一致
加一个锁/分布式锁,更新db的时候,同样更新cache,保证不存在线程安全问题。
短暂地允许数据库和缓存数据不一致
更新db的时候,同样更新cache,给缓存加一个比较短的过期时间,即使数据不一致,影响也比较小。
Read/Write Through Pattern 读写穿透模式
读写穿透模式中,服务端把cache视为主要数据存储,从中读取数据并将数据写入其中。cache服务负责将此数据读取和写入db,从而减轻应用程序的职责。
这个模式在平时开发过程中非常少见,大概率是因为Redis没有提供cache将数据写入db的功能。
Read-Through Pattern实际只是在Cache Aside Pattern之上进行了封装。在Cache
Aside Pattern下,发生读请求的时候,如果cache中不存在对应的数据,是由客户端自己负责把数据写入cache,而Read Through Pattern则是cache服务自己来写入缓存的,这对客户端是透明的。
Write Behind Pattern 异步缓存写入模式
Write Behind Pattern 异步缓存写入模式 和 Read/Write Through Pattern 读写穿透模式很相似,两者都是由cache服务负责cache和db的读写。
但是读写穿透模式同步更新cache和db,而异步缓存写入模式只是更新缓存,不直接更新db,改为异步批量的方式更新db。
这对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新db,但cache服务就挂掉了。
这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少。比如消息队列中消息的异步写入磁盘、MySQL的Innodb Buffer Pool机制都用到了这种策略。
Write Behind Pattern下db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
Spring Boot
Spring Boot启动流程
@SpringBootApplication注解
Spring Boot的启动,首先需要一个加了@SpringBootApplication
注解的启动类。
这个注解本质上,是由@EnableAutoConfiguration
、@SpringBootConfiguration
、@ComponentScan
三个注解连起来构成。
@EnableAutoConfiguration
最为核心会导入自动配置
AutoConfigurationImportSelector
类,这个类会将所有符合条件的@Configuration
配置都进行加载。@SpringBootConfiguration
等同于@Configuration
将当前类标记为配置类,加载到容器中。
@ComponentScan
自动扫描并加载符合条件的Bean
SpringApplication.run
方法
在run方法开始执行后,会经历如下4个阶段:
服务构建
构建SpringApplication本身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 1. 将资源加载器、主方法类加载至内存中
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 2. 逐一判断对应的服务类是否存在,来确定Web服务的类型。默认是基于Servlet的Web服务,如Tomcat
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 3. 加载初始化类,读取所有"META-INF/spring.factories"文件中的
// 注册初始化、上下文初始化、监听器这三类配置
// Spring Boot和Spring Boot Autoconfigure这两个工程中配置了7个上下文初始化和8个监听器
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); // 没有默认的注册初始化配置
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 通过StackWalker判断出main方法所在的类(大概率就是启动类本身)
this.mainApplicationClass = deduceMainApplicationClass();
}环境准备
SpringApplication.run就是进入环境准备阶段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46public ConfigurableApplicationContext run(String... args) {
// ...
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
if (context.isRunning()) {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}容器创建
填充容器
jmap
概述
jmap命令是一个可以输出所有内存中对象的工具,甚至可以将JVM中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的所有对象的情况(如:产生哪些对象,及其数量)。
命令格式
1 | jmap [option] <pid> |
基本命令
输出jvm的heap内容到文件
jmap -dump:live,format=b,file=[outputFileName.hprof] [pid]
使用hprof二进制形式输出jvm的heap内容到文件。live子选项是可选的,只输出活的对象到文件。
打印正等候回收的对象的信息
jmap -finalizerinfo [pid]
打印heap的概要信息
JDK 8: jmap -heap [pid]
JDK 9往后:jhsdb jmap --heap --pid [pid]
1 | root@PC:~# jhsdb jmap --heap --pid 51266 |
打印每个class的实例数目、内存占用、类名信息
jmap -histo:live [pid]
如何快速定位OOM问题
OOM的原因
一次性申请过多的对象
更改申请对象的数量。(分页)
内存资源耗尽 未释放
找到未释放的对象进行释放。
本身资源不够
查看堆信息:
JDK 8: jmap -heap [pid]
JDK 11往后:jhsdb jmap --heap --pid [pid]
通过dump定位
系统已经OOM挂掉了
提前设置
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=[指定的存放路径]
需要保证服务器的硬盘空间够大,因为该设置会记录系统在整个运行过程中所有的对象信息,占用空间。
发生OOM后,会生成
java_pidxxxxx.hprof
的heap dump文件。使用JProfiler进行查看
找到与业务相关的类 - 右键,使用选定对象
选择 传入引用,确定。
显示到GC根(GC Root)的路径,确定
根据显示的线程堆栈,查看代码进行分析。
系统运行中,还未OOM
方式一 导出dump文件
jmap -dump:format=b,file=[outputFileName.hprof] [pid]
在系统运行阶段导出dump文件,会造成一次FullGC、STW(Stop The World,所有线程中断)。但不导出dump文件的话,时间成本要增高很多
方式二 Arthas
RabbitMQ
简介
消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通时,将消息转发给相应的应用程序或者服务。
如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。
RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据。
Docker下相关命令
下载
1
2# 要选择带management的版本,带管理页面
docker pull rabbitmq:3.12-management创建用户
1
2
3
4rabbitmqctl add_user [username] [password]
# 赋予管理员权限
rabbitmqctl set_user_tags [username] administrator
架构
交换机 exchange
发布订阅模式 fanout
发布一次,消费多个。
JMeter
下载
Apache JMeter - Download Apache JMeter
选择二进制文件包。
Windows下
windows下,解压后运行”\apache-jmeter-5.6.2\bin\jmeter.bat”。
Options- choose language - 选择中文。
Linux下
解压后运行apache-jmeter-5.6.2/bin/jmeter.sh -n -t first.jmx -l result.jtl
-n
命令行下执行-t
要运行的测试脚本文件-l
记录结果的文件
查看result.jtl文件,要将文件拷出到windows,在监听器 - 聚合报告 下,打开文件浏览
配置不同用户进行测试
cookie管理器中,通过${}
获取CSV中设置的变量值