2024年9月

XGBoost模型 0基础小白也能懂(附代码)

原文链接

啥是XGBoost模型

XGBoost 是 eXtreme Gradient Boosting 的缩写称呼,它是一个非常强大的 Boosting 算法工具包,优秀的性能(效果与速度)让其在很长一段时间内霸屏数据科学比赛解决方案榜首,现在很多大厂的机器学习方案依旧会首选这个模型。

XGBoost 在并行计算效率、缺失值处理、控制过拟合、预测泛化能力上都变现非常优秀。本文我们给大家详细展开介绍 XGBoost,包含「算法原理」和「工程实现」两个方面。

关于 XGBoost 的原理,其作者陈天奇本人有一个非常详尽的
Slides
做了系统性的介绍。

Boosted Tree

Boosted Tree(提升树)是一种常用的机器学习方法,属于集成学习的一种。它通过将多个弱学习器(通常是决策树)组合起来,以提升整个模型的预测性能。Boosted Tree的核心思想是通过逐步训练多个决策树,每个树都试图修正前一个树的错误,最终得到一个更强大的模型。

模型:假设我们有
\(K\)
棵树
\(\hat{y_i}=\sum_{k=1}^Kf_k(x_i),f_k\in{F}\)

\(F\)
为包含所有回归树的函数空间。
目标函数:
\(Obj=\sum_{i=1}^nl(y_i,\hat{y_i})+\sum_{k=1}^K\Omega(f_k)\)
\(\sum_{i=1}^nl(y_i,\hat{y_i})\)
是成本函数
\(\sum_{k=1}^K\Omega(f_k)\)
是正则化项,代表树的复杂程度,树越复杂正则化项的值越高(正则化项如何定义我们会在后面详细说)。

当我们讨论决策树或相关的树模型时,通常是启发式的。启发式(heuristic)在机器学习中指的是使用经验法则或近似方法来解决问题,而不保证找到最优解。

Gradient Boosting(如何学习)

在做 GBDT 的时候,我们没有办法使用 SGD(Stochastic Gradient Descent,随机梯度下降),因为它们是树,而非数值向量——也就是说从原来我们熟悉的参数空间变成了函数空间。Gradient Boosting Decision Trees(GBDT)与深度学习或线性模型不同,它的核心不是直接通过参数更新来优化,而是通过构建新的决策树来逐步降低误差。

解决方案:初始化一个预测值,每次迭代添加一个新函数
\((f)\)

1)目标函数变换

根据解决方案可以对目标函数进行初步变形

其中constant是常数项,比如
\(\Omega(f_1),\Omega(f_2)\)
之类的,然后第三行就是考虑平方损失,
\(l(y_i,\hat{y_i})=\frac{1}{2}(y_i-\hat{y_i})^2\)
,代进去就行

所以我们的目的就是找到
\(f(t)\)
使得目标函数最低。然而,经过上面初次变形的目标函数仍然很复杂,目标函数会产生二次项。引入泰勒公式

这图也多少有点问题,是在还没考虑平方损失的地方引入泰勒公式,然后泰勒公式也有问题,后面两项应该是
\(f(x)\)
的一阶导数和二阶导数,所以才是
\(g_i,h_i\)

再把里面的常数项提取出,和
\(f_t\)
无关

2)重新定义树

前面已经用
\(f_t(x)\)
表示一棵树,在本小节,我们重新定义一下树:我们通过叶子结点中的分数向量和将实例映射到叶子结点的索引映射函数来定义树:(有点儿抽象,具体请看下图)

图里有问题,第一个叶子结点权重是+2

3)定义树的复杂程度

其中
\(T\)
才是叶子节点的个数,
\(\gamma\)
是控制树的复杂度的参数,树的叶子节点越多,复杂度越高。通过调节
\(\gamma\)
可以控制模型的复杂度。后面一堆是 L2 Norm正则化系数

4)重新审视目标函数

定义在叶子结点
\(j\)
中的实例的集合为:
\(I_j=\{i|q(x_i)=j\}\)
,这么定义也是为了能够构建出第三个式子,都写成
\(\sum_{j=1}^T\)

同时也会发现上式是
\(T\)
个独立二次函数的和

5)计算叶子结点的值

搞了一大坨,其实也就是先把值换成
\(G_j,H_j\)
,然后用一元二次方程求一个最优值就完了。

下图是前面公式讲解对应的一个实际例子。

这里再次总结一下,我们已经把目标函数变成了仅与
\(G,H,\gamma,\lambda,T\)
这五项已知参数有关的函数,把之前的变量
\(f_t\)
消灭掉了,也就不需要对每一个叶子进行打分了!

那么现在问题来,刚才我们提到,以上这些是假设树结构确定的情况下得到的结果。但是树的结构有好多种,我们应该如何确定呢?

6) 贪婪算法生成树

上一部分中我们假定树的结构是固定的。但是,树的结构其实是有无限种可能的,本小节我们使用贪婪算法生成树:

首先生成一个深度为0的树(只有一个根结点,也叫叶子结点)

对于每棵树的每个叶子结点,尝试去做分裂(生成两个新的叶子结点,原来的叶子结点不再是叶子结点)。在增加了分裂后的目标函数前后变化为(我们希望增加了树之后的目标函数小于之前的目标函数,所以用之前的目标函数减去之后的目标函数):

\(Gain=\frac{1}{2}(\frac{G_L^2}{H_L+\lambda}+\frac{G_R^2}{H_R+\lambda}-\frac{(G_L+G_R)^2}{H_L+H_R+\lambda})-\gamma\)

接下来要考虑的是如何寻找最佳分裂点。

例如,如果
\(x_j\)
是年龄,当分裂点是
\(a\)
的时候的增益
\(Gain\)
是多少?

其实这里对排序后的实例进行从左到右的线性扫描就足以决定特征的最佳分裂点。从左到右依次扫描:一旦数据按照特征值进行了排序,我们从第一个样本开始,依次计算每个可能的分裂点。对于每个分裂点,我们把样本分为“左侧”和“右侧”两个子集,分别计算划分前后目标函数的变化。下面还有别的一些办法

7)如何处理分类型变量

在很多情况下,我们不需要为分类变量设计特殊的处理方式,可以将其转换为one-hot 编码来处理。

\(z_j=
\begin{cases}
0& \text{if x is in category y}\\
1& \text{otherwise}
\end{cases}
\)

如果有太多的分类的话,矩阵会非常稀疏,算法会优先处理稀疏数据。

8) 修剪和正则化

回顾之前的增益,当训练损失减少的值小于正则化带来的复杂度时,增益有可能会是负数,此时就是模型的简单性和可预测性之间的权衡

XGBoost核心原理归纳解析

铺垫了那么多,总算到这里了。XGBoost 也是一个 Boosting 加法模型,每一步迭代只优化当前步中的子模型。


\(m\)
步我们有:
\(F_m(x_i)=F_{m-1}(x_i)+f_m(x_i)\)

\(f_m(x_i)\)
为当前步的子模型。
\(F_{m-1}(x_i)\)
为前
\(m-1\)
个完成训练且固定了的子模型。

泰勒展开

然后去掉常数,带入复杂度(和之前一样)

1)近似算法

基于性能的考量,XGBoost 还对贪心准则做了一个近似版本,简单说,处理方式是「将特征分位数作为划分候选点」。这样将划分候选点集合由全样本间的遍历缩减到了几个分位数之间的遍历。

展开来看,特征分位数的选取还有 global 和 local 两种可选策略:

精确贪心准则:这是默认的精确算法,遍历所有可能分裂点,找到能最大化增益的点。计算量最大,但分裂效果最优。
Global 近似分裂:使用全体样本的特征分位数进行一次性划分,分裂点在所有节点中复用,计算量大幅减少,适合较大的数据集。
Local 近似分裂:在每个节点分裂前根据当前节点的样本重新计算特征分位数,能够更加灵活适应不同节点的特征分布,适合样本分布差异较大的情况。

近似算法的性能与精确贪心算法几乎相同,但大大降低了计算成本。

2)加权分位数

在 XGBoost 中,加权分位数(Weighted Quantile Sketch)用于加速分裂点的寻找过程。加权分位数算法并不是直接根据样本的特征值来划分分位点,而是考虑了
样本的二阶导数(Hessian)
作为权重,从而更好地平衡分裂点的选择,特别是在近似算法中。

令偏导为0易得
\(f_m^*(x_i)=-\frac{g_i}{h_i}\)

3) 列采样与学习率

列采样指的是在构建每棵决策树时,XGBoost 不会使用全部特征,而是随机选择部分特征用于分裂。这种方法源自于随机森林的思想,目的是增加模型的多样性,从而防止过拟合。

学习率在梯度提升树(GBDT)中是一个非常重要的超参数,用于控制每棵树对模型的贡献。学习率可以防止模型更新过快,从而提升模型的稳定性和性能。也叫步长、shrinkage,具体的操作是在每个子模型前(即每个叶节点的回归值上)乘上该系数,不让单颗树太激进地拟合,留有一定空间,使迭代更稳定。XGBoost默认设定为 。

4) 特征缺失与稀疏性

简单说,它的做法是将缺失值和稀疏
\(0\)
值等同视作缺失值,将其「绑定」在一起,分裂节点的遍历会跳过缺失值的整体。这样大大提高了运算效率。

比如在下面的例子中有六种划分情况,XGBoost 会遍历以上6种情况(3个非缺失值的切分点×缺失值的两个方向),最大的分裂收益就是本特征上的分裂收益

XGBoost工程优化

1)并行列块设计

XGBoost 将每一列特征提前进行排序,以块(Block)的形式储存在缓存中,并以索引将特征值和梯度统计量对应起来,每次节点分裂时会重复调用排好序的块。而且不同特征会分布在独立的块中,因此可以进行分布式或多线程的计算。

2)缓存访问优化

特征值排序后通过索引来取梯度
\(g_i,h_i\)
会导致访问的内存空间不一致,进而降低缓存的命中率,影响算法效率。为解决这个问题,XGBoost为每个线程分配一个单独的连续缓存区,用来存放梯度信息。

3) 核外块计算

数据量非常大的情形下,无法同时全部载入内存。XGBoost 将数据分为多个 blocks 储存在硬盘中,使用一个独立的线程专门从磁盘中读取数据到内存中,实现计算和读取数据的同时进行。
为了进一步提高磁盘读取数据性能,XGBoost 还使用了两种方法:

① 压缩 block,用解压缩的开销换取磁盘读取的开销。
② 将 block 分散储存在多个磁盘中,提高磁盘吞吐量。

XGBoost vs GBDT

GBDT 是机器学习算法,XGBoost 在算法基础上还有一些工程实现方面的优化。

GBDT 使用的是损失函数一阶导数,相当于函数空间中的梯度下降;XGBoost 还使用了损失函数二阶导数,相当于函数空间中的牛顿法。

正则化
:XGBoost 显式地加入了正则项来控制模型的复杂度,能有效防止过拟合。

列采样
:XGBoost 采用了随机森林中的做法,每次节点分裂前进行列随机采样。

缺失值
:XGBoost 运用稀疏感知策略处理缺失值,GBDT无缺失值处理策略。

并行高效
:XGBoost 的列块设计能有效支持并行运算,效率更优。

代码实现

需要先下载xgboost

pip install xgboost

代码如下

# 导入所需的库
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_diabetes  # 替换为 load_diabetes
from sklearn.metrics import mean_squared_error

# 1. 加载糖尿病数据集
# 这个数据集包含442个样本,10个特征,用于预测一个连续目标变量
diabetes = load_diabetes()
X, y = diabetes.data, diabetes.target  # X是特征数据,y是标签(目标变量)

# 2. 划分数据集为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 3. 将数据转换为 DMatrix 格式
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

# 4. 设置 XGBoost 模型的超参数
params = {
    'objective': 'reg:squarederror',  # 回归任务使用的目标函数,平方误差
    'max_depth': 3,                   # 决策树的最大深度,控制模型的复杂度
    'eta': 0.05,                       # 学习率,控制每棵树对整体模型的贡献
    'eval_metric': 'rmse' ,            # 评估指标,使用均方根误差(RMSE)
    'lambda': 2,                        # L2 正则化项,防止过拟合
    'alpha': 0.5   # L1 正则化项
}

# 5. 设定训练轮数
num_round = 200  # 训练的轮数,即构建多少棵树

# 6. 定义评估数据集
evals = [(dtrain, 'train'), (dtest, 'eval')]  # (数据集, 数据集名称)

# 7. 训练 XGBoost 模型,加入 early_stopping_rounds早停机制,防止过拟合
bst = xgb.train(params, dtrain, num_round, evals, early_stopping_rounds=10)

# 8. 使用训练好的模型对测试集进行预测
y_pred = bst.predict(dtest)

# 9. 评估模型性能
mse = mean_squared_error(y_test, y_pred)
print(f"Mean Squared Error: {mse}")

# 10. 保存训练好的模型
bst.save_model('xgboost_model.json')

# 11. 加载已保存的模型
loaded_bst = xgb.Booster()
loaded_bst.load_model('xgboost_model.json')

# 12. 使用加载的模型进行预测
y_pred_loaded = loaded_bst.predict(dtest)
mse_loaded = mean_squared_error(y_test, y_pred_loaded)
print(f"Mean Squared Error from loaded model: {mse_loaded}")

结果如下

[0]	train-rmse:76.08309	eval-rmse:71.75905
[1]	train-rmse:74.34324	eval-rmse:70.47408
[2]	train-rmse:72.66427	eval-rmse:69.24759
[3]	train-rmse:71.10664	eval-rmse:68.09809
[4]	train-rmse:69.63498	eval-rmse:67.14668
[5]	train-rmse:68.24045	eval-rmse:66.09854
[6]	train-rmse:66.93042	eval-rmse:64.91738
[7]	train-rmse:65.73304	eval-rmse:64.08775
[8]	train-rmse:64.58640	eval-rmse:63.26052
[9]	train-rmse:63.51304	eval-rmse:62.49745
[10]	train-rmse:62.44810	eval-rmse:61.64759
[11]	train-rmse:61.51387	eval-rmse:60.96222
[12]	train-rmse:60.61767	eval-rmse:60.32972
[13]	train-rmse:59.77722	eval-rmse:59.74329
[14]	train-rmse:59.01348	eval-rmse:59.13121
[15]	train-rmse:58.24704	eval-rmse:58.55106
[16]	train-rmse:57.57392	eval-rmse:58.15165
[17]	train-rmse:56.92761	eval-rmse:57.68188
[18]	train-rmse:56.33319	eval-rmse:57.37781
[19]	train-rmse:55.72582	eval-rmse:56.97001
[20]	train-rmse:55.14420	eval-rmse:56.45029
[21]	train-rmse:54.61096	eval-rmse:55.97904
[22]	train-rmse:54.12594	eval-rmse:55.57225
[23]	train-rmse:53.68383	eval-rmse:55.39305
[24]	train-rmse:53.24822	eval-rmse:55.01127
[25]	train-rmse:52.85214	eval-rmse:54.85699
[26]	train-rmse:52.43814	eval-rmse:54.49904
[27]	train-rmse:52.07004	eval-rmse:54.42905
[28]	train-rmse:51.68191	eval-rmse:54.25354
[29]	train-rmse:51.28268	eval-rmse:54.09452
[30]	train-rmse:50.94229	eval-rmse:54.06703
[31]	train-rmse:50.58475	eval-rmse:53.88010
[32]	train-rmse:50.24739	eval-rmse:53.74475
[33]	train-rmse:49.97042	eval-rmse:53.49905
[34]	train-rmse:49.65855	eval-rmse:53.41597
[35]	train-rmse:49.38190	eval-rmse:53.34692
[36]	train-rmse:49.07203	eval-rmse:53.32202
[37]	train-rmse:48.81472	eval-rmse:53.22084
[38]	train-rmse:48.57124	eval-rmse:53.24058
[39]	train-rmse:48.33730	eval-rmse:53.13983
[40]	train-rmse:47.97171	eval-rmse:53.05406
[41]	train-rmse:47.75619	eval-rmse:52.87405
[42]	train-rmse:47.43067	eval-rmse:52.80852
[43]	train-rmse:47.18844	eval-rmse:52.70296
[44]	train-rmse:46.96694	eval-rmse:52.61260
[45]	train-rmse:46.79053	eval-rmse:52.58588
[46]	train-rmse:46.58746	eval-rmse:52.51602
[47]	train-rmse:46.38476	eval-rmse:52.50433
[48]	train-rmse:46.15591	eval-rmse:52.44922
[49]	train-rmse:46.00542	eval-rmse:52.36981
[50]	train-rmse:45.84480	eval-rmse:52.27445
[51]	train-rmse:45.63700	eval-rmse:52.23794
[52]	train-rmse:45.49250	eval-rmse:52.25740
[53]	train-rmse:45.31208	eval-rmse:52.16836
[54]	train-rmse:45.15374	eval-rmse:52.22044
[55]	train-rmse:45.00284	eval-rmse:52.15072
[56]	train-rmse:44.87677	eval-rmse:52.04112
[57]	train-rmse:44.71921	eval-rmse:52.08482
[58]	train-rmse:44.55626	eval-rmse:52.02783
[59]	train-rmse:44.41483	eval-rmse:52.09304
[60]	train-rmse:44.27997	eval-rmse:52.03098
[61]	train-rmse:44.15710	eval-rmse:52.08378
[62]	train-rmse:44.00683	eval-rmse:52.02136
[63]	train-rmse:43.84878	eval-rmse:52.06178
[64]	train-rmse:43.74180	eval-rmse:52.06495
[65]	train-rmse:43.59775	eval-rmse:52.08875
[66]	train-rmse:43.44009	eval-rmse:52.20317
[67]	train-rmse:43.29717	eval-rmse:52.14245
[68]	train-rmse:43.10437	eval-rmse:52.15464
[69]	train-rmse:43.00768	eval-rmse:52.17011
[70]	train-rmse:42.87951	eval-rmse:52.11852
[71]	train-rmse:42.79951	eval-rmse:52.21249
[72]	train-rmse:42.66769	eval-rmse:52.22331
Mean Squared Error: 2727.2736118611274
Mean Squared Error from loaded model: 2727.2736118611274

train-rmse是训练集上的预测值与真实值之间的误差。eval-rmse是模型在测试集上的 RMSE

分析下早停机制下最后的数据,42.66769 表示在训练集上,模型的预测误差为 42.67。RMSE 越低,表示模型在训练集上拟合得越好。52.22 说明模型在测试集上的预测误差明显高于训练集,表明模型可能存在一定的过拟合问题,模型在训练集上表现良好,但在新数据(测试集)上的泛化能力不如在训练集上的表现。

manim
中绘制一个角度其实就是绘制两条直线,本篇介绍的不是绘制角度,而是绘制
角度标记

对于
锐角

钝角

角度标记
是一个弧,弧的度数与角的度数一样;

对于
直角

角度标记
是一个垂直的拐角。

manim
中关于
角度标记
的模型主要有3个:

  1. Angle
    :根据两条直线绘制角度标记
  2. RightAngle
    :根据两条
    互相垂直
    的线绘制直角标记
  3. Elbow
    :不受限于直线,任意方向和大小的直角标记

其中,
RightAngle
模块继承自
Angle

角度标记
的主要作用是在动画中标记出一些特殊角度,更好的展示数学定理的证明过程。

1. 主要参数

Angle
模块是通用的角度标记,它的主要参数有:

参数名称 类型 说明
line1 Line 构成角度的第一条线
line2 Line 构成角度的第二条线
radius float 角度标记的半径
quadrant Point2D 此参数控制角度标记显示在哪个位置
other_angle bool True
:顺时针从line1到line2
False
:逆时针从line1到line2
dot bool 是否在角度标记中显示一个点
dot_radius float 点的半径
dot_distance float 点到圆弧(角度标记)的相对距离
dot_color Color 点的颜色
elbow bool 是否显示成直角的形状

后面在使用示例中演示这些参数的使用。

RightAngle
模块继承自
Angle
,除了上面
Angle
的参数之外,还有一个自己特有的参数。

参数名称 类型 说明
length float 标记的大小

Elbow
模块与上面两个不一样,它不是根据两条线来生成角度标记。

参数名称 类型 说明
width float 标记的大小
angle float 标记朝向那个方向

Elbow
的形状和
RightAngle
是一样的。

2. 主要方法

Angle
模块的方法主要有3个:

名称 说明
from_three_points 根据三个点来生成角度标记
get_lines 获取生成角度的两条线
get_value 获取角度的值

一般我绘制一个角度标记时,都是根据两条相交的线来确定角度位置的。

通过
from_three_points
方法,可以根据任意3个点来生成一个角度标记。

A = np.array([2, -1, 0])
B = np.array([0, 0, 0])
C = np.array([1, 1, 0])

angle = Angle.from_three_points(A, B, C)

函数的参数是
A

B

C
三个点,

  • A:角度的起点
  • B:角度的顶点
  • C:角度的终点

生成的角度以
B
为顶点,从点A到点C逆时针旋转。

方法
get_lines
可获取构成角度的两条线,也就是上图中的
BA

BC
两条线。

lines = angle.get_lines()

最后,
get_value
方法,可以实时得到当前角度的值,值可以是度数,也可以是弧度。

print(f"角度:{angle.get_value(degrees=True)}")
print(f"弧度:{angle.get_value()}")

# 运行结果
角度:71.56505117707799
弧度:1.2490457723982544

3. 使用示例

3.1. 角度大小

因为角度标记
Angle
是一个弧形,所以角度的大小通过参数
radius
(半径)来调整。

line1 = Line(LEFT, RIGHT)
line2 = Line(DOWN, UP)

Angle(line1, line2)
Angle(line1, line2, radius=0.2)
Angle(line1, line2, radius=0.5)
Angle(line1, line2, radius=0.8)

3.2. 角度位置

角度标记的位置由两个参数来控制,
quadrant

other_angle

quadrant
参数一共有四种选项:
(1, 1)

(1, -1)

(-1, 1)

(-1, -1)

这个参数分两部分,分别表示角度标记在
Line1
上的
起点位置
和在
Line2
上的
终点位置

比如下面相交的两条直线,
quadrant
的第一个值和第二个值分别在
Line1

Line2
上的位置如图。

other_angle
默认为
False
,表示绘制角度时从
Line1

Line2

设置
other_angle
为True时,绘制角度的顺序相反,从
Line2

Line1

l1 = Line(
    LEFT + (1 / 3) * UP,
    RIGHT + (1 / 3) * DOWN,
)
l2 = Line(
    DOWN + (1 / 3) * RIGHT,
    UP + (1 / 3) * LEFT,
)

Angle(l1, l2)
Angle(l1, l2, quadrant=(1, -1))
Angle(l1, l2, quadrant=(-1, 1))
Angle(l1, l2, quadrant=(-1, -1))
Angle(l1, l2, other_angle=True)
Angle(l1, l2, quadrant=(1, -1), other_angle=True)
Angle(l1, l2, quadrant=(-1, 1), other_angle=True)
Angle(l1, l2, quadrant=(-1, -1), other_angle=True)

3.3. 角度中的点

Angle
中可以加一个点的标记,当一个画面中有很多角度的时候,这个标记可以帮助我们区分不同的角。

通过
dot_radius

dot_distance

dot_color
等参数,可以调整点的大小,位置和颜色。

line1 = Line(
    LEFT / 2,
    RIGHT / 2,
)
line2 = Line(
    DOWN / 2,
    UP / 2,
)

Angle(
    line1,
    line2,
    dot=True,
    dot_radius=0.02,
    dot_color=RED,
)
Angle(
    line1,
    line2,
    dot=True,
    dot_radius=0.08,
    dot_color=BLUE,
)
Angle(
    line1,
    line2,
    dot=True,
    dot_distance=0.2,
    dot_color=GREEN,
)
Angle(
    line1,
    line2,
    dot=True,
    dot_distance=0.8,
    dot_color=YELLOW,
)

3.4. 直角标记

最后,还有一个特殊的角度标记--直角标记。

manim
中提供了2个模块来标记直角,
RightAngle

Elbow

它们的显示效果差不多,区别在于,
RightAngle
需要根据两条线来生成,


Elbow
更加灵活一些,它可以在任意位置生成直角标记。

line1 = Line(
    LEFT / 2,
    RIGHT / 2,
)
line2 = Line(
    DOWN / 2,
    UP / 2,
)

RightAngle(
    line1,
    line2,
    length=0.2,
)
RightAngle(
    line1,
    line2,
    length=0.4,
)
RightAngle(
    line1,
    line2,
    quadrant=(1, -1),
)
RightAngle(
    line1,
    line2,
    quadrant=(-1, -1),
)
Elbow(width=0.5)
Elbow(width=1)
Elbow(width=1, angle=PI / 2)
Elbow(width=1, angle=5 * PI / 4)

4. 附件

文中完整的代码放在网盘中了(
angle.py
),

下载地址:
完整代码
(访问密码: 6872)

在电商行业竞争尤为激烈的当下,除了打价格战外,如何有效的控制成本,是每个从业者都在思考的问题

IDM-VTON
是一个
AI
虚拟换装工具,旨在帮助服装商家解决约拍模特导致的高昂成本问题,只需一张服装图片,就可以生成各种身穿该服装的模特,大大简化了传统的产品展示过程

IDM-VTON
最新中文版:

百度网盘:
https://pan.baidu.com/s/1fMTJHrLGr6-CWoFv1LmkDw?pwd=gw15

IDM-VTON
采用了先进的图像识别和视觉检测算法,在用户上传服装图片和模特姿势图后,能在短时间内生成多张商业用级的照片,在操作界面可以直观地看到服装的实际穿着效果

IDM-VTON
为服装商家提供了一个创新的解决方案,不仅节省了场地、拍摄、后期等费用,在降低成本的同时,还加速了新服装上市的效率,提高了商品的竞争力

技术优势

1.
数据学习:通过学习大量的真实穿着图片,扩散模型能够理解衣物在不同体型和肤色上的表现

2.
个性化适配:利用深度学习技术,模型能够根据用户的体型、肤色等特征,生成个性化的试穿效果

3.
环境适应:通过先进的图像处理技术,
IDM-VTON
能够适应不同的光线和背景,确保试穿效果的真实性

应用场景

·
电子商务:提高在线购物体验,让用户在购买前能够更直观地看到衣物的穿着效果

·
时尚设计:帮助设计师快速预览设计效果,加速设计流程

·
个性化推荐:根据用户身材和偏好数据,精准匹配合适的服装

·
社交媒体:博主可以在社交账号上尝试不同的穿衣风格,增强粉丝互动性和娱乐性

使用方法

1.
上传人物图片

2.
上传服装图片

3.
选择服装所在的部位(上半身,下半身,鞋类等)

4.
想要改变服装的大小(如:长袖变短袖),去掉“自动生成遮罩”的勾选,点下画笔后,在上传的人物图片处手动绘制遮罩的面积

5.
提示词(可选),帮助程序更精准的识别衣服,如
Dress
(连衣裙)

6.
点击“试穿运行”开始换装

在控制台可以查看当前的处理进度,程序执行完毕会输出信息“图片处理完成”

稍微等待一下,图片就生成好了,模特的试衣效果非常惊艳!(从
Output
界面右上角下载图片)

注意事项

①项目安装路径不要包含中文

②推荐使用
GTX1060
以上显卡运行此项目

③使用过程中若不慎关闭软件后台,请重新打开,并刷新网页

一位离职的前端同事,最近接了个
React Native
的活儿,遇到许多搞不定的问题,于是找到我帮忙“补课”(没有系统的学习
React Native
,也不具备原生
Android

iOS
开发基础知识)。

此前带过另一位前端同事入门
React Native
开发,有段时间甚至一天得花一两个小时,专门视频连线手把手传帮带,帮忙解决各种疑难杂症。

这可能是纯前端开发小伙伴,刚开始接触 APP 开发最头痛的一段。不管是
React Native
还是
Flutter
,虽然都号称跨平台开发,但如果没有相应平台(
Android

iOS
等)的开发基础,还是很难深入的。一般会卡在下面这些问题上:

  1. 开发环境的搭建和修改,可能涉及到
    JDK

    Maven

    Ruby

    Gems

    CocoaPods
    等;
  2. Android

    iOS
    项目的一些配置,如
    build.gradle

    AndroidManifest.xml

    Info.plist

    Podfile
    等;
  3. Android Studio

    XCode
    的使用;
  4. Android

    iOS
    原生代码的修改,包括
    Java
    /
    Kotlin

    Objective-C
    /
    Swift
  5. C/C++
    源码编译问题的处理;
  6. 命令行工具的使用,如
    Shell
    脚本编写、
    ADB
    的使用、
    react-native run-*
    命令的使用等;
  7. 调试工具的使用,如
    Flipper

    DevTools

    Reactotron
    等;
  8. 各种原生相关的三方库依赖问题处理;
  9. 由各种缓存所引发的问题处理;

另外,可能还会遇到诸如
切换/点击响应很慢、画面卡顿
等问题,
感觉上没有原生的看上去丝滑
。很多人会归咎于非原生,然而大部分时候并非如此。很可能是由于不明白相应的原理,导致写的代码执行效率太差。这是很大一部分纯前端小伙伴的通病。

时间精力允许的情况下,会在这里记录一些相应的问题解决方法。

JDBC

JDBC(Java DataBase Connectivty,Java数据库连接)API,是一种用于执行Sql语句的Java API,可以为关系型数据库提供统一的访问,其由一组Java编写的类和接口组成.

JDBC驱动程序

起初,SUN公司推出JDBC API希望能适用于所有数据库,但实际中是不可能实现的,
各个厂商提供的数据库差异太大,SUN公司于数据库厂商协同之后决定:
由SUN公司提供一套访问数据库的API,各个厂商根据规范提供一套访问自家数据库API的接口,
SUN公司提供的规范API称之为
JDBC
,厂商提供的自家数据库API接口称之为
驱动

JDBC原理

  • 经过SUN公司于各个数据库厂商的协同,JDBC的结构如下图:

JDBC驱动的原理

我们知道了JDBC运行的原理,那么JDBC驱动是怎么运行的呢,我们开始进行探究

  • JDBC驱动的类型


    根据访问数据库数据库技术不同,JDBC驱动程序被访问四类


    • Type1:
      JDBC-ODBC
      桥驱动程序
      • 此类驱动程序由JDBC-ODBC桥金额一个ODBC驱动程序组成
    • Type2:部分Java本地JDBC API驱动程序
      • 此类驱动程序必须在本地计算机上先安装好特点的驱动程序才能进行使用
    • Type3:纯Java的数据库中间件驱动程序(目前主流)
      • Pure Java Driver for Database Middleware
        使用此类驱动时,不需要再本地计算机上安装任何附加软件,但必须再安装数据库管理系统的服务器端加装中间件
        (Middleware)
        ,这个中间件负责所有存取数据库时的必要转换.
        中间件的工作原理是:
        驱动程序将JDBC访问转换为于数据库无关的标准网络协议(通常是HTTP或HTTPS)送出,然后再由中间件服务器将其转换为数据库专用的访问指令,完成对数据库的操作.中间件可以支持对多种数据库的访问.
    • Type4:纯Java的JDBC驱动程序(最理想的驱动程序)
      • Direct-toDatabasePureJavaDriver
        此类驱动程序是之间面向数据库的Java驱动程序,即所谓的”瘦”驱动程序.使用该驱动程序无需安装如何附加软件(包括本地计算机或是数据库服务器端),所有存取的数据库操作都直接由JDBC驱动程序来完成
  • JDBC驱动工作动作


    • 对于第三类Type3驱动程序来说,其是由纯Java语言开发的,此类驱动程序体积最小
      • 下面给出Type3驱动程序的结构图

    - 可见,Type 3,JDBC驱动程序为两层结构,分别为驱动程序客户端和驱动程序服务端,客户端直接于用户交互,**其为用户提供符合JDBC规范的数据库统一编程接口,**客户端将数据请求通过**特定的网络请求**发送值服务器.服务器作为中间件的角色,其负责接收和处理用户的请求,JDBC驱动程序本身不进行直接与数据库的交互,而是借助其他已经实现的驱动,称之为”雇佣”.当然”雇佣”的数量越多,可支持的数据库数量就越多.”雇佣”的数量和成员可以动态的改变,以满足业务的扩展,这也是Type3JDBC性能强大的原因

将JDBC驱动导入idea(以Mysql为例)

  1. 第一步肯定是下载JDBC驱动程序了,各个厂商把特定的驱动程序打包成.jar发布在其官网上大伙可用到官网下载

    下载好后,解压至文件夹备用
  1. 打开idea,在要导入驱动的项目中新建一个名为lib的文件夹(建议命名)

  1. 将下载的zip文件解压,找到其中的mysql-connector-java-8.0.26.jar包,
    将其复制

  1. 复制其中的mysql-connector-java-8.0.18.jar文件,在lib文件夹上右键,粘贴到IDEA中,刚刚新建的lib文件夹里

使导入的驱动生效

  1. 在idea点击File→Project Structure

  1. 在Modules中选择Denpendencies

  1. 点击左侧+号,选择 JARS or directories

  1. 在弹出的窗口中选择刚刚导入 lib 文件夹的驱动,点击Ok

  1. 可以看到Module模块中,多出了一个mysql驱动,选择之后点击Apply,然后Ok

JDBC中的的类及其应用

JDBC API中包含四个常用的接口和一个类,分别是
Connection
接口,
Statement
接口,
PreparedStatement
接口,
ResultSet
接口,
DriverManager
类,jar包中已经包含这些接口的实现类直接使用即可

Statement接口

Statement接口是Java程序执行数据库操作的重要接口,用于已经建立了数据库连接的基础上,向数据库发送要执行的Sql语句

  • 作用
    :执行不带参数的简单Sql语句
  • 主要方法
    • void addBatch(String sql )throws SQLException
      :该方法用于将Sql语句添加到Statement对象的当前命令列表中,
      用于Sql语句的批量处理
    • void clearBatch() throws SQLException
      :立即
      释放
      Statement对象中的命令列表
    • boolean excute(String sql) throws SQLException
      :
      执行指定的Sql语句
      ,成功返回
      true
      否则返回
      false
    • int[] excuteBatch() throws SQLException
      :
      将命令列表中的sql命令提交执行
      ,返回一个int数组表示每个sql语句影响的行数
    • ResultSet excuteQuery(String sql) throws SQLException
      :该方法用于
      执行查询类型(Select类型)的Sql语句,
      返回的查询所获取的结果集
      ResultSet
      对象
    • void close() throws SQLException
      :用于立即释放此Statement对象的数据库和JDBC资源

Connection接口

Connection接口位于java.sql包中,是用于与数据库连接的对象,只有获取了与数据库连接的对象后,才能访问数据库进行操作

  • 作用
    :与数据库进行连接

  • 主要方法


    • Statement createStatement() throws SQLException
      :用于创建一个Statement对象,用于执行Sql语句

    • PreparedStatement prepareStatement(String sql) throws SQLException
      : 创建一个PreparedStatement对象,用于执行预编译的Sql语句

    • CallableStatement prepareCall(String sql)throws SQLException
      :创建一个CallableStatement对象用于执行存储过程或函数

    • void commit() throws SQLException
      :提交当前事务

    • void rollback() throws SQLException
      :回滚事务

    • void close() throws SQLException
      :关闭连接


      在进行数据库连接的时候还要用到DriverManager类中的
      getConnection(url,username,password)
      方法


    E.g:

            String url = "jdbc:mysql://localhost:3306/demo";
            String username = "root";
            String password = "root";
            //建立连接
            Connection connection = DriverManager.getConnection(url, username, password);
            String sql = "SELECT * FROM dept";
            //创建Statement对象执行查询操作
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            //处理查询结果
            while (resultSet.next()) {
                String dId = resultSet.getString("d_id");
                String dName = resultSet.getString("d_name");
                String loc = resultSet.getString("loc");
                System.out.println(dId + " " + dName + " " + loc);
            }
            connection.close();
            statement.close();
            resultSet.close();
        }
    

DriverManager类

DriverManager类是JDBC API的核心,该类中包含了与数据库交互操作的方法,类中的方法都由数据库厂商提供

  • 作用:
    管理和协调不同的JDBC驱动程序
  • 主要方法
    • public static Connection getConnection(String url, String user, String password)throws SQLException
      :根据指定的数据库url,用户名以及密码建立数据库连接
    • public static Connection getConnection(String url,Properties info)
      :根据指定的数据库url以及连接属性建立数据库连接
    • public static synchronized void deregisterDriver(Driver driver) throws SQLException
      :从DriverManager管理列表中删除一个驱动,driver参数是要删除的驱动对象

PreparedStatement接口

PreparedStatement接口位于java.servlet包中,其继承了Statement接口

  • 与Statement的区别:
    • 执行速度较快:
      PreparedStatement对象是已经预编译过的,执行速度快于Statement.
      因此若要执行大量的Sql语句时使用PreparedStatement以提高效率
  • 主要方法
    • setXXX()


      此类方法都是设置sql语句中传入的参数里类型


      • void setBinaryStream(int parameterIndex,InputStream x) throws SQLException
        :将二进制流作为sql语句传入的参数,二进制流可用高效地处理图片,音频,视频登媒介,
        parameterIndex
        是参数位置索引
      • void setBoolean(int parameterIndex,boolean x) throws SQLException
        :将boolen作为sql传入的参数类型,parameterIndex为参数位置索引
      • void setByte(int parameterIndex,byte x) throws SQLException
        :将byte作为sql传入的参数类型
      • void setDate(int parameterIndex,Date x) throws SQLException
        : 将java.sql.Date值x做为SQL语句中的参数值
      • void setDouble(int parameterIndex,double x)
        :将double值x做为SQL语句中的参数值
      • void setInt(int parameterIndex,int x) throws SQLException
        :将int值x做为SQL语句中的参数值
      • void setObject(int parameterIndex,Object x) throws SQLException
        :将object对象x做为SQL语句中的参数值
      • void setString(int parameterIndex,String x) throws SQLException
        : 将String值x做为SQL语句的参数值
      • void setTimestamp(int parameterIndex,Timestamp x) throws SQLException
        : 将java.sql.Timestamp值x做为SQL语句中的参数值
    • int executeUpdate() throws SQLException
      :
      executeUpdate()
      方法返回的
      int
      值表示受影响的行数。如果返回值为 0,则可能表示没有符合条件的记录被修改;执行
      INSERT

      UPDATE

      DELETE
      这些DML语句时同理

ResultSet接口

是用于接收查询的结果,是结果集合,当你执行一个SELECT语句时DBMS会返回一个包含查询结果的数据表,ResultSet接收用于表现这个数据表的对象

  • 作用:
    表示查询后的返回值
  • 主要方法
    • Boolean next() throws SQLException
      :移动游标到结果集的下一行,并返回一个boolen值,结尾返回false
    • getXXX(String columnLabel)
      :获取指定列名的值,XXX表示Java数据类返回XXX类型,如getString(),
      columnLabel
      表示列名
    • getXXX(int columnIndex)
      :获取指定列索引的值,列索引从 1 开始

SQL注入

所谓SQL注入,是值通过把恶意SQL语句插入到Web表单提交或页面请求的查询字符串,最终达到欺骗服务器的结果

SQL注入实例

  • 对于一个简单的登入功能,关键函数如下:
 static boolean noProtectLogin(String username, String password, Statement statement) throws SQLException {
        //username="abc";
        //password = "or '1'='1'";
        String sql = "SELECT *FROM  user WHERE username= '" + username + "'AND password=+''";
        ResultSet resultSet = statement.executeQuery(sql);
        return resultSet.next();
    }
  • 方法中的username于password没有进行任何处理,直接接受前端传入的数据,这样拼接的SQL语句会发送注入漏洞
  • 若把password参数修改成
    "or '1'='1'"
    ,username为任意值,那么这条语句结果为
    SELECT *FROM user WHERE username= 'abc' AND password= 'or '1'='1''
    显然这条语句的一直是true,这样可以把user表中的所有用户信息查询到,就可以成功实现无密码登入

SQL预编译

也称之为SQL预处理是一种,可以提高sql语句安全性和性能的技术

  • 预编译SQL语句允许在运行之前定义SQL语句结构,同时使用占位符(通常是问号
    ?
    )来动态表示数据部分

  • 在Java中可以使用
    PreparedStatement
    来创建和执行预编译的SQL语句,刚刚的登入操作使用
    PreparedStatement
    操作的代码如下

    static boolean noProtectLogin(String username, String password, Connection connection) throws SQLException {
    //      username="abc";
    //      password = "or '1'='1'";
            String sql = "SELECT * FROM  user WHERE username= ? AND password = ? ";
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setString(1, username);
            statement.setString(2, password);
            ResultSet resultSet = statement.executeQuery();
            return resultSet.next();
        }
    

    • 这样我们在进行恶意的SQL注入,如把password定义成
      "or '1'='1'"
      执行结果直接返回false

事务

什么是事务? 官方的说法:事务是访问数据库的一个操作序列,数据库应用系统通过执行业务集合要完成对数据库的存取,简单来说:
事务是执行工作操作中最小的不可再分的工作单位
,通常一个业务对应一个事务,多个操作同时进行要么同时成功,要么同时失败,这就是事务

事务的理解

事务的特性

  • 原子性: 即不可分割,事务要么全部被执行,要么全部不执行
    .若所有的事务都提交成功,那么数据库操作被提交,数据库状态发生变化,
    若有一个子事务失败,那么其余事务的数据库操作都会回滚
    ,即数据库状态回到事务执行之前,保持状态不变
  • 一致性:事务的执行使得数据库从一种正确状态转换为另一个正确状态
  • 隔离性:在事务正确提交之前,不允许把事务对该数据的改变提交给其他事务,即在正确提交之前,其可能的结果不应该用于给其他事务
  • 持久性:即事务正确提交之后,其结果会永远保存在数据库之中,即事务提交之后有了其他故障,事务的处理结果也会得到保存

事务的通俗例子

  • 假设张三要给李四转账,要完成这个操作,要执行两个事务,一个是:扣除张三的账户余额;另一个是:李四的账余额增加
    ,这两个事务是不可分割的

事务的作用

  • 主要作用:保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不会破坏后台的数据完整性,拿ATM机举例子,若ATM在操作过程中突然出现故障,此时事务必须确保故障前对账号的操作不生效,确保用户于银行的利益不受损

JDBC中对事物进行管理

在JDBC中,Connection接口中定义了几个对事务操作的方法,我们一一讲解

  • void setAutoCommit*(*boolean autoCommit*)* throws SQLException
    :在默认情况下JDBC连接处于自动提交模式,这意味着每个SQL语句执行后都会立即提交,这明显不符合事务的特性,要我们进行显性关闭,即将
    autoCommit
    设置为false
  • void commit()
    :若所有操作都执行成功调用该方法提交事务
  • void rollback()
    :若事务中任何操作失败或出现异常错误则需调用
    rollback()
    回滚事务

银行存/取款举例

拿刚刚的张三和李四的例子说明

try {
            String sql1 = "UPDATE bank SET balance = balance - ? WHERE b_id=?";
            String sql2 = "UPDATE bank SET balance = balance + ? WHERE b_id=?";

            stmt1 = conn.prepareStatement(sql1);
            stmt2 = conn.prepareStatement(sql2);
            //事务1执行

            stmt1.setDouble(1, 500);
            stmt1.setString(2, "01");
            int r1 = stmt1.executeUpdate();
            //事务2执行

            stmt2.setDouble(1, 500);
            stmt2.setString(2, "02");
            int r2 = stmt2.executeUpdate();

            if (r1 == 1 && r2 == 1) {
                System.out.println("业务执行成功");
                conn.rollback();
            } else {
                System.out.println("业务执行失败");
                conn.commit();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            //有异常回滚事务
            conn.rollback();
        } finally {
            Objects.requireNonNull(stmt1).close();
            Objects.requireNonNull(stmt2).close();
            conn.close();
        }

连接池

连接池是创建和管理数据库连接的技术,这些连接随时准备被任何需要它的线程使用

连接池的原理

  • 连接池的基本思想是在系统初始化时,将数据库连接
    作为对象存储在运行内存中
    ,
    当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使用完毕后,用户也并非将连接之间关闭,而是将连接放回连接池中,提供给下一次请求访问使用
    ,而连接池的建立,断开都由连接池自身来管理.同时,还可以
    通过设置连接池的参数来控制连接池中的初始连接数,连接的上下限数以及每个连接的最大使用次数,最大空闲时间等等
  • 连接池参数作用
    • 最小连接数:是连接池一直保持于数据库连接的数量
      ,因此若应用程序对数据库连接的使用量不大还设置较大的最小连接数,会造成大量的连接资源的浪费
    • 最大连接数:是连接池能申请的最大连接数,若数据库连接请求超过最大连接数,后续的数据库连接请求将被加入到等待队列中
    • 若min连接于max连接相差很大时,那么最先连接请求将会获利,之后超过min连接的连接请求等价于新建一个数据库连接,但这些大于min连接的数据库连接在使用之后不会马上被释放,将被放入连接池中等待重复利用

C3P0

C3P0是一个开放源代码的JDBC连接池,包括了jdbc3和jdbc2扩展规范说明的Connection和Statement池的DataSources对象

C3P0的配置

导入方法于导入JDBC-Mysql jar包类似这里不再赘述

  • 接着要配置c3p0-config.xml文件(这里选择xml配置)→直接配置到src文件夹下,否则会报配置错误

  • c3p0-config.xml文件的一般模板(可直接拷贝使用):

    <c3p0-config>
        <default-config>
            <!-- 数据库驱动名 -->
            <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
    
            <!-- 数据库的url -->
            <property name="jdbcUrl">jdbc:mysql://localhost:3306/demo</property>
    
            <!--用户名。Default: null -->
            <property name="user">root</property>
    
            <!--密码。Default: null -->
            <property name="password">root</property>
    
            <!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
            <property name="initialPoolSize">3</property>
    
            <!--连接池中保留的最大连接数。Default: 15 -->
            <property name="maxPoolSize">5</property>
            <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
    		
            <property name="acquireIncrement">3</property>
            <!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
            <property name="maxIdleTime">60</property>
    
            <!--当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出 SQLException,如设为0则无限期等待。单位毫秒。Default:  0 -->
            <property name="checkoutTimeout">0</property>
        </default-config>
    </c3p0-config>
    

C3P0的使用方法

C3P0
中只有一个类,显然这个类是最主要的,其包含了C3P0所有对数据库连接的操作,接下来我们开始讲解

ComboPooledDataSource类

  • 主要方法


    • Connection getConnection()
      :从连接池中获取一个数据库连接,若当前没有空闲连接,则新建连接,直到达到最大连接数.其返回一个
      Connection
      对象

    除了使用xml文件配置还可以在代码中直接使用
    ComboPooledDataSource
    类中的方法配置,但一般使用xml文件,避免冗余


    • void setXXX()
      :设置C3P0中的各类属性,XXX表示属性,例如
      setUser(),setMinPoolSize(int min)
      等等
  • 使用连接池测试更新语句

    public static void main(String[] args) throws SQLException {
            //建立连接池,获取连接
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            Connection conn = dataSource.getConnection();
            //用连接池测试更新语句
            String sql = "SELECT * FROM  user WHERE username= ? AND password = ? ";
            PreparedStatement stmt = conn.prepareStatement(sql);
    
            stmt.setInt(1, 11111);
            stmt.setString(2, "11111111");
            ResultSet resultSet = stmt.executeQuery();
            if (resultSet.next()) {
                int username = resultSet.getInt("username");
                String password = resultSet.getString("password");
                System.out.println("username:" + username + "password:" + password);
            }
            stmt.close();
            conn.close();
        }
    

Druid(最好用的Java连接池)

Druid是目前最好数据库连接池,在功能.性能.扩展性方面都吊打其他连接池,包括
DBCP,C3P0,BoneCP,Proxool
等等

Druid配置

  • Druid执行要一个jar包大家可以去官网:
    https://repo1.maven.org/maven2/com/alibaba/druid/下载所需的jar包,在导入即可

  • Druid的参数列表


    属性(Parameter) 默认值(Default) 描述(Description)
    username **** 连接数据库的用户名
    password **** 连接数据库的密码
    jdbcUrl **** 同C3P0中的jdbcUrl属性
    driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
    initialSize 0 *
    初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 参见DBCP中的initialSize属性
    maxActive 8 最大连接池数量
    (Maximum number of Connections a pool will maintain at any given time.
    maxIdle 8 已经不再使用,配置了也没效果
    minIdle **** 最小连接池数量
    maxWait **** 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
    poolPreparedState- ments false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。
    maxOpenPrepared- Statements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。      在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
    testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
    testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
    testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
    validationQuery **** 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、 testWhileIdle都不会其作用。在mysql中通常为select 'x',在oracle中通常为select 1 from dual
    timeBetweenEviction-RunsMillis **** 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据
    minEvictableIdle- TimeMillis **** Destory线程中如果检测到当前连接的最后活跃时间和当前时间的差值大于minEvictableIdleTimeMillis,则关闭当前连接。
    removeAbandoned **** 对于建立时间超过removeAbandonedTimeout的连接强制关闭
    removeAbandoned-Timeout **** 指定连接建立多长时间就需要被强制关闭
    logAbandoned false 指定发生removeabandoned的时候,是否记录当前线程的堆栈信息到日志中
    filters **** 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 1)监控统计用的filter:stat 2)日志用的filter:log4j  3)防御sql注入的filter:wall

    • 红色属性为必要配置属性
    • 定义application.properties(名字可以顺便取但必须是properties),可以放置任意目录下


    • application.properties文件一般模板如下

    driverClassName=com.mysql.cj.jdbc.Driver
    url=jdbc:mysql://localhost:3306/test?userSSL=false&serverTimezone=Asia/Shanghai
    username=root
    password=123456
    initialSize=3
    maxActive=5
    maxWait=1000
    

Druid使用方法

Druid也是只要一个核心类
DruidDataSource
它实现了
javax.sql.DataSource
接口

  • 主要类与方法


    • DruidDataSource()构造方法
      :构造一个默认的 DruidDataSource实例这个实例主要用于显性配置Druid属性
      • setXXX():
        设置属性值,如
        setUrl(String url)
        ,
        setInitialSize(int initialSize)
        等等
    • DruidDataSourceFactory
      类:是一个工厂类,用于根据提供的配置信息创建
      DruidDataSource
      实例,这个类简化了从配置文件中加载配置信息并创建
      DruidDataSource
      的过程
      • createDataSource(Properties properties)
        :最常用的方法之一,其接受一个Properties对象作为参数,从该对象读取配置信息,并创建一个
        DruidDataSource
  • 将Druid封装为JdbcUtils类

    public class JdbcUtil {
        private static DataSource dataSource;
    
        static {
            //先建立配置,连接连接池
            try {
                InputStream inputStream = JdbcUtil.class.getResourceAsStream("resource/application.properties");
                Properties props = new Properties();
                props.load(inputStream);
                dataSource = DruidDataSourceFactory.createDataSource(props);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static Connection getConnection() {
            Connection conn = null;
            try {
                conn = dataSource.getConnection();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    
        public static void close(ResultSet rs, PreparedStatement stmt, Connection conn) {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (stmt != null) {
                    stmt.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 本文借鉴同平台许多作者如
    @少平的博客人生,@chy_18883701161,@滥好人