From e529327f5c083492d852fa2f35246a9b4f116b6b Mon Sep 17 00:00:00 2001 From: Tyhoo Wu Date: Sat, 31 Aug 2024 08:16:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AI/linear-regression.md | 417 ---- AI/maximum-likelihood-estimation.md | 185 -- ...y-need-gradient-arg-in-pytorch-backward.md | 176 -- BigData/flink-kafka-sink-multiple-topics.md | 156 -- BigData/flink-stream-processing-theory.md | 198 -- BigData/how-to-install-opentsdb.md | 169 -- BigData/the-road-of-exploring-opentsdb.md | 676 ------- DesignPattern/adapter-pattern.md | 250 --- DesignPattern/bridge-pattern.md | 61 - DesignPattern/builder-pattern.md | 271 --- .../chain-of-responsibility-pattern.md | 105 - DesignPattern/command-pattern.md | 105 - DesignPattern/composite-pattern.md | 262 --- DesignPattern/decorator-pattern.md | 233 --- DesignPattern/facade-pattern.md | 153 -- DesignPattern/factory-pattern.md | 239 --- DesignPattern/flyweight-pattern.md | 155 -- DesignPattern/interpreter-pattern.md | 145 -- DesignPattern/iterator-pattern.md | 201 -- DesignPattern/mediator-pattern.md | 132 -- .../object-oriented-design-principles.md | 103 - DesignPattern/overview.md | 112 -- DesignPattern/prototype-pattern.md | 137 -- DesignPattern/proxy-pattern.md | 126 -- DesignPattern/singleton-pattern.md | 182 -- .../non-technical-interview-questions.md | 915 --------- Java/alibaba-java-coding-guidelines-1.md | 562 ------ Java/alibaba-java-coding-guidelines-2.md | 212 -- Java/dynamic-proxy-in-java.md | 595 ------ Java/java-in-the-future.md | 180 -- Java/resultmap-in-mybatis-plus.md | 370 ---- Java/testable-mock.md | 358 ---- LeetCode/0020-valid-parentheses.md | 121 -- LeetCode/0027-remove-element.md | 114 -- LeetCode/0045-jump-game-ii.md | 156 -- LeetCode/0055-jump-game.md | 112 -- LeetCode/0070-climbing-stairs.md | 75 - .../0094-binary-tree-inorder-traversal.md | 271 --- LeetCode/0101-symmetric-tree.md | 233 --- .../0102-binary-tree-level-order-traversal.md | 125 -- .../0121-best-time-to-buy-and-sell-stock.md | 91 - ...0122-best-time-to-buy-and-sell-stock-ii.md | 143 -- LeetCode/0136-single-number.md | 80 - .../0144-binary-tree-preorder-traversal.md | 226 --- .../0145-binary-tree-postorder-traversal.md | 198 -- LeetCode/0206-reverse-linked-list.md | 182 -- LeetCode/0746-min-cost-climbing-stairs.md | 98 - LeetCode/README.md | 4 - Life/drunk-post-of-a-programmer.md | 78 - .../how-to-prove-that-you-are-a-programmer.md | 109 -- Life/study-vs-work.md | 430 ----- ...he-most-common-lies-told-by-programmers.md | 259 --- MySQL/how-to-use-mysql-explain.md | 692 ------- MySQL/mysql-index-theory-and-best-practice.md | 479 ----- MySQL/mysql-transaction-innodb-mvcc.md | 667 ------- Netty/the-truth-of-netty.md | 1714 ----------------- ProblemResearch/data-from-mongodb-to-kafka.md | 86 - .../hbase-region-server-cannot-start.md | 26 - .../kafka-broker-already-registered.md | 45 - Python/python-environment.md | 213 -- TODO/some-blogs.md | 26 - Tool/about-git.md | 231 --- Tool/awesome-sites.md | 229 --- Tool/github-page-docsify.md | 65 - Tool/how-to-adjust-jd-gui-fontsize.md | 40 - Tool/image-to-latex.md | 183 -- Tool/kafka-commands.md | 105 - Tool/kafka-write-speed.md | 41 - Tool/markdown-formula.md | 937 --------- Tool/work-on-deepin-linux.md | 208 -- about/Friends.md | 37 - about/README.md | 76 - 72 files changed, 17346 deletions(-) delete mode 100644 AI/linear-regression.md delete mode 100644 AI/maximum-likelihood-estimation.md delete mode 100644 AI/why-need-gradient-arg-in-pytorch-backward.md delete mode 100644 BigData/flink-kafka-sink-multiple-topics.md delete mode 100644 BigData/flink-stream-processing-theory.md delete mode 100644 BigData/how-to-install-opentsdb.md delete mode 100644 BigData/the-road-of-exploring-opentsdb.md delete mode 100644 DesignPattern/adapter-pattern.md delete mode 100644 DesignPattern/bridge-pattern.md delete mode 100644 DesignPattern/builder-pattern.md delete mode 100644 DesignPattern/chain-of-responsibility-pattern.md delete mode 100644 DesignPattern/command-pattern.md delete mode 100644 DesignPattern/composite-pattern.md delete mode 100644 DesignPattern/decorator-pattern.md delete mode 100644 DesignPattern/facade-pattern.md delete mode 100644 DesignPattern/factory-pattern.md delete mode 100644 DesignPattern/flyweight-pattern.md delete mode 100644 DesignPattern/interpreter-pattern.md delete mode 100644 DesignPattern/iterator-pattern.md delete mode 100644 DesignPattern/mediator-pattern.md delete mode 100644 DesignPattern/object-oriented-design-principles.md delete mode 100644 DesignPattern/overview.md delete mode 100644 DesignPattern/prototype-pattern.md delete mode 100644 DesignPattern/proxy-pattern.md delete mode 100644 DesignPattern/singleton-pattern.md delete mode 100644 Interview/non-technical-interview-questions.md delete mode 100644 Java/alibaba-java-coding-guidelines-1.md delete mode 100644 Java/alibaba-java-coding-guidelines-2.md delete mode 100644 Java/dynamic-proxy-in-java.md delete mode 100644 Java/java-in-the-future.md delete mode 100644 Java/resultmap-in-mybatis-plus.md delete mode 100644 Java/testable-mock.md delete mode 100644 LeetCode/0020-valid-parentheses.md delete mode 100644 LeetCode/0027-remove-element.md delete mode 100644 LeetCode/0045-jump-game-ii.md delete mode 100644 LeetCode/0055-jump-game.md delete mode 100644 LeetCode/0070-climbing-stairs.md delete mode 100644 LeetCode/0094-binary-tree-inorder-traversal.md delete mode 100644 LeetCode/0101-symmetric-tree.md delete mode 100644 LeetCode/0102-binary-tree-level-order-traversal.md delete mode 100644 LeetCode/0121-best-time-to-buy-and-sell-stock.md delete mode 100644 LeetCode/0122-best-time-to-buy-and-sell-stock-ii.md delete mode 100644 LeetCode/0136-single-number.md delete mode 100644 LeetCode/0144-binary-tree-preorder-traversal.md delete mode 100644 LeetCode/0145-binary-tree-postorder-traversal.md delete mode 100644 LeetCode/0206-reverse-linked-list.md delete mode 100644 LeetCode/0746-min-cost-climbing-stairs.md delete mode 100644 LeetCode/README.md delete mode 100644 Life/drunk-post-of-a-programmer.md delete mode 100644 Life/how-to-prove-that-you-are-a-programmer.md delete mode 100644 Life/study-vs-work.md delete mode 100644 Life/what-are-the-most-common-lies-told-by-programmers.md delete mode 100644 MySQL/how-to-use-mysql-explain.md delete mode 100644 MySQL/mysql-index-theory-and-best-practice.md delete mode 100644 MySQL/mysql-transaction-innodb-mvcc.md delete mode 100644 Netty/the-truth-of-netty.md delete mode 100644 ProblemResearch/data-from-mongodb-to-kafka.md delete mode 100644 ProblemResearch/hbase-region-server-cannot-start.md delete mode 100644 ProblemResearch/kafka-broker-already-registered.md delete mode 100644 Python/python-environment.md delete mode 100644 TODO/some-blogs.md delete mode 100644 Tool/about-git.md delete mode 100644 Tool/awesome-sites.md delete mode 100644 Tool/github-page-docsify.md delete mode 100644 Tool/how-to-adjust-jd-gui-fontsize.md delete mode 100644 Tool/image-to-latex.md delete mode 100644 Tool/kafka-commands.md delete mode 100644 Tool/kafka-write-speed.md delete mode 100644 Tool/markdown-formula.md delete mode 100644 Tool/work-on-deepin-linux.md delete mode 100755 about/Friends.md delete mode 100755 about/README.md diff --git a/AI/linear-regression.md b/AI/linear-regression.md deleted file mode 100644 index e0a7710..0000000 --- a/AI/linear-regression.md +++ /dev/null @@ -1,417 +0,0 @@ -# 从线性回归走进机器学习 - -## 认识线性回归 - -> 回归,是指研究一组随机变量(`Y1 ,Y2 ,…,Yi`)和另一组(`X1,X2,…,Xk`)变量之间关系的统计分析方法。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210810145620814-401160334.png) - -线性回归是统计学中最基础的数学模型,在很多学科的研究中都能看到线性回归的影子,比如量化金融、计量经济学等等。线性回归通过对已有数据建模,从而实现对未知数据的预测。下面会通过一个房价预测的例子来详细说明。 - -数据来自[https://www.kaggle.com/kennethjohn/housingprice](https://www.kaggle.com/kennethjohn/housingprice),该数据集提供了波特兰市47套房屋的面积(`HouseSize`)、卧室数量(`Bedrooms`)和价格(`Price`),如下: - -```python -import pandas as pd - -data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip') -print(data.head(5)) -``` - -| 房屋面积(平方英尺) | 卧室数量 | 价格(美元) | -| :------------------: | :------: | :----------: | -| 2104 | 3 | 399900 | -| 1600 | 3 | 329900 | -| 2400 | 3 | 369000 | -| 1416 | 2 | 232000 | -| 3000 | 4 | 539900 | - -基于已有数据,我们希望通过计算机的学习,找到数据中的规律,并用来预测其他房屋的价格。这是机器学习最朴素的应用场景。这个过程也被称为监督学习(`Supervised Learning`),即给定一些数据,使用计算机学习到一种模型,然后用它来预测新的数据。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210812151605054-1627775632.png) - -在房价预测的例子中,想要预测的目标值房价是连续的,我们称这类问题为回归(`Regression`)问题。与之相对应,当目标值只能在一个有限的离散集合里选择,比如预测房价是否大于100万,结果只有“是”和“否”两种选项,我们称这类问题为分类(`Classification`)问题。 - -## 线性回归初体验 - -上面我们知道,通过训练已有数据,得到模型,然后预测其他房屋的价格,这里,先使用`scikit-learn`这个开源的python机器学习库感受一下。 - -官网:[https://scikit-learn.org/stable/index.html](https://scikit-learn.org/stable/index.html) - -(初次接触机器学习没有环境的朋友可以参考之前的文章《Python开发环境搭建》:[https://www.cnblogs.com/bytesfly/p/python-environment.html](https://www.cnblogs.com/bytesfly/p/python-environment.html)) - - -```python -import numpy as np -import pandas as pd -from sklearn.linear_model import SGDRegressor -from sklearn.pipeline import make_pipeline -from sklearn.preprocessing import StandardScaler - -# 获取数据集 -data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip') - -# 特征标准化,并选择梯度下降法进行线性回归 -reg = make_pipeline(StandardScaler(), SGDRegressor()) -# 机器学习(模型训练) -reg.fit(data[['HouseSize', 'Bedrooms']], data['Price']) - -# 待预测数据 -predict_data = np.array([ - [1000, 2], - [3000, 3], - [4000, 4], - [5000, 5] -]) -# 预测 -result = reg.predict(predict_data) -# 打印预测结果 -print(np.c_[predict_data, result].astype(np.int)) -``` -预测结果: - -| 房屋面积(平方英尺) | 卧室数量 | 价格(美元) | -| :------------------: | :------: | :----------: | -| 1000 | 2 | 211108 | -| 3000 | 3 | 480319 | -| 4000 | 4 | 610837 | -| 5000 | 5 | 741356 | - -当然,实际生产中需要经过多次调优,且模型评估结果达到一定的要求才能正式上线。 - -## 特征工程 - -在讨论线性回归的数学表示之前,我觉得有必要提一下特征工程(`Feature Engineering`)。上面“线性回归初体验”的代码注释中,有`特征标准化`字眼,对应的代码是`StandardScaler()`,初学者应该会疑惑这个地方是什么意思,为什么需要这一步。 - -> 数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。 - -可见,特征工程在机器学习中占有相当重要的地位。在实际应用当中,可以说特征工程是机器学习成功的关键。 - -> Feature engineering is the process of using domain knowledge of the data to create features that make machine learning algorithms work. - -如果你想要你的预测模型达到最佳,你不仅要选择最优的算法,还要尽可能的从原始数据中获取更多有价值的信息。这就是特征工程要做的事,它的目的是得到更好的训练数据,是使用专业背景知识和技巧处理数据,使得特征能在机器学习算法上发挥更好的作用这一过程。 - -换句话说,特征工程就是一个把原始数据转化成特征的过程,这些特征可以很好的描述这些数据,并且利用它们建立的模型在未知数据上的表现效果可以尽可能的好。从数学的角度来看,特征工程就是人工地去设计输入自变量`X`。 - - - -特征工程包含以下内容: - -- 特征提取:将任意数据(如文本、图像、声音等)转换为可用于机器学习的数字特征,因为机器学习进行的是数值相关的运算处理。这里我们收集影响房价的因素,比如房屋面积大小、卧室数量、出门到附近地铁口的距离、周围学校的数量等数字特征。 - -- 特征选择:从特征集合中挑选一组最具统计意义的特征子集,从而达到`降维`的效果。这里我们为了简化问题,只选择`房屋面积`和`卧室数量`这两个属性作为特征值。实际生产中肯定需要根据专业背景知识选择特征值。 - -- 特征预处理:通过一些转换函数将特征数据转换成更加适合算法模型的特征数据。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210816094548806-1299955245.png) - -> The `sklearn.preprocessing` package provides several common utility functions and transformer classes to change raw feature vectors into a representation that is more suitable for the downstream estimators. - -`Feature Scaling`(常叫”特征归一化“、”标准化“),是特征预处理中的重要技术,有时甚至决定了算法能不能work以及work得好不好。为什么需要`Feature Scaling`呢? - -1. 特征间的单位(尺度)可能不同,比如这里的房屋面积和卧室数量,房屋面积的变化范围可能是[800,8000],卧室数量的变化范围可能是[1,5],在计算时,尺度大的特征(房屋面积)会起决定性作用,而尺度小的特征(卧室数量)其作用可能会被忽略,为了消除特征间单位和尺度差异的影响,以对每维特征同等看待,需要对特征进行归一化。 -2. 可以有效提高梯度下降(`Gradient Descent`)的收敛速度。等后面讲过梯度下降法应该就能明白。 - -先看看如何使用`sklearn.preprocessing`下的`StandardScaler`: - -```python -import pandas as pd -from sklearn.preprocessing import StandardScaler - -# 获取数据集 -data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip', - usecols=['HouseSize', 'Bedrooms']) -# 这里为了演示,只选择前5行数据进行特征标准化 -data = data[:5] - -scaler = StandardScaler() -# Compute the mean and std to be used for later scaling -scaler.fit(data) -# Perform standardization by centering and scaling -print(scaler.transform(data)) -``` - -这样,就把前5行原始数据转换成了: - -```bash -[[ 0. 0. ] - [-0.88604177 0. ] - [ 0.52037374 0. ] - [-1.20951734 -1.58113883] - [ 1.57518537 1.58113883]] -``` - -那么,`StandardScaler`到底对数据做了怎样的转换呢? - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210816101124628-193119658.png) - -注意:该转换作用于每一列(即每个特征)。其中`mean`表示这一列数据的平均值,`σ`表示这一列数据的标准差。 - -这样,原始数据就被映射到均值为`0`,标准差为`1`范围内。 - -如何证明呢?其实并不困难。均值为0很显然,每个原始值都减去`mean`,再求和必然为0,均值也就自然是0。再看标准差: - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210816110104427-1559209730.png) - -把均值0代进去,就变成了求x的平方和了,再看分子、分母,其实是相等的(能`get`到?想想均值与标准差之间的联系?标准差是如何来的?)。如此一来算出来的结果正好是1。 - -清楚了`StandardScaler`的转换过程,我们也可以自己实现一下,如下: - -```python -import numpy as np -import pandas as pd - -# 获取数据集 -data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip', - usecols=['HouseSize', 'Bedrooms']) -# 这里为了演示,只选择前5行数据进行标准化 -data = data[:5] - -for column in data.columns: - # 取出每一列(即每个特征)的数据 - value = data[column] - # 求平均值 - mean = np.mean(value) - # 求标准差 - std = np.std(value) - # 对原始数值进行转换 - data[column] = (value - mean) / std - -print(data) -``` - -运行结果如下: - -```bash - HouseSize Bedrooms -0 0.000000 0.000000 -1 -0.886042 0.000000 -2 0.520374 0.000000 -3 -1.209517 -1.581139 -4 1.575185 1.581139 -``` - -对比前面用`sklearn.preprocessing`下的`StandardScaler`计算出来的结果是一样的。 - -温馨提示:上面代码中求标准差时用的是`numpy`中的`std`(计算时默认除以`N`),而不是`pandas`中的`std`(计算时默认除以`N-1`),否则计算出来的结果会有些差异。 - -## 线性回归的数学表示 - -现在把线性回归问题扩展到更一般的场景。假设x是多元的,或者说是多维的。比如,预测房价,需要考虑房屋面积大小、卧室数量、出门到附近地铁口的距离、周围学校或商场的数量等等。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210816170319986-1823209032.png) -这里,训练已有房价数据就是为了确定参数`w`,一旦确定了`w`,就能根据`x`的取值,求出`y`,也就预测出了房价。 - -此时线性回归的监督学习过程就可以被定义为:给定n个数据对`(x, y)`,寻找最佳参数`w` ,使模型可以更好地拟合这些数据。 - -如何衡量模型是否以最优的方式拟合数据呢? - -机器学习中常用损失函数(`Loss Function`)来衡量这个问题。损失函数又称为代价函数(`Cost Function`),它计算了模型预测值`y`和真实值`y`之间的差异程度。从名字也可以看出,这个函数计算了模型犯错的损失或代价,一般来说,损失函数值越小,数据拟合得可能越好(这里暂不考虑过拟合的情况)。 - -对于线性回归,一个简单实用的损失函数计算方式是预测值与真实值误差的平方的均值。数理统计中叫做均方误差(`MSE: Mean Squared Error`)。`MSE`的值越小,说明预测模型具有更好的精确度。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210817102419696-1022285028.png) - -同样,`scikit-learn`也提供了对`MSE`相关的描述与实现,详情见: - -[https://scikit-learn.org/stable/modules/model_evaluation.html#mean-squared-error](https://scikit-learn.org/stable/modules/model_evaluation.html#mean-squared-error) - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210817103202555-2000470016.png) - -下面简单看下如何用`scikit-learn`中的`mean_squared_error`来计算模型误差: - -```python -from sklearn.metrics import mean_squared_error - -y_true = [3, -0.5, 2, 7] -y_pred = [2.5, 0.0, 2, 8] - -print(mean_squared_error(y_true, y_pred)) -``` - -打印结果为: - -```ba -0.375 -``` - -与你口算结果是否一致? - -## 梯度下降法的推导 - -上面我们描述了线性回归的数学表示以及计算损失的方法,这样一来,问题就转换成了,求解最佳参数`w`,代入所有已知的训练数据对`(x, y)`到上面的均方差公式中,使得计算出来的损失值`L(w)`尽可能小。其实,本质上是一个求解最优化问题。 - -`值得注意的是,在房价预测中,房价影响因素x是自变量,房价y是因变量,但在这里的损失函数中,训练数据对(x, y)是已知的(不再是变量了),而w可以看成是自变量,损失值L(w)是因变量,损失值L(w)随着w取值的变化而变化。我们关心L(w)最小时,w的取值,这样w就变成已知的了,此时认为模型可以拟合这些训练数据,进而用来预测其他房屋的价格。` - -讲到求损失函数的最小值,高中数学我们知道,一种办法可以直接让其导数为零,直接寻找损失函数的极小值点。这种方法求最优解,其实是在解这个矩阵方程,英文中称这种方法为正规方程(`Normal Equation`)。感兴趣的朋友可以自行网上查询一些资料学习一下。 - -由于正规方程法可能不存在唯一解,且当特征维度较大时,计算量比较大,非常耗时。下面重点介绍另外一种求解方法————梯度下降法。 - -> 求解损失函数最小问题,或者说求解使损失函数最小的最优化问题时,经常使用搜索的方法。具体而言,选择一个初始点作为起点,然后开始不断搜索,损失函数逐渐变小,当到达搜索迭代的结束条件时,该位置为搜索算法的最终结果。我们先随机猜测一个`w`,然后对`w`值不断进行调整,来让`L(w)`逐渐变小,最好能找到使得`L(w)`最小的`w`。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210817161028621-151607314.png) - -> 一般情况下,0 < α < 1 。α越大,表示我们希望损失函数以更快的速度下降,α越小,表示我们希望损失函数下降的速度变慢。如果α设置得不合适,每次的步长太大,损失函数很可能无法快速收敛到最小值(步长太大可能直接跨过了极小值点);步长太小,计算次数过多,时间过长,效率就很差了。到这里,你能理解上面特征预处理时,对特征值进行归一化、标准化的好处吗?【可以有效提高梯度下降(`Gradient Descent`)的收敛速度】 - -当一个训练集有`m`个训练样本时,求导只需要对多条训练样本的数据做加和。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210817162449483-771338874.png) - -代入公式(15),如下: - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210817162659898-1988297446.png) - -`w`是一个向量,假设它是`n`维的,在更新`w`时,需要同时对`n`维所有`w`值进行更新,其中第`j`维就是使用这里的公式。 - -具体而言,这个算法为: - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210817163222156-757384512.png) - -> 这一方法在每一次迭代时使用整个训练集中的所有样本来更新参数,也叫做批量梯度下降法(`Batch Gradient Descent,BGD`)。线性回归的损失函数`L`是一个凸二次函数(`Convex Quadratic Function`),凸函数的局部极小值就是全局最小值,线性回归的最优化问题只有一个全局解。也就是说,假设不把学习率`α`设置的过大,迭代次数足够多,梯度下降法总是收敛到全局最小值。 - -## 梯度下降法的实现 - -有了上面的层层铺垫,可以用`NumPy`自己实现一下。代码的关键地方大多加了注释,遇到不明白的步骤建议再读读前面的相关内容,遇到不熟悉的numpy函数建议单独看看什么意思,遇到矩阵乘法可以用纸笔简单演算一下。 - -```python -import numpy as np - - -class Transformer: - def fit(self, x): - pass - - def transform(self, x): - return x - - -class StandardTransformer(Transformer): - def __init__(self): - self.means = [] - self.stds = [] - - def fit(self, x): - # 按照每个维度(特征)统计, 即按列统计 - self.means = np.mean(x, axis=0) - self.stds = np.std(x, axis=0) - - def transform(self, x): - x = x.copy() - for i in range(x.shape[1]): - # 对每列执行特征标准化 - x[:, i] = (x[:, i] - self.means[i]) / self.stds[i] - return x - - -class LinearRegression: - - def __init__(self, transformer: Transformer): - self.transformer = transformer - self.w = None - self.losses_history = [] - - def fit(self, x, y, alpha=0.0001, num_iters=1000): - num_of_samples, num_of_features = x.shape - # 计算均值与标准差,用于后面的特征标准化 - self.transformer.fit(x) - # 特征标准化 - x_ = self.transformer.transform(x) - # 在线性回归的数学表示中,有过说明,为了简化公式方便使用矩阵运算,在特征值第一列前面插入全为1的一列 - x_ = np.column_stack((np.ones(num_of_samples), x_)) - - # 把y变成列向量,后面参与矩阵运算 - y_ = y.reshape((num_of_samples, 1)) - - # 使用梯度下降法迭代计算w - self.gradient_descent(x_, y_, alpha, num_iters) - - def gradient_descent(self, x, y, alpha: float, num_iters: int): - num_of_samples, num_of_features = x.shape - # 初始化参数w全为0 - self.w = np.zeros((num_of_features, 1)) - - # 开始迭代 - for i in range(num_iters): - # (num_of_samples, num_of_features) * (num_of_features, 1) - y_ = np.dot(x, self.w) - - diff = y_ - y - - # 这一步建议再回头对照上面推导出来的梯度下降法的迭代公式 - # 如果矩阵乘法有点难理解, 建议先在纸上对照公式看一个特征是怎么计算的(即x是按照一列一列参与运算的) - gradient = np.dot(diff.T, x).T - # 更新w - self.w -= alpha * gradient - - # 计算损失 - loss = np.sum(diff * diff) / num_of_samples / 2 - # 加入到迭代过程中的损失统计列表 - self.losses_history.append(loss) - - def predict(self, x): - num_of_samples, num_of_features = x.shape - # 特征标准化 - x_ = self.transformer.transform(x) - # 在线性回归的数学表示中,有过说明,为了简化公式方便使用矩阵运算,在特征值第一列前面插入全为1的一列 - x_ = np.column_stack((np.ones(num_of_samples), x_)) - # 根据训练数据计算出来的w, 直接代入计算即可(矩阵乘法) - return np.dot(x_, self.w) - - -if __name__ == '__main__': - import pandas as pd - - # 获取数据集 - data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip') - - # 调用上面自己实现的线性回归(梯度下降法) - reg = LinearRegression(StandardTransformer()) - # 训练数据 - reg.fit(data[['HouseSize', 'Bedrooms']].values, data['Price'].values) - - # 待预测数据 - predict_data = np.array([ - [1000, 2], - [3000, 3], - [4000, 4], - [5000, 5] - ]) - # 预测 - result = reg.predict(predict_data) - # 打印预测结果 - print(np.c_[predict_data, result].astype(np.int)) - - # 画图观察一下随着迭代次数的增加, 损失值的变化 - import matplotlib.pyplot as plt - - losses_history = reg.losses_history - - plt.plot(np.arange(0, len(losses_history)), losses_history) - plt.xlabel("num_of_iter") - plt.ylabel("loss") - plt.show() -``` - -此时的预测结果为: - -| 房屋面积(平方英尺) | 卧室数量 | 价格(美元) | -| :------------------: | :------: | :----------: | -| 1000 | 2 | 178440 | -| 3000 | 3 | 426503 | -| 4000 | 4 | 567997 | -| 5000 | 5 | 709491 | - -再看随着迭代次数的增加,损失值的变化图: - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210818163053622-198898415.png) - -可见,在迭代到500次的时候损失值基本不再下降了。当然迭代多少次趋于稳定,与步长(学习率`alpha`)密切相关。 - -## 总结 - -线性回归是机器学习技术的一个很好的起点,对初学者走进机器学习很有帮助。 - - - -参考: - -线性回归的求解:[https://lulaoshi.info/machine-learning/linear-model/minimise-loss-function.html](https://lulaoshi.info/machine-learning/linear-model/minimise-loss-function.html) diff --git a/AI/maximum-likelihood-estimation.md b/AI/maximum-likelihood-estimation.md deleted file mode 100644 index 658a3c5..0000000 --- a/AI/maximum-likelihood-estimation.md +++ /dev/null @@ -1,185 +0,0 @@ -# 最大似然估计 - -原文链接: [https://lulaoshi.info/machine-learning/linear-model/maximum-likelihood-estimation](https://lulaoshi.info/machine-learning/linear-model/maximum-likelihood-estimation) - -前文 [从线性回归走进机器学习](AI/linear-regression.md) 介绍了线性回归以及最小二乘法的数学推导过程。 - -对于一组训练数据,使用线性回归建模,可以有不同的模型参数来描述数据,这时候可以用最小二乘法来选择最优参数来拟合训练数据,即使用误差的平方作为损失函数。 - -机器学习求解参数的过程被称为参数估计,机器学习问题也变成求使损失函数最小的最优化问题。 - -最小二乘法比较直观,很容易解释,但不具有普遍意义,对于更多其他机器学习问题,比如二分类和多分类问题,最小二乘法就难以派上用场了。本文将给大家介绍一个具有普遍意义的参数估计方法:`最大似然估计`。 - -## 概率和似然 - -下面以一个猜硬币的例子来模拟机器学习的概率推理过程。 - -假设你被告知一个硬币抛掷10次的正反情况,接下来由你来猜,而你只有一次机会,猜对硬币下一次正反情况则赢得100元,猜错则损失100元。 - -这时,你会如何决策? - -一般地,硬币有正反两面,如果硬币正反两面是均匀的,即每次抛掷后硬币为正的概率是0.5。使用这个硬币,很可能抛10次,有5次是正面。 - -但是假如有人对硬币做了手脚,比如提前对硬币做了修改,硬币每次都会正面朝上,现在抛10次,10次都是正面,那么下次你绝对不会猜它是反面,因为前面的10次结果摆在那里,直觉上你不会相信这是一个普通的硬币。 - -现在有一人抛了10次硬币,得到6正4反的结果,如何估算下次硬币为正的概率呢? - -因为硬币并不是我们制作的,我们不了解硬币是否是完全均匀的,只能根据现在的观察结果来反推硬币的情况。 - -假设硬币上有个参数`θ`,它决定了硬币的正反均匀程度,`θ=0.5`表示正反均匀,每次抛硬币为正的概率为0.5,`θ=1.0`表示硬币只有正面,每次抛硬币为正的概率为1。 - -那么,从观察到的正反结果,反推硬币的构造参数`θ`的过程,就是一个参数估计的过程。 - -### 概率 - -抛掷10次硬币可能出现不同的情况,可以是“5正5反”、“4正6反”,“10正0反”等。 - -假如我们知道硬币是如何构造的,即已知硬币的参数$\theta$,那么出现“6正4反”的概率为: - -$$ -P(6正4反 \ |\ \theta=0.5)=C_{10}^{6}\times 0.5^6 \times (1-0.5)^4 \approx 0.2051 \\ -P(6正4反 \ |\ \theta=0.6)=C_{10}^{6}\times 0.6^6 \times (1-0.6)^4 \approx 0.2508 \\ -P(6正4反 \ |\ \theta=0.9)=C_{10}^{6}\times 0.9^6 \times (1-0.9)^4 \approx 0.0112 -$$ - -详细解释: - -1. 上面这个公式是概率函数,表示已知参数$\theta$,事实“6正4反”发生的概率。 - -2. 参数$\theta$取不同的值时,事情发生的概率不同。在数学上一般使用$P$或$Pr$表示概率(Probability)函数。 - -3. 上述过程中,抛10次硬币,要选出6次正面,使用了排列组合。因为“6正4反”可能会出现`正正正正正正反反反反`、`正正正正正反正反反反`、`正正正正反正正反反反`等共210种组合,要在10次中选出6次为正面。假如每次正面的概率是0.6,那么反面的概率就是(1-0.6)。每次抛掷硬币的动作是相互独立,互不影响的,“6正4反”发生的概率就是各次抛掷硬币的概率乘积,再乘以210种组合。 - -概率反映的是:**已知背后原因,推测某个结果发生的概率**。 - -### 似然 - -与概率不同,似然反映的是:**已知结果,反推原因**。 - -具体而言,似然(`Likelihood`)函数表示的是基于观察的数据,取不同的参数$\theta$时,统计模型以多大的可能性接近真实观察数据。 - -这就很像前面举的例子,已经给你了一系列硬币正反情况,但你并不知道硬币的构造,下次下注时你要根据已有事实,反推硬币的构造。 - -例如,当观察到硬币“10正0反”的事实,猜测硬币极有可能每次都是正面;当观察到硬币“6正4反”的事实,猜测硬币有可能不是正反均匀的,每次出现正面的可能性是0.6。 - -似然函数与前面的概率函数的计算方式极其相似,与概率函数不同的是,似然函数是$\theta$的函数,即$\theta$是未知的。 - -似然函数衡量的是在不同参数$\theta$下,真实观察数据发生的可能性。 - -似然函数通常是多个观测数据发生的概率的联合概率,即多个观测数据都发生的概率。 - -在机器学习里可以这样理解,目标$y$和特征$\boldsymbol{x}$同时发生,这些数值被观测到的概率。 - -单个观测数据发生的可能性为$P(\theta)$,如果各个观测之间是相互独立的,那么多个观测数据都发生的概率可表示为**各个样本发生的概率的乘积**。 - -这里稍微解释一下事件独立性与联合概率之间的关系。 - -如果事件A和事件B相互独立,那么事件A和B同时发生的概率是 $ P(A) \times P(B)$ 。 - -例如: - -- 事件“下雨”与事件“地面湿”就不是相互独立的,“下雨”与"地面湿"是同时发生、高度相关的,这两个事件都发生的概率就不能用概率的乘积来表示。 -- 两次抛掷硬币相互之间不影响,因此硬币正面朝上的概率可以用各次概率的乘积来表示。 - -似然函数通常用$L$表示,对应英文`Likelihood`。观察到抛硬币“6正4反”的事实,硬币参数$\theta$取不同值时,似然函数表示为: - -$$ -L(\theta ; 6正4反)=C_{10}^{6}\times \theta^6 \times (1-\theta)^4 \\ -$$ - -这个公式的图形如下图所示。 - -![](http://img2020.cnblogs.com/blog/1546632/202110/1546632-20211026143124935-2014252115.png) - -从图中可以看出:参数$\theta$为0.6时,似然函数最大,参数为其他值时,“6正4反”发生的概率都相对更小。 - -所以猜测下次硬币为正,因为根据已有观察,硬币很可能以0.6的概率为正。 - -推广到更为一般的场景,似然函数的一般形式可以用下面公式来表示,也就是之前提到的,各个样本发生的概率的乘积。 - -$$ -L(\theta ; \mathbf{X}) = P_1(\theta ; X_1) \times P_2(\theta ; X_2) ... \times P_n(\theta ; X_n) = \prod P_i(\theta; X_i) -$$ - -## 最大似然估计 - -理解了似然函数的含义,就很容易理解最大似然估计的机制。 - -似然函数是关于模型参数的函数,是描述观察到的真实数据在不同参数下发生的概率。 - -最大似然估计要寻找最优参数,让似然函数最大化。或者说,使用最优参数时观测数据发生的概率最大。 - -### 线性回归的最大似然估计 - -之前的文章提到,线性回归的误差项$ε$是预测值与真实值之间的差异,如下面公式所示。它可能是一些随机噪音,也可能是线性回归模型没考虑到的一些其他影响因素。 -$$ -y^{(i)} = \epsilon^{(i)} + \sum_{j=1}^n w_jx_{j}^{(i)} =\epsilon^{(i)} + \boldsymbol{w}^\top \boldsymbol{x}^{(i)} -$$ - -线性回归的一大假设是:误差服从均值为0的正态分布,且多个观测数据之间互不影响,相互独立。正态分布(高斯分布)的概率密度公式如下面公式,根据正态分布的公式,可以得到$\epsilon$的概率密度。 - -假设$x$服从正态分布,它的均值为$\mu$,方差为$\sigma$,它的概率密度公式如下。公式左侧的$P(x ; \mu, \sigma)$表示$x$是随机变量,$;$分号强调$\mu$和$\sigma$不是随机变量,而是这个概率密度函数的参数。条件概率函数中使用的$|$竖线有明确的意义,$P(y|x)$表示给定$x$(Given $x$),$y$发生的概率(Probability of $y$)。 -$$ -P(x ; \mu, \sigma) = \frac{1}{\sqrt{2 \pi \sigma^2}}\exp{({-\frac{(x - \mu)^2}{2 \sigma^2}})} -$$ - -既然误差项服从正态分布,那么: - -$$ -P(\epsilon^{(i)}) = \frac{1}{\sqrt{2 \pi \sigma^2}}\exp{({-\frac{(\epsilon^{(i)})^2}{2 \sigma^2}})} -$$ - -由于$\epsilon^{(i)} = y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)}$,并取均值$\mu$为0,可得到: -$$ -P(y^{(i)}|\boldsymbol{x}^{(i)}; \boldsymbol{w}) = \frac{1}{\sqrt{2 \pi \sigma^2}}\exp{({-\frac{(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2}{2 \sigma^2}})} -$$ -上式表示给定$\boldsymbol{x}^{(i)}$,$y^{(i)}$的概率分布。$\boldsymbol{w}$并不是随机变量,而是一个参数,所以用$;$分号隔开。或者说$\boldsymbol{w}$和$\boldsymbol{x}^{(i)}$不是同一类变量,需要分开单独理解。$P(y^{(i)}|\boldsymbol{x}^{(i)}, \boldsymbol{w})$则有完全不同的意义,表示$\boldsymbol{x}^{(i)}$和$\boldsymbol{w}$同时发生时,$y^{(i)}$的概率分布。 - - -前文提到,似然函数是所观察到的各个样本发生的概率的乘积。一组样本有$m$个观测数据,其中单个观测数据发生的概率为刚刚得到的公式,$m$个观测数据的乘积如下所示。 -$$ -L(\boldsymbol{w}) = L(\boldsymbol{w}; \boldsymbol{X}, \boldsymbol{y}) = \prod_{i=1}^{m}P(y^{(i)}|\boldsymbol{x}^{(i)}; \boldsymbol{w}) -$$ - -最终,似然函数可以表示成: -$$ -L(\boldsymbol{w}) = \prod_{i=1}^{m} \frac{1}{\sqrt{2 \pi \sigma^2}}\exp{({-\frac{(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2}{2 \sigma^2}})} -$$ - - -其中,$\boldsymbol{x}^{(i)}$和$y^{(i)}$都是观测到的真实数据,是已知的,$\boldsymbol{w}$是需要去求解的模型参数。 - -给定一组观测数据$\boldsymbol{X}$和$\boldsymbol{y}$,如何选择参数$\boldsymbol{w}$来使模型达到最优的效果?最大似然估计法告诉我们应该选择一个$\boldsymbol{w}$,使得似然函数$L$最大。$L$中的乘积符号和$\exp$运算看起来就非常复杂,直接用$L$来计算十分不太方便,于是统计学家在原来的似然函数基础上,取了$\log$对数。$\log$的一些性质能大大化简计算复杂程度,且对原来的似然函数增加$\log$对数并不影响参数$w$的最优值。通常使用花体的$\ell$来表示损失函数的对数似然函数。 -$$ -\begin{aligned} -\ell(\boldsymbol{w}) &= \log\ L(\boldsymbol{w}) \\ -& = \log \ \prod_{i=1}^{m} \frac{1}{\sqrt{2 \pi \sigma^2}}\exp{({-\frac{(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2}{2 \sigma^2}})} \\ -&= \sum_{i=1}^{m}\log[\frac{1}{\sqrt{2 \pi \sigma^2}}\cdot\exp{({-\frac{(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2}{2 \sigma^2}})}] \\ -&= \sum_{i=1}^{m}\log[\frac{1}{\sqrt{2 \pi \sigma^2}}] + \sum_{i=1}^{m}\log[\exp{({-\frac{(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2}{2 \sigma^2}})}] \\ -&= m\log{\frac{1}{\sqrt{2 \pi \sigma^2}}} - \frac{1}{2 \sigma^2}\sum_{i=1}^{m}(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x^{(i)}})^2 -\end{aligned} -$$ - -上面的推导过程主要利用了下面两个公式: - -$$ -\log (ab) = \log(a) + \log(b) \\ -\log_e (\exp(a)) = a -$$ - -由于$\log$对数可以把乘法转换为加法,似然函数中的乘积项变成了求和项。又因为$\log$对数可以消去幂,最终可以得到上述结果。 - -由于我们只关心参数$\boldsymbol{w}$取何值时,似然函数最大,标准差$\sigma$并不会影响$\boldsymbol{w}$取何值时似然函数最大,所以可以忽略掉带有标准差$\sigma$的项$m\log{\frac{1}{\sqrt{2 \pi \sigma^2}}}$。再在$-\frac{1}{2 \sigma^2}\sum_{i=1}^{m}(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2$加个负号,负负得正,原来似然函数$\ell$最大化问题就变成了最小化问题,其实最后还是最小化: -$$ -\sum_{i=1}^{m}(y^{(i)} - \boldsymbol{w}^\top \boldsymbol{x}^{(i)})^2 -$$ - -这与最小二乘法所优化的损失函数几乎一样,都是“真实值 - 预测值”的平方和,可以说是殊途同归。 - -### 最小二乘与最大似然 - -前面的推导中发现,最小二乘与最大似然的公式几乎一样。直观上来说,最小二乘法是在寻找观测数据与回归超平面之间的误差距离最小的参数。最大似然估计是最大化观测数据发生的概率。当我们假设误差是正态分布的,所有误差项越接近均值0,概率越大。正态分布是在均值两侧对称的,误差项接近均值的过程等同于距离最小化的过程。 - -## 总结 - -最大似然估计是机器学习中最常用的参数估计方法之一,逻辑回归、深度神经网络等模型都会使用最大似然估计。我们需要一个似然函数来描述真实数据在不同模型参数下发生的概率,似然函数是关于模型参数的函数。最大似然估计就是寻找最优参数,使得观测数据发生的概率最大、统计模型与真实数据最相似。 diff --git a/AI/why-need-gradient-arg-in-pytorch-backward.md b/AI/why-need-gradient-arg-in-pytorch-backward.md deleted file mode 100644 index 89c5110..0000000 --- a/AI/why-need-gradient-arg-in-pytorch-backward.md +++ /dev/null @@ -1,176 +0,0 @@ -# PyTorch中backward()函数的gradient参数作用 - -这篇文章讲得比较清晰,特地备份一下: [pytorch中backward函数的gradient参数作用](https://www.cnblogs.com/zhouyang209117/p/11023160.html) - -## 问题引入 - -在深度学习中,经常需要对函数求梯度(`gradient`)。`PyTorch`提供的`autograd`包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。 - -`PyTorch`中,`torch.Tensor`是存储和变换数据的主要工具。如果你之前用过`NumPy`,你会发现`Tensor`和`NumPy`的多维数组非常类似。 - -然而,`Tensor`提供`GPU`计算和自动求梯度等更多功能,这些使`Tensor`更加适合深度学习。 - -> `PyTorch`中`tensor`这个单词一般可译作`张量`,张量可以看作是一个多维数组。标量可以看作是零维张量,向量可以看作一维张量,矩阵可以看作是二维张量。 - -如果将`PyTorch`中的`tensor`属性`requires_grad`设置为`True`,它将开始追踪(`track`)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。 - -完成计算后,可以调用`backward()`来完成所有梯度计算。此`tensor`的梯度将累积到`grad`属性中。 如下: -```python -import torch - -x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) -y = x ** 2 + 2 -z = torch.sum(y) -z.backward() -print(x.grad) -``` -输出: -```bash -tensor([2., 4., 6.]) -``` -下面先解释下这个`grad`怎么算的。 - -## 导数、偏导数的简单计算 - -从数学定义上讲,求导或者求偏导大多是函数对自变量而言。但是在很多机器学习的资料和开源库都会看到说标量对向量求导。 - -先简单解释下上面的例子: - -设 $x=[x_1,x_2,x_3]$ ,则 - -$$ -z=x_1^2+x_2^2+x_3^2+6 -$$ - -由求偏导的数学知识,可知: -$$ -\frac{\partial z}{\partial x_1}=2x_1 -$$ - -$$ -\frac{\partial z}{\partial x_2}=2x_2 -$$ - -$$ -\frac{\partial z}{\partial x_3}=2x_3 -$$ - -然后把$x_1=1.0$,$x_2=2.0$,$x_3=3.0$代入得到: -$$ -(\frac{\partial z}{\partial x_1},\frac{\partial z}{\partial x_2},\frac{\partial z}{\partial x_3})=(2x_1,2x_2,2x_3)=(2.0,4.0,6.0) -$$ - -可见结果与`PyTorch`的输出一致。这时再反过来想想,其实所谓的`标量对向量求导`,本质上是函数对各个自变量求导,这里只是把各个自变量看成一个向量,和数学上的定义并不冲突。 - -## backward的gradient参数作用 - -在上面调用`z.backward()`时,是可以传入一些参数的,如下所示: - -![](https://img2020.cnblogs.com/blog/1546632/202110/1546632-20211016174351282-1644733166.png) - -不知道你是怎么理解这里的`gradient`参数的,有深入理解这个参数的朋友欢迎评论区分享你的高见! - -同样,先看下面的例子。已知 -$$ -y_1=x_1x_2x_3 -$$ - -$$ -y_2=x_1+x_2+x_3 -$$ - -$$ -y_3=x_1+x_2x_3 -$$ - -$$ -A=f(y_1,y_2,y_3) -$$ -其中函数$f(y_1,y_2,y_3)$的具体定义未知,现在求 -$$ -\frac{\partial A}{\partial x_1}=? -$$ - -$$ -\frac{\partial A}{\partial x_2}=? -$$ - -$$ -\frac{\partial A}{\partial x_3}=? -$$ - -根据多元复合函数的求导法则,有: -$$ -\frac{\partial A}{\partial x_1}=\frac{\partial A}{\partial y_1}\frac{\partial y_1}{\partial x_1}+\frac{\partial A}{\partial y_2}\frac{\partial y_2}{\partial x_1}+\frac{\partial A}{\partial y_3}\frac{\partial y_3}{\partial x_1} -$$ - -$$ -\frac{\partial A}{\partial x_2}=\frac{\partial A}{\partial y_1}\frac{\partial y_1}{\partial x_2}+\frac{\partial A}{\partial y_2}\frac{\partial y_2}{\partial x_2}+\frac{\partial A}{\partial y_3}\frac{\partial y_3}{\partial x_2} -$$ - -$$ -\frac{\partial A}{\partial x_3}=\frac{\partial A}{\partial y_1}\frac{\partial y_1}{\partial x_3}+\frac{\partial A}{\partial y_2}\frac{\partial y_2}{\partial x_3}+\frac{\partial A}{\partial y_3}\frac{\partial y_3}{\partial x_3} -$$ - -上面3个等式可以写成矩阵相乘的形式,如下: -$$ -[\frac{\partial A}{\partial x_1},\frac{\partial A}{\partial x_2},\frac{\partial A}{\partial x_3}]= -[\frac{\partial A}{\partial y_1},\frac{\partial A}{\partial y_2},\frac{\partial A}{\partial y_3}] -\left[ -\begin{matrix} -\frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \frac{\partial y_1}{\partial x_3} \\ -\frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \frac{\partial y_2}{\partial x_3} \\ -\frac{\partial y_3}{\partial x_1} & \frac{\partial y_3}{\partial x_2} & \frac{\partial y_3}{\partial x_3} -\end{matrix} -\right] -$$ - -其中 -$$ -\left[ -\begin{matrix} -\frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \frac{\partial y_1}{\partial x_3} \\ -\frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \frac{\partial y_2}{\partial x_3} \\ -\frac{\partial y_3}{\partial x_1} & \frac{\partial y_3}{\partial x_2} & \frac{\partial y_3}{\partial x_3} -\end{matrix} -\right] -$$ -叫作雅可比(`Jacobian`)式。雅可比式可以根据已知条件求出。 - -现在只要知道$[\frac{\partial A}{\partial y_1},\frac{\partial A}{\partial y_2},\frac{\partial A}{\partial y_3}]$的值,哪怕不知道$f(y_1,y_2,y_3)$的具体形式也能求出来$[\frac{\partial A}{\partial x_1},\frac{\partial A}{\partial x_2},\frac{\partial A}{\partial x_3}]$。 - -那现在的问题是怎么样才能求出 -$$ -[\frac{\partial A}{\partial y_1},\frac{\partial A}{\partial y_2},\frac{\partial A}{\partial y_3}] -$$ - -答案是由`PyTorch`的`backward`函数的`gradient`参数提供。这就是`gradient`参数的作用。 - -比如,我们传入`gradient`参数为`torch.tensor([0.1, 0.2, 0.3], dtype=torch.float)`,并且假定$x_1=1$,$x_2=2$,$x_3=3$,按照上面的推导方法: -![](https://img2020.cnblogs.com/blog/1546632/202110/1546632-20211018142917485-127081114.png) - -紧接着可以用代码验证一下: -```python -import torch - -x1 = torch.tensor(1, requires_grad=True, dtype=torch.float) -x2 = torch.tensor(2, requires_grad=True, dtype=torch.float) -x3 = torch.tensor(3, requires_grad=True, dtype=torch.float) - -x = torch.tensor([x1, x2, x3]) -y = torch.randn(3) - -y[0] = x1 * x2 * x3 -y[1] = x1 + x2 + x3 -y[2] = x1 + x2 * x3 - -y.backward(torch.tensor([0.1, 0.2, 0.3], dtype=torch.float)) - -print(x1.grad, x2.grad, x3.grad) -``` -输出: -```bash -tensor(1.1000) tensor(1.4000) tensor(1.) -``` -由此可见,推导和代码运行结果一致。 - diff --git a/BigData/flink-kafka-sink-multiple-topics.md b/BigData/flink-kafka-sink-multiple-topics.md deleted file mode 100644 index 1845e03..0000000 --- a/BigData/flink-kafka-sink-multiple-topics.md +++ /dev/null @@ -1,156 +0,0 @@ -# Flink把数据sink到kafka多个topic - - -## 需求与场景 - -上游某业务数据量特别大,进入到kafka一个topic中(当然了这个topic的partition数必然多,有人肯定疑问为什么非要把如此庞大的数据写入到1个topic里,历史留下的问题,现状就是如此庞大的数据集中在一个topic里)。这就需要根据一些业务规则把这个大数据量的topic数据分发到多个(成百上千)topic中,以便下游的多个job去消费自己topic的数据,这样上下游之间的耦合性就降低了,也让下游的job轻松了很多,下游的job只处理属于自己的数据,避免成百上千的job都去消费那个大数据量的topic。数据被分发之后再让下游job去处理 对网络带宽、程序性能、算法复杂性都有好处。 - -这样一来就需要 这么一个分发程序,把上下游job连接起来。 - -## 分析与思考 - -1. Flink中有connect算子,可以连接2个流,在这里1个就是上面数据量庞大的业务数据流,另外1个就是规则流(或者叫做配置流,也就是决定根据什么样的规则分发业务数据) - -2. 但是问题来了,根据规则分发好了,如何把这些数据sink到kafka多个(成百上千)topic中呢? - -3. 首先想到的就是添加多个sink,每分发到一个topic,就多添加1个addSink操作,这对于如果只是分发到2、3个topic适用的,我看了一下项目中有时候需要把数据sink到2个topic中,同事中就有人添加了2个sink,完全ok,但是在这里要分发到几十个、成百上千个topic,就肯定不现实了,不需要解释吧。 - -4. sink到kafka中,其实本质上就是用`KafkaProducer`往kafka写数据,那么不知道有没有想起来,用`KafkaProducer`写数据的时候api是怎样的,`public Future send(ProducerRecord record);` 显然这里需要一个ProducerRecord对象,再看如何实例化`ProducerRecord`对象,`public ProducerRecord(String topic, V value)`, 也就是说每一个message都指定topic,标明是写到哪一个topic的,而不必说 我们要写入10个不同的topic中,我们就一定new 10 个 KafkaProducer - -5. 到上面这一步,如果懂的人就会豁然开朗了,我本来想着可能需要稍微改改flink-connector-kafka实现,让我惊喜的是flink-connector-kafka已经留有了接口,只要实现`KeyedSerializationSchema`这个接口的`String getTargetTopic(T element);`就行 - -## 代码实现 - -先看一下`KeyedSerializationSchema`接口的定义,我们知道kafka中存储的都是byte[],所以由我们自定义序列化key、value - -```java -/** - * The serialization schema describes how to turn a data object into a different serialized - * representation. Most data sinks (for example Apache Kafka) require the data to be handed - * to them in a specific format (for example as byte strings). - * - * @param The type to be serialized. - */ -@PublicEvolving -public interface KeyedSerializationSchema extends Serializable { - - /** - * Serializes the key of the incoming element to a byte array - * This method might return null if no key is available. - * - * @param element The incoming element to be serialized - * @return the key of the element as a byte array - */ - byte[] serializeKey(T element); - - /** - * Serializes the value of the incoming element to a byte array. - * - * @param element The incoming element to be serialized - * @return the value of the element as a byte array - */ - byte[] serializeValue(T element); - - /** - * Optional method to determine the target topic for the element. - * - * @param element Incoming element to determine the target topic from - * @return null or the target topic - */ - String getTargetTopic(T element); -} -``` -重点来了,实现这个`String getTargetTopic(T element);`就可以决定这个message写入到哪个topic里。 - -于是 我们可以这么做,拿到业务数据(我们用的是json格式),然后根据规则分发的时候,就在这条json格式的业务数据里添加一个写到哪个topic的字段,比如说叫`topicKey`, -然后我们实现`getTargetTopic()`方法的时候,从业务数据中取出`topicKey`字段就行了。 - -实现如下(这里我是用scala写的,java类似): -```scala -class OverridingTopicSchema extends KeyedSerializationSchema[Map[String, Any]] { - - override def serializeKey(element: Map[String, Any]): Array[Byte] = null - - override def serializeValue(element: Map[String, Any]): Array[Byte] = JsonTool.encode(element) //这里用JsonTool指代json序列化的工具类 - - /** - * kafka message value 根据 topicKey字段 决定 往哪个topic写 - * @param element - * @return - */ - override def getTargetTopic(element: Map[String, Any]): String = { - if (element != null && element.contains(“topicKey”)) { - element(“topicKey”).toString - } else null - } -} -``` - -之后在new `FlinkKafkaProducer`对象的时候 把上面我们实现的这个`OverridingTopicSchema`传进去就行了。 - -```java -public FlinkKafkaProducer( - String defaultTopicId, // 如果message没有指定写往哪个topic,就写入这个默认的topic - KeyedSerializationSchema serializationSchema,//传入我们自定义的OverridingTopicSchema - Properties producerConfig, - Optional> customPartitioner, - FlinkKafkaProducer.Semantic semantic, - int kafkaProducersPoolSize) { - //.... -} -``` - -**至此,我们只需要把上面new 出来的`FlinkKafkaProducer`添加到addSink中就能实现把数据sink到kafka多个(成百上千)topic中。** - - -*下面简单追踪一下`FlinkKafkaProducer`源码,看看flink-connector-kafka是如何将我们自定义的`KeyedSerializationSchema`作用于最终的`ProducerRecord`* - -```java - /** 这个是用户可自定义的序列化实现 - * (Serializable) SerializationSchema for turning objects used with Flink into. - * byte[] for Kafka. - */ - private final KeyedSerializationSchema schema; - - @Override - public void invoke(FlinkKafkaProducer.KafkaTransactionState transaction, IN next, Context context) throws FlinkKafkaException { - checkErroneous(); -// 调用我们自己的实现的schema序列化message中的key - byte[] serializedKey = schema.serializeKey(next); - -// 调用我们自己的实现的schema序列化message中的value - byte[] serializedValue = schema.serializeValue(next); - -// 调用我们自己的实现的schema取出写往哪个topic - String targetTopic = schema.getTargetTopic(next); - - if (targetTopic == null) { -// 如果没有指定写往哪个topic,就写往默认的topic -// 这个默认的topic是我们new FlinkKafkaProducer时候作为第一个构造参数传入(见上面的注释) - targetTopic = defaultTopicId; - } - - Long timestamp = null; - if (this.writeTimestampToKafka) { - timestamp = context.timestamp(); - } - ProducerRecord record; - int[] partitions = topicPartitionsMap.get(targetTopic); - if (null == partitions) { - partitions = getPartitionsByTopic(targetTopic, transaction.producer); - topicPartitionsMap.put(targetTopic, partitions); - } - if (flinkKafkaPartitioner != null) { - record = new ProducerRecord<>( - targetTopic, // 这里看到了我们上面一开始分析的ProducerRecord - flinkKafkaPartitioner.partition(next, serializedKey, serializedValue, targetTopic, partitions), - timestamp, - serializedKey, - serializedValue); - } else { - record = new ProducerRecord<>(targetTopic, null, timestamp, serializedKey, serializedValue); - } - pendingRecords.incrementAndGet(); - transaction.producer.send(record, callback); - } -``` diff --git a/BigData/flink-stream-processing-theory.md b/BigData/flink-stream-processing-theory.md deleted file mode 100644 index 57dd389..0000000 --- a/BigData/flink-stream-processing-theory.md +++ /dev/null @@ -1,198 +0,0 @@ -# Flink流处理基础 - -> Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams. - -Apache Flink是一个分布式、有状态的流计算引擎。 - -![](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201101110625248-1426818584.png) - -下面将正式开启Flink系列的学习笔记与总结。(`https://flink.apache.org/`)。此篇是准备篇,主要介绍流处理相关的基础概念。别小看这些理论,对后续的学习与理解很有帮助哦。 - -下面很多词汇来自flink官方:`https://ci.apache.org/projects/flink/flink-docs-release-1.11/concepts/glossary.html` - -## 什么是数据流 - -> An event is a statement about a change of the state of the domain modelled by the application. Events can be input and/or output of a stream or batch processing application. - -> Any kind of data is produced as a stream of events. Credit card transactions, sensor measurements, machine logs, or user interactions on a website or mobile application, all of these data are generated as a stream. - -`数据流`:是一个可能无限的事件序列。这里的事件可以是实时监控数据、传感器测量值、转账交易、物流信息、电商网购下单、用户在界面上的操作等等。 - -流又可分为`无界流`和`有界流`。如下图所示: - -![无界流和有界流的区分](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201107142150285-833461000.png) - -1. `Unbounded streams` have a start but no defined end. They do not terminate and provide data as it is generated. - -2. `Bounded streams` have a defined start and end. Bounded streams can be processed by ingesting all data before performing any computations - -## 数据流上的操作 - -流处理引擎通常会提供一系列操作来实现数据流的获取(`Source`)、转换(`Transformation`)、输出(`Sink`)。 - -借用flink官方1.10版本的图`https://ci.apache.org/projects/flink/flink-docs-release-1.10/concepts/programming-model.html`,如下: - -![Dataflows](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201107195641816-242156563.png) - -> A `logical graph` is a directed graph where the nodes are Operators and the edges define input/output-relationships of the operators and correspond to data streams or data sets. A logical graph is created by submitting jobs from a Flink Application.`Logical graphs` are also often referred to as `dataflow graphs`. - -> `Operator`: Node of a Logical Graph. An Operator performs a certain operation, which is usually executed by a Function. Sources and Sinks are special Operators for data ingestion and data egress. - - -`dataflow`图:描述了数据如何在不同的操作之间流动。`dataflow`图通常为有向图。图中节点称为`算子`(也常称为`操作`),表示计算;边表示数据依赖关系,`算子`从输入获取数据,对其进行计算,然后产生数据并发往输出以供后续处理。没有输入端的算子称为`source`,没有输出端的算子称为`sink`。一个`dataflow`图至少要有一个`source`和一个`sink`。 - -`dataflow`图被称作逻辑图(`logical graph`),因为仅仅表达的是计算逻辑,实际分布式处理时,每个算子可能会在不同机器上运行多个并行计算。 - -![Parallel Dataflows](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201107201220685-1561125626.png) - -### Source、Sink、Transformation - -> Sources are where your program reads its input from. - -> Data sinks consume DataStreams and forward them to files, sockets, external systems, or print them. - -数据接入(`Source`)和数据输出(`Sink`)操作允许流处理引擎和外部系统进行通信。数据接入操作是从外部数据源获取原始数据并将其转换成适合流处理引擎后续处理的格式。实现数据接入操作逻辑的算子称为`Source`,可以来自socket、文件、kafka等。数据输出操作是将流处理引擎中的数据以适合外部存储格式输出,负责数据输出的算子称为`Sink`,可以写入文件、数据库、kafka等。 - -> A Transformation is applied on one or more data streams or data sets and results in one or more output data streams or data sets. A transformation might change a data stream or data set on a per-record basis, but might also only change its partitioning or perform an aggregation. - -在数据接入与数据输出中间,往往有大量的转换操作(`Transformation`)。转换操作会分别处理每个事件。这些操作逐个读取事件,对其应用某些转换并产生一条新的输出流。算子既可以同时接收多个输入流或产出多条输出流,也可以进行单流分割、多流合并等。 - -## 有状态怎么理解 - -在数据流上的操作可以是无状态的(`stateless`),也可以是有状态的(`stateful`),无状态的操作不会维持内部状态,即处理事件时无需依赖已处理过的事件,也不保存历史事件。由于事件处理互不影响而且与事件到来的时间无关,无状态的操作很容易并行化。此外,如果发生故障,无状态的算子可以很容易地重启并恢复工作。相反,有状态的算子可能需要维护之前接收到的事件信息,它们的状态会根据新来的事件更新,并用于未来事件的处理逻辑中。有状态的流处理应用在并行化和容错方面会更具挑战性。 - -有状态算子同时使用传入的事件和内部状态来计算输出。由于流式算子处理的都是潜在无穷无尽的数据,所以必须小心避免内部状态无限增长。为了限制状态大小,算子通常都会只保留到目前为止所见事件的摘要或概览。这种摘要可能是一个数量值,一个累加值,一个对至今为止全部事件的抽样,一个窗口缓冲或是一个保留了应用运行过程中某些有价值信息的自定义数据结构。 - -不难想象,支持有状态算子会面临很多实现上的挑战。有状态算子需要保证状态可以恢复,并且即使出现故障也要确保结果正确。 - - -### 至多一次(`At Most Once`) - -它表示每个事件至多被处理一次。 - -任务发生故障时最简单的措施就是既不恢复丢失的状态,也不重放丢失的事件。换句话说,事件可以随意丢弃,不保证结果的正确性。 - -### 至少一次(`At Least Once`) - -它表示所有事件最终都会被处理,虽然有些可能会处理多次。 - -对大多数应用而言,用户期望是不丢事件。如果最终结果的正确性仅依赖信息的完整度,那重复处理或者可以接受。例如,确认某个事件是否出现过,就可以用至少一次保证正确的结果。它最坏的结果也无非就是重复判断了几次。但如果要计算某个事件出现的次数,至少一次可能就会返回错误的结果。 - -为了确保至少一次语义的正确性,需要想办法从源头或者缓冲区中重放事件。持久化事件日志会将所有事件写入永久存储,这样在任务故障时就可以重放它们。实现该功能的另一个方法是采用记录确认(`ack`)。将所有事件存在缓冲区中,直到处理管道中所有任务都确认某个事件已经处理完毕才会将事件丢弃。 - -### 精确一次(`Exactly Once`) - -它表示不但没有事件丢失,而且每个事件对于内部状态的更新都只有一次。 - -精确一次是最严格,也是最难实现的一类保障。本质上,精确一次语义意味着应用总会提供正确的结果,就如同故障从未发生过一样。 - -精确一次同样需要事件重放机制。此外,流处理引擎需要确保内部状态的一致性,即在故障恢复后,计算引擎需要知道某个事件对应的更新是否已经反映到状态上。事务性更新是实现该目标的一个方法,但它可能会带来极大的性能开销。Flink采用了轻量级检查点机制(`Checkpoint`)来实现精确一次的结果保障。(flink系列后续会有文章重点分析,这里先知道有这么一回事即可。) - -上面提到的“结果保障”,指的都是流处理引擎内部状态的一致性。也就是说,我们关注故障恢复后应用代码能够看到的状态值。请注意,保证应用状态的一致性和保证输出的一致性并不是一回事。一旦数据从`Sink`中写出,除非目标系统支持事务,否则最终结果的正确性难以保证。 - -## 窗口操作 - -官方有篇关于窗口介绍的博客:`https://flink.apache.org/news/2015/12/04/Introducing-windows.html` - -`windows`相关的文档:`https://ci.apache.org/projects/flink/flink-docs-release-1.11/dev/stream/operators/windows.html#windows` - -> `Windows` are at the heart of processing infinite streams. Windows split the stream into “buckets” of finite size, over which we can apply computations. - -转换操作(`Transformation`)可能每处理一个事件就产生结果并(可能)更新状态。然而,有些操作必须收集并缓冲记录才能计算结果,例如流式`Join`或者求中位数的聚合(`Aggregation`)。为了在无界数据流上高效地执行这些操作,必须对操作所维持的数据量加以限制。下面将讨论支持该项功能的窗口操作。 - -此外,有了窗口操作,就能在数据流上完成一些具有实际语义价值的计算。比如说,`最近几分钟某路口的车流量`、`某路口每10分钟的车流量`等。 - -窗口操作会持续创建一些称为`桶`的有限事件集合,并允许我们基于这些有限集进行计算。事件通常会根据其时间或其他数据属性分配到不同桶中。为了准确定义窗口算子语义,我们需要决定事件如何分配到桶中以及窗口用怎样的频率产生结果。窗口的行为是由一系列策略定义的,这些窗口策略决定了什么时间创建桶,事件如何分配到桶中以及桶内数据什么时间参与计算。其中参与计算的决策会根据触发条件判定,当触发条件满足时,桶内数据会发送给一个计算函数,由它来对桶内的元素应用计算逻辑。这些计算函数可以是某些聚合(例如计数(`count`)、求和(`sum`)、最大值(`max`)、最小值(`min`)、平均值(`avg`)等),也可以是自定义操作。策略的指定可以基于`时间`(例如“最近5秒钟接收的事件”)、`数量`(例如“最新100个事件”)或其他数据属性。 - -接下来就重点介绍几种常见的窗口语义。 - -### 滚动窗口 - -![基于时间的滚动窗口](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201108110525183-1785476154.png) - -> A `tumbling windows` assigner assigns each element to a window of a specified window size. Tumbling windows have a fixed size and do not overlap. For example, if you specify a tumbling window with a size of 5 minutes, the current window will be evaluated and a new window will be started every five minutes as illustrated by the following figure. - -滚动窗口将事件分配到长度固定且互不重叠的桶中。 - -- 基于数量的(`count-based`)滚动窗口定义了在触发计算前需要集齐多少条事件; -- 基于时间的(`time-based`)滚动窗口定义了在桶中缓冲事件的时间间隔。如上图所示,基于时间间隔的滚动窗口将事件汇集到桶中,每隔一段时间(`window size`)触发一次计算。 - -### 滑动窗口 - -![基于时间的滑动窗口](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201108113402930-1045627239.png) - -> The `sliding windows` assigner assigns elements to windows of fixed length. Similar to a tumbling windows assigner, the size of the windows is configured by the window size parameter. An additional window slide parameter controls how frequently a sliding window is started. Hence, sliding windows can be overlapping if the slide is smaller than the window size. In this case elements are assigned to multiple windows. - -滑动窗口将事件分配到大小固定且允许相互重叠的桶中,这意味着每个事件可能会同时属于多个桶。 - -> For example, you could have windows of size 10 minutes that slides by 5 minutes. With this you get every 5 minutes a window that contains the events that arrived during the last 10 minutes as depicted by the following figure. - -我们通过指定长度(`size`)和滑动间隔(`slide`)来定义滑动窗口。滑动间隔决定每隔多久生成一个新的桶。举个例子,`SlidingTimeWindows(size = 10min, slide = 5min)`表示的语义是`每隔5分钟统计一次最近10分钟的数据`。 - -### 会话窗口 - -![会话窗口](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201108145816227-655512034.png) - -> The `session windows` assigner groups elements by sessions of activity. Session windows do not overlap and do not have a fixed start and end time, in contrast to tumbling windows and sliding windows. Instead a session window closes when it does not receive elements for a certain period of time, i.e., when a gap of inactivity occurred. A session window assigner can be configured with either a static session gap or with a session gap extractor function which defines how long the period of inactivity is. When this period expires, the current session closes and subsequent elements are assigned to a new session window. - -会话窗口在一些实际场景中非常有用,这些场景既不适合用滚动窗口也不适合用滑动窗口。比如说,有一个应用要在线分析用户行为,在该应用中我们要把事件按照用户的同一活动或者会话来源进行分组。会话由发生在相邻时间内的一系列事件外加一段非活动时间组成。具体来说,用户浏览一连串新闻文章的交互过程可以看做是一个会话。由于会话长度并非预先定义好,而是和实际数据有关,所以无论是滚动还是滑动窗口都无法适用该场景。而我们需要一个窗口操作,能将属于同一会话的事件分配到相同桶中。会话窗口根据会话间隔(`session gap`)将事件分为不同的会话,该间隔值定义了会话在关闭前的非活动时间长度。 - -## 时间语义 - -相关文档:`https://ci.apache.org/projects/flink/flink-docs-release-1.11/concepts/timely-stream-processing.html` - -上面介绍的几种窗口类型都要在生成结果前缓冲数据,不难发现时间成了应用中较为核心的要素。举例来说,算子在计算时是应该依赖事件实际发生的时间还是应用处理事件的时间呢?这需要根据具体的应用场景来分析。 - -流式应用中有两个不同概念的时间,即处理时间(`Processing time`)和事件时间(`Event time`)。如下图所示: - -![Event Time and Processing Time](http://img2020.cnblogs.com/blog/1546632/202011/1546632-20201108152539699-992265938.png) - -### 处理时间 - -> `Processing time`: Processing time refers to the system time of the machine that is executing the respective operation. - -处理时间是当前流处理算子所在机器上的本地时钟时间。 - -基于处理时间的窗口会包含那些恰好在一段时间内到达窗口算子的事件,这里的时间段是按照机器上的本地时钟时间测量的。 - -基于`处理时间`的操作结果是不可预测的,计算结果是不确定的。当数据流的处理速度与预期速度不一致、事件到达算子的顺序混乱、本地时钟不正确,基于`处理时间`的窗口事件可能就会不一样。 - -### 事件时间 - -> `Event time`: Event time is the time that each individual event occurred on its producing device. This time is typically embedded within the records before they enter Flink, and that event timestamp can be extracted from each record. - -事件时间是数据流中事件实际发生的时间。它附加在事件自身,在进入流处理引擎(`Flink`)前就存在。 - -即便事件有延迟,依赖`事件时间`也能反映出真实发生的情况,从而能准确地将事件分配到对应的时间窗口。 - -基于`事件时间`的操作是可预测的,其结果具有确定性。无论数据流的处理速度如何、事件到达算子的顺序怎样,基于`事件时间`的窗口都会生成同样的结果。 - -使用事件时间要克服的挑战之一是如何处理延迟事件。不能因为来了事件时间为一年前的事件,一年前的时间窗口就一直不关闭等待迟到事件的到来。这样就引出了一个重要的问题:`怎么决定事件时间窗口的触发时机?`换言之,需要等待多久才能确定已经收到了所有发生在某个特定时间点之前的事件? - -> The mechanism in Flink to measure progress in event time is `watermarks`. Watermarks flow as part of the data stream and carry a timestamp t. A Watermark(t) declares that event time has reached time t in that stream, meaning that there should be no more elements from the stream with a timestamp t’ <= t (i.e. events with timestamps older or equal to the watermark). - -水位线(`watermark`)是一个全局进度指标,表示确信不会再有延迟事件到来的某个时间点。本质上,水位线提供了一个逻辑时钟,用来通知系统当前的事件时间。当一个算子接收到时间为`t`的水位线,就可以认为不会再收到任何时间戳小于或者等于`t`的事件了。 - -水位线允许我们在结果的准确性和延迟之间做出取舍。激进的水位线策略保证了低延迟,但随之而来的是低可信度。该情况下,延迟事件可能会在水位线之后到来,我们必须额外加一些代码来处理它们。反之,如果水位线过于保守,虽然可信度得以保证,但可能会无谓地增加了处理延迟。 - -实际应用中,系统可能难以获取足够多的信息来完美确定水位线。流处理引擎需提供某些机制来处理那些晚于水位线的迟到事件。根据具体需求的不同,可能直接忽略这些事件,可能将它们写入日志,或者利用它们修正之前的结果。 - -### 小结 - -既然事件时间能解决大多数问题,为何还要去关心处理时间呢?事实上,处理时间也有其适用的场景。处理时间窗口能够将延迟降至最低。由于无需考虑迟到或乱序的事件,窗口只需简单地缓冲事件,然后在达到特定时间后立即触发窗口计算即可。因此,对于那些更重视处理速度而非准确度的应用,处理时间就会派上用场。另一种场景是,你需要周期性地实时报告结果而无论其准确性如何。例如,你想观察数据流的接入情况,通过计算每秒大致的事件数来检测数据中断,就可以使用处理时间来进行窗口计算。 - -总而言之,虽然处理时间提供了较低的延迟,但它的结果依赖处理速度,具有不确定性。事件时间则与之相反,能保证结果的相对准确性,并允许你处理延迟或无序的事件。 - -## 如何评测流处理的性能 - -对于`批处理应用`而言,作业的总执行时间通常会作为性能评测的一个方面。但`流式应用`事件无限输入、程序持续运行,没有总执行时间的概念,所以常用延迟和吞吐来评测流式应用的性能。通常,我们希望系统低延迟、高吞吐。 - -`延迟`:表示处理一个事件所需的时间。即从接收事件至观察到事件处理效果的时间间隔。比如说,平均延迟为10毫秒,表示平均每条数据会在10毫秒内处理;95%延迟为10毫秒则表示95%的事件会在10毫秒内处理。保证低延迟对很多流式应用(告警系统、网络监测、诈骗识别、风险控制等)至关重要,从而滋生出所谓的`实时应用`。 - -`吞吐`:表示单位时间可以处理多少事件。可用来衡量系统的处理速度。但要注意,处理速度还取决于数据到来速度,因此,吞吐低不一定意味着系统性能差。在流处理应用中,通常希望系统有能力应对以最大速度到来的事件,即系统满负载时的性能上限(`峰值吞吐`)。实际生产中,一旦事件到达速度过高导致系统处理不过来,系统就会被迫开始缓冲事件。若此时系统吞吐已到极限,再提高事件到达速度只会让延迟更大。如果系统还继续以力不能及的高速度接收事件,那么缓冲区可能会用尽。这种情况通常被称为背压(`backpressure`),我们有多种可选策略来处理它。flink系列后续会有文章重点介绍这块。 - - -## 参考资料 - -- [1] [Fabian Hueske & Vasiliki Kalavri 著,崔星灿 译;基于Apache Flink的流处理(Stream Processing with Apache Flink);中国电力出版社,2020] -- [2] [Apache Flink Documentation v1.11](https://ci.apache.org/projects/flink/flink-docs-release-1.11/) diff --git a/BigData/how-to-install-opentsdb.md b/BigData/how-to-install-opentsdb.md deleted file mode 100644 index f11157e..0000000 --- a/BigData/how-to-install-opentsdb.md +++ /dev/null @@ -1,169 +0,0 @@ -# centos7安装部署opentsdb2.4.0 - - -## 写在前面 - -最近因为项目需要在读opentsdb的一部分源码,后面会做个小结分享出来。本人是不大喜欢写这种安装部署的文章,考虑到opentsdb安装部署对于初次接触者来说不太友好,另外对公司做测试的同事可能有些帮助作用,方便他们快速安装部署,就把OpenTSDB 2.4.0安装部署文档写在这里。 - -对于opentsdb是什么,应用领域这里就不说了,不了解的请看官网`http://opentsdb.net/`。 - -这里只提一点,`opentsdb`的后端数据存储依赖于`HBase`。 - -所以安装步骤就分为了三步 (我们也可以类比安装传统的依赖关系型数据库比如mysql作为后端数据存储的软件) - -1. 安装HBase (类比传统软件我们要安装mysql) - -2. 创建表结构 (类比我们在mysql中创建database以及table) - -3. 安装配置并启动opentsdb (类比一些springboot的应用) - -## 安装HBase - -如果已经有HBase环境了,那么请跳过这一步(大多数使用`HBase`集群环境应该都是用`CDH`管理) - -官网地址: `http://hbase.apache.org/` - -其实`HBase`的存储又是依赖`HDFS`,当然了如果只是本地测试用,可以直接用`本地文件系统`代替`HDFS`,这样就不需要部署一套`HDFS`集群了 - -搭建`Standalone HBase`,官方文档`http://hbase.apache.org/book.html#quickstart`,一步一步都有,请仔细读 - -这里简单梳理一下关键的地方: - -- `conf/hbase-env.sh`文件 - -```bash -#The java implementation to use. -export JAVA_HOME=/usr/jdk64/jdk1.8.0_112 -``` -- `conf/hbase-site.xml`文件 -```xml - - - - hbase.rootdir - file:///home/itwild/hbase - - - - hbase.zookeeper.property.dataDir - /home/itwild/zookeeper - - -``` -然后执行`bin/start-hbase.sh`, 启动成功了`jps`命令可以看到`HMaster`进程。 - -在standalone模式下,虽然看到的是一个`JVM`实例,实际上启了`HMaster`、`HRegionServer`、`ZooKeeper` - -启动成功,可打开HBase Web UI, `http://localhost:16010` - -## 在HBase中创建表结构 - -1. 执行`bin/hbase shell`进入一个交互的界面 (这里我们也可以类比执行`mysql -uXXX -pXXX`后进入) - -2. 在交互界面里我们依次执行下面4条建表(table)语句 -```bash -#opentsdb中那些metric数据就存在这张表中 -#这张表数据会很大,考虑到读写效率,我们注意到这张表就一个列族 -create 'tsdb',{NAME => 't', VERSIONS => 1, BLOOMFILTER => 'ROW'} -#opentsdb中建立metric name、tagK、tagV字面量与uid一一对应的表 -#opentsdb不会存储实际的字符串字面值 -#比如system.cpu.util的metric,会将system.cpu.util转化为id(默认自增,后面介绍部分源码的时候会有讲到)后,存入HBase -#这张表有id、name两个列族,可通过id找到name,也可以通过name找到id -create 'tsdb-uid',{NAME => 'id', BLOOMFILTER => 'ROW'},{NAME => 'name', BLOOMFILTER => 'ROW'} -#下面两张表暂时可不必太关心,先创建出来就好 -create 'tsdb-tree',{NAME => 't', VERSIONS => 1, BLOOMFILTER => 'ROW'} -create 'tsdb-meta',{NAME => 'name', BLOOMFILTER => 'ROW'} -``` - -注意一下,这里的建表语句我有意把压缩(`COMPRESSION`)选项去掉了,因为存储用的是本地文件系统,有些压缩可能是不支持的,生产环境使用`HDFS`的建表语句可能是这样的 -```bash -create 'tsdb',{NAME => 't', VERSIONS => 1, BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY'} -create 'tsdb-uid',{NAME => 'id', BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY'},{NAME => 'name', BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY'} -create 'tsdb-tree',{NAME => 't', VERSIONS => 1, BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY'} -create 'tsdb-meta',{NAME => 'name', BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY'} -``` - -- 在hbase shell的交互界面中执行`list`,即可看到上面创建的4张表 -```bash -hbase(main):004:0> list -TABLE -tsdb -tsdb-meta -tsdb-tree -tsdb-uid -``` -## 安装配置并启动opentsdb - -下载地址:`https://github.com/OpenTSDB/opentsdb/releases` - -这里`centos7`系统,选择下载`opentsdb-2.4.0.noarch.rpm`包 - -- 执行`yum -y localinstall opentsdb-2.4.0.noarch.rpm` - -如果这里安装报错,可能需要`vi /usr/bin/yum`临时改一下python解析器的版本`#!/usr/bin/python2.7`, -改过之后安装过程中又有报错,可能需要`vi /usr/libexec/urlgrabber-ext-down`同样临时改一下python解析器的版本`#!/usr/bin/python2.7` - -- 把opentsdb的服务注册为系统服务,即可以用`systemctl status/start/stop/restart opentsdb`来查看控制 - -`vi /usr/lib/systemd/system/opentsdb.service`添加以下内容 -```bash -[Unit] - -Description=OpenTSDB Service - -[Service] - -Type=forking - -PrivateTmp=yes - -ExecStart=/usr/share/opentsdb/etc/init.d/opentsdb start - -ExecStop=/usr/share/opentsdb/etc/init.d/opentsdb stop - -Restart=on-abort -``` -然后你就发现可以用`systemctl status opentsdb`了,不过现在服务还是`dead`状态 - -> 注意一下,默认opentsdb配置文件目录:/etc/opentsdb/opentsdb.conf,默认opentsdb日志目录:/var/log/opentsdb - -- 修改`/etc/opentsdb/opentsdb.conf`配置文件 -```bash -tsd.network.port = 4242 -tsd.http.staticroot = /usr/share/opentsdb/static/ -tsd.http.cachedir = /tmp/opentsdb -tsd.core.auto_create_metrics = true -tsd.core.plugin_path = /usr/share/opentsdb/plugins -#zookeeper的地址,即hbase依赖的zookeeper的地址,localhost:2181,localhost:2182,localhost:2183 -tsd.storage.hbase.zk_quorum = localhost:2181 -tsd.storage.fix_duplicates = true -tsd.http.request.enable_chunked = true -tsd.http.request.max_chunk = 4096000 -tsd.storage.max_tags = 16 -#这里看到了我们上面在hbase中创建的4张表 -tsd.storage.hbase.data_table = tsdb -tsd.storage.hbase.uid_table = tsdb-uid -tsd.storage.hbase.tree_table = tsdb-tree -tsd.storage.hbase.meta_table = tsdb-meta -#下面几个配置项到部分源码解析的时候会有介绍,暂时可以先忽略 -#tsd.query.skip_unresolved_tagvs = true -#hbase.rpc.timeout = 120000 -``` -- 启动opentsdb,`systemctl start opentsdb`,成功的话,就可以打开opentsdb的界面了`http://localhost:4242/` - -至此,有关opentsdb的安装部署就完成了。后面我会结合opentsdb部分源码分享一些探究性问题。祝君好运! - -## 更新 - -- 2021.11.03 更新 - -好久没接触opentsdb了,也忘得差不多了,有网友问我为什么启动opentsdb2.4.1失败,我看了下 [https://github.com/OpenTSDB/opentsdb/](https://github.com/OpenTSDB/opentsdb/) ,2021-09-02发布的`OpenTSDB 2.4.1`更新了一些东西: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211103122409515-1957344301.png) - -其中官方增加了把opentsdb的服务注册为系统服务的方式,具体见: [https://github.com/OpenTSDB/opentsdb/blob/master/build-aux/rpm/systemd/opentsdb.service](https://github.com/OpenTSDB/opentsdb/blob/master/build-aux/rpm/systemd/opentsdb.service) ,建议按照官方相应的版本部署安装,以免出现问题! diff --git a/BigData/the-road-of-exploring-opentsdb.md b/BigData/the-road-of-exploring-opentsdb.md deleted file mode 100644 index a2b5a3a..0000000 --- a/BigData/the-road-of-exploring-opentsdb.md +++ /dev/null @@ -1,676 +0,0 @@ -# opentsdb探索之路——部分设计与实现 - - -基于opentsdb-2.4.0版本,本篇开启opentsdb探索之路(主要涉及`读写特性`以及一些`其他细节`),下一篇将开启opentsdb优化之路——`性能优化思路与建议`(总结当前痛点问题、优化思路和解决方案,同时也欢迎朋友提出更好的思路与方案)。 -***注意:阅读本篇文章应该要对`HBase`有最基本的认识,比如`rowkey`、`region`、`store`、` ColumnFamily`、`ColumnQualifier`等概念以及`HBase`逻辑结构、物理存储结构有大致的认知。*** -## opentsdb 概览(overview) -![opentsdb总体架构图](https://img2020.cnblogs.com/blog/1546632/202004/1546632-20200405151735981-1960286423.png) - -上图取自官方`http://opentsdb.net/overview.html`。其中的`TSD`(对应实际进程名是`TSDMain`)就是`opentsdb`组件。每个实例`TSD`都是独立的。没有`master`,没有共享状态(`shared state`),因此实际生产部署可能会通过`nginx`+`Consul`运行多个`TSD`实例以实现`负载均衡`。 -> Each TSD uses the open source database HBase or hosted Google Bigtable service to store and retrieve time-series data - -我们大多应该还是用`HBase`作为数据存储。 -安装部署一文中提到过在[HBase中创建表结构](https://www.cnblogs.com/itwild/p/12528757.html#%E5%9C%A8hbase%E4%B8%AD%E5%88%9B%E5%BB%BA%E8%A1%A8%E7%BB%93%E6%9E%84),这里先简单介绍一下这4张表(`table`),随着探究的深入会对`tsdb`和`tsdb-uid`这两张表有更深刻的认识,至于`tsdb-meta`、`tsdb-tree`两张表不是这里讨论的重点,简单了解一下即可。相关文档:`http://opentsdb.net/docs/build/html/user_guide/backends/index.html` -- tsdb: `opentsdb`全部的时序数据都存在这张表中,该表只有一个名为"t"的列族(`ColumnFamily`)。所以这张表的数据非常大,大多情况下读写性能瓶颈也就与这张表密切相关,进而优化也可能与它相关。 - **rowkey的设计为`an optional salt, the metric UID, a base timestamp and the UID for tagk/v pairs`,即[可选的salt位+metric的UID+小时级别的时间戳+依次有序的tagk、tagv组成的UID键值对],如下:** -```sh -[salt][...] -``` -暂不考虑`salt`位,关于加`salt`下面有章节单独拿出来看它的设计与实现。来看一个不加`salt`且含有两个`tag`的时序数据的`rowkey`组成: -```sh -00000150E22700000001000001000002000004 -'----''------''----''----''----''----' -metric time tagk tagv tagk tagv -``` -至于`rowkey`为什么要这样设计以及具体实现,后面详细介绍,这里先有个基本认知。 -- tsdb-uid: 为了减少`rowkey`的长度,`opentsdb`会将`metric`、`tagk`、`tagv`都映射成`UID`,映射是双向的,比如说既可以根据`tagk`找到对应的`UID`,也可以根据`UID`直接找到相应的`tagk`。而这些`映射关系`就记录在`tsdb-uid`表中。该表有两个`ColumnFamily`,分别是`name`和`id`,另外这两个`ColumnFamily`下都有三列,分别是`metric`、`tagk`、`tagv`。如下图所示: - -|RowKey|id:metric|id:tagk|id:tagv|name:metric|name:tagk|name:tagv| -|:--:|:--:|:--:|:--:|:--:|:--:|:--:| -|metric01|0x01|||||| -|metric02|0x02|||||| -|tagk01||0x01||||| -|tagv01|||0x01|||| -|tagv02|||0x02|||| -|0x01||||metric01||| -|0x01|||||tagk01|| -|0x01||||||tagv01| -|0x02||||metric02||| -|0x02||||||tagv02| -从上面可以看出,`metric`、`tagk`、`tagv`三种类型的`UID`映射互不干扰,这也就使得`0x01`这个`UID`在不同类型中有着不同的含义。后面会从源码角度讲一下uid大致的分配。 -- tsdb-meta: 在完成时序数据的写入之后,会根据当前`opentsdb`实例的配置决定是否为相关时序记录元数据信息。看一下`opentsdb.conf`配置文件中`tsd.core.meta.enable_tsuid_tracking`配置项即可。 - `tsd.core.meta.enable_tsuid_tracking`(默认`false`): 如果开启该选项,每次写入一个`DataPoint`(时序数据)的同时还会向`tsdb-meta`表中写入`rowkey`为该时序数据的`tsuid`(下面会讲到它,即完整的`rowkey`除去`salt`和`timestamp`后的数据), `value`为1的记录。这样,每个点就对应两次`HBase`的写入,一定程度上加大了HBase集群的压力。相关代码见`TSDB#storeIntoDB()#WriteCB#call()` -```java -// if the meta cache plugin is instantiated then tracking goes through it -if (meta_cache != null) { - meta_cache.increment(tsuid); -} else { -// tsd.core.meta.enable_tsuid_tracking - if (config.enable_tsuid_tracking()) { - // tsd.core.meta.enable_realtime_ts - if (config.enable_realtime_ts()) { - // tsd.core.meta.enable_tsuid_incrementing - if (config.enable_tsuid_incrementing()) { - TSMeta.incrementAndGetCounter(TSDB.this, tsuid); - } else { - TSMeta.storeIfNecessary(TSDB.this, tsuid); - } - } else { - // 写入rowkey为tsuid,value为1的记录 - final PutRequest tracking = new PutRequest(meta_table, tsuid, - TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); - client.put(tracking); - } - } -} -``` -- tsdb-tree: 作用,可按照树形层次结构组织时序,就像浏览文件系统一样浏览时序。相关介绍`http://opentsdb.net/docs/build/html/user_guide/trees.html`。这里就不细说了,有兴趣的话看下上面链接中官方介绍的`Examples`,就能秒懂是干嘛的。 -## opentsdb 存储细节(Writing) -相关文档: -`http://opentsdb.net/docs/build/html/user_guide/writing/index.html` -### rowkey的设计 -只有一个名为"t"的列族 -- 时序数据的`metric`、`tagk`、`tagv`三部分字符串都会被转成`UID`,这样再长的字符串在`rowkey`中也会由`UID`代替,大大缩短了`rowkey`的长度 -- `rowkey`中的时序数据的`timestamp`并非实际的时序数据时间,是格式化成以`小时`为单位的时间戳(所谓的`base_time`),也就是说该`rowkey`中的`base_time`表示的是该时序数据发生在哪个整点(小时)。每个数据写入的时候,会用该时序数据实际时间戳相对`base_time`的偏移量(`offset`)作为`ColumnQualifier`写入。 - 结合下面的图以及之后的代码,就一目了然。 - -|rowkey|t: +1|t: +2|t: +3|t: ...|t: +3600| -|--|--|--|--|--|--| -|salt+metric_uid+base_time+tagk1+tagv1+...+tagkN+tagvN|10|9|12|...|8| -### rowkey的具体实现 -在没有启用`salt`的情况下,我整理出来生成`rowkey`的代码如下(注意一下:源码中并没有这段代码哦): -```java -public byte[] generateRowKey(String metricName, long timestamp, Map tags) { - // 获取metricUid - byte[] metricUid = tsdb.getUID(UniqueId.UniqueIdType.METRIC, metricName); - - // 将时间戳转为秒 - if ((timestamp & Const.SECOND_MASK) != 0L) { - timestamp /= 1000L; - } - - final long timestamp_offset = timestamp % Const.MAX_TIMESPAN;//3600 - // 提取出时间戳所在的整点(小时)时间 - final long basetime = timestamp - timestamp_offset; - - // 用TreeMap存储, 排序用的是memcmp()方法,下面会有介绍 - Map tagsUidMap = new org.hbase.async.Bytes.ByteMap<>(); - - tags.forEach((k, v) -> tagsUidMap.put( - tsdb.getUID(UniqueId.UniqueIdType.TAGK, k), - tsdb.getUID(UniqueId.UniqueIdType.TAGV, v))); - - // 不加salt的rowkey,metricUid+整点时间戳+所有的tagK、tagV - byte[] rowkey = new byte[metricUid.length + Const.TIMESTAMP_BYTES + - tags.size() * (TSDB.tagk_width() + TSDB.tagv_width())]; - - // 下面拷贝相应的数据到rowkey字节数组中的相应位置 - System.arraycopy(metricUid, 0, rowkey, 0, metricUid.length); - Bytes.setInt(rowkey, (int) basetime, metricUid.length); - - int startOffset = metricUid.length + Const.TIMESTAMP_BYTES; - for (Map.Entry entry : tagsUidMap.entrySet()) { - System.arraycopy(entry.getKey(), 0, rowkey, startOffset, TSDB.tagk_width()); - startOffset += TSDB.tagk_width(); - - System.arraycopy(entry.getValue(), 0, rowkey, startOffset, TSDB.tagv_width()); - startOffset += TSDB.tagv_width(); - } - - return rowkey; - } -``` -其中的`ByteMap`就是`TreeMap`,见`org.hbase.async.Bytes.ByteMap` -```java -/** A convenient map keyed with a byte array. */ -public static final class ByteMap extends TreeMap - implements Iterable> { - - public ByteMap() { - super(MEMCMP); - } -} -``` -多个`tag`的排序规则是对`tag_id`的`bytes`进行排序,调用的是`org.hbase.async.Bytes#memcmp(final byte[] a, final byte[] b)`方法,如下 -```java -/** - * {@code memcmp} in Java, hooray. - * @param a First non-{@code null} byte array to compare. - * @param b Second non-{@code null} byte array to compare. - * @return 0 if the two arrays are identical, otherwise the difference - * between the first two different bytes, otherwise the different between - * their lengths. - */ -public static int memcmp(final byte[] a, final byte[] b) { - final int length = Math.min(a.length, b.length); - if (a == b) { // Do this after accessing a.length and b.length - return 0; // in order to NPE if either a or b is null. - } - for (int i = 0; i < length; i++) { - if (a[i] != b[i]) { - return (a[i] & 0xFF) - (b[i] & 0xFF); // "promote" to unsigned. - } - } - return a.length - b.length; -} -``` -### 压缩(compaction) -相关文档: -`http://opentsdb.net/docs/build/html/user_guide/definitions.html#compaction` -> An OpenTSDB compaction takes multiple columns in an HBase row and merges them into a single column to reduce disk space. This is not to be confused with HBase compactions where multiple edits to a region are merged into one. OpenTSDB compactions can occur periodically for a TSD after data has been written, or during a query. - -`tsd.storage.enable_compaction`:是否开启压缩(默认为true,开启压缩) - -为了减少存储空间(讲道理对查询也有好处),`opentsdb`在写入时序数据的同时会把`rowkey`放到`ConcurrentSkipListMap`中,一个`daemon`线程不断检查`System.currentTimeMillis()/1000-3600-1`之前的数据能否被压缩,满足压缩条件则会把一小时内的时序数据(它们的`rowkey`是相同的)查出来在内存压缩(`compact`)成一列回写(`write`)到`HBase`中,然后`delete`之前的原始数据。或者是查询(`query`)操作可能也会触发`compaction`操作。代码见`CompactionQueue` -```java -final class CompactionQueue extends ConcurrentSkipListMap { - - public CompactionQueue(final TSDB tsdb) { - super(new Cmp(tsdb)); - // tsd.storage.enable_appends - if (tsdb.config.enable_compactions()) { - // 启用了压缩则会启一个daemon的线程 - startCompactionThread(); - } - } - - /** - * Helper to sort the byte arrays in the compaction queue. - *

- * This comparator sorts things by timestamp first, this way we can find - * all rows of the same age at once. - */ - private static final class Cmp implements Comparator { - - /** The position with which the timestamp of metric starts. */ - private final short timestamp_pos; - - public Cmp(final TSDB tsdb) { - timestamp_pos = (short) (Const.SALT_WIDTH() + tsdb.metrics.width()); - } - - @Override - public int compare(final byte[] a, final byte[] b) { - // 取rowkey中的base_time进行排序 - final int c = Bytes.memcmp(a, b, timestamp_pos, Const.TIMESTAMP_BYTES); - // If the timestamps are equal, sort according to the entire row key. - return c != 0 ? c : Bytes.memcmp(a, b); - } - } -} -``` -看看上面启动的`daemon`线程在做啥`CompactionQueue#Thrd` -```java -/** - * Background thread to trigger periodic compactions. - */ - final class Thrd extends Thread { - public Thrd() { - super("CompactionThread"); - } - - @Override - public void run() { - while (true) { - final int size = size(); - // 达到最小压缩阈值则触发flush() - if (size > min_flush_threshold) { - final int maxflushes = Math.max(min_flush_threshold, - size * flush_interval * flush_speed / Const.MAX_TIMESPAN); - final long now = System.currentTimeMillis(); - // 检查上个整点的数据能否被压缩 - flush(now / 1000 - Const.MAX_TIMESPAN - 1, maxflushes); - } - } - } -} -``` -再看`CompactionQueue#flush(final long cut_off, int maxflushes)` -```java -private Deferred> flush(final long cut_off, int maxflushes) { - final ArrayList> ds = - new ArrayList>(Math.min(maxflushes, max_concurrent_flushes)); - int nflushes = 0; - int seed = (int) (System.nanoTime() % 3); - for (final byte[] row : this.keySet()) { - final long base_time = Bytes.getUnsignedInt(row, - Const.SALT_WIDTH() + metric_width); - if (base_time > cut_off) { - // base_time比较靠近当前时间,则直接跳出 - break; - } else if (nflushes == max_concurrent_flushes) { - break; - } - // 这里会发向hbase发get请求获取时序数据,在callback中进行压缩操作 - ds.add(tsdb.get(row).addCallbacks(compactcb, handle_read_error)); - } - return group; -} -``` -最后看一下`compaction`具体做了啥,见`CompactionQueue#Compaction#compact()` -```java -public Deferred compact() { - // merge the datapoints, ordered by timestamp and removing duplicates - final ByteBufferList compacted_qual = new ByteBufferList(tot_values); - final ByteBufferList compacted_val = new ByteBufferList(tot_values); - - mergeDatapoints(compacted_qual, compacted_val); - - // build the compacted columns - final KeyValue compact = buildCompactedColumn(compacted_qual, compacted_val); - - final boolean write = updateDeletesCheckForWrite(compact); - - final byte[] key = compact.key(); - - deleted_cells.addAndGet(to_delete.size()); // We're going to delete this. - - if (write) { - // 把压缩后的结果回写到tsdb表 - Deferred deferred = tsdb.put(key, compact.qualifier(), compact.value(), compactedKVTimestamp); - - if (!to_delete.isEmpty()) { - // 压缩结果写入成功后 delete查询出来的cells - deferred = deferred.addCallbacks(new DeleteCompactedCB(to_delete), handle_write_error); - } - return deferred; - } -} - -// delete compacted cells的回调 -private final class DeleteCompactedCB implements Callback { - - /** What we're going to delete. */ - private final byte[] key; - private final byte[][] qualifiers; - - @Override - public Object call(final Object arg) { - return tsdb.delete(key, qualifiers).addErrback(handle_delete_error); - } - - @Override - public String toString() { - return "delete compacted cells"; - } -} -``` -追踪整个`compaction`过程,我们不难发现其中多了不少`get`、`write`、`delete`请求,数据量非常大的情况下无形给`HBase`带来不小压力。留意一下,这里可能也是我们重点优化的地方。 - -### 追加模式(appends) -相关文档: -`http://opentsdb.net/docs/build/html/user_guide/writing/index.html#appends` -> Also in 2.2, writing to HBase columns via appends is now supported. This can improve both read and write performance in that TSDs will no longer maintain a queue of rows to compact at the end of each hour, thus preventing a massive read and re-write operation in HBase. However due to the way appends operate in HBase, an increase in CPU utilization, store file size and HDFS traffic will occur on the region servers. Make sure to monitor your HBase servers closely. - -`tsd.storage.enable_appends`:默认是false -在追加模式下,`opentsdb`写入的时候,会将`rowkey`相同的点的value值写到一个单独的`ColumnQualifier`(0x050000)中。所以与之前的直接写入模式是兼容的,这就意味着可以随时启用或者禁用追加模式。 -```java -/** The prefix ID of append columns */ -public static final byte APPEND_COLUMN_PREFIX = 0x05; - -/** The full column qualifier for append columns */ -public static final byte[] APPEND_COLUMN_QUALIFIER = new byte[]{APPEND_COLUMN_PREFIX, 0x00, 0x00}; -``` -显然这就是我们想要的压缩后的效果。少了把已经写入`HBase`的数据拉过来在`opentsdb`内存压缩,回写数据,再删除原数据的一系列操作,当然了压力应该是丢给了`HBase`。 - -追加模式会消耗更多的HBase集群的资源(官方是这么说的,究竟多大,有待研究),另外本人猜测对于大量高并发的写入可能有锁的同步问题,讲道理单从瞬间写入性能考虑,追加模式下的性能应该是不及之前的直接写入。 - -## opentsdb UID的分配(UID Assignment) -相关文档: -`http://opentsdb.net/docs/build/html/user_guide/uids.html#uid` - -相信到这里应该已经到`UID`有一定的认识了,使用`UID`大大节省了存储空间。 -> Within the storage system there is a counter that is incremented for each metric, tagk and tagv. When you create a new tsdb-uid table, this counter is set to 0 for each type. - -很类似`mysql`中的自增主键。见`UniqueId#allocateUid()` -```java -private Deferred allocateUid() { -// randomize_id默认是false,两种方式:一种是随机数uid,另外一种是递增uid -// tagk和tagv目前无法配置,用的是递增uid(metric倒是可配 tsd.core.uid.random_metrics默认false) - if (randomize_id) { - return Deferred.fromResult(RandomUniqueId.getRandomUID()); - } else { //实际走这里,会去hbase的tsdb-uid表请求递增uid - return client.atomicIncrement(new AtomicIncrementRequest(table, - MAXID_ROW, ID_FAMILY, kind)); - } -} -``` -`tsdb-uid`表中`rowkey`为`0x00`的`cell`中存有目前三种类型的最大`UID` - -![查看metric、tagk、tagv目前的最大uid](https://img2020.cnblogs.com/blog/1546632/202004/1546632-20200406200646026-1861055799.png) - -这里我们看到`metric`、`tagk`、`tagv`三种类型的`UID`映射是独立的。另外,注意两个与此相关的配置项 -- `tsd.core.auto_create_metrics`:默认为`false`,是否给`tsdb-uid`表中不存在的`metric`分配`UID`,`false`的情况下,写入新的`metric`时序数据会抛出异常 -- `tsd.core.preload_uid_cache`:默认为`false`,是否程序启动时就从`tsdb-uid`表获取`UID`并缓存在本地,见`TSDB#TSDB(final HBaseClient client, final Config config)` -```java -if (config.getBoolean("tsd.core.preload_uid_cache")) { - final ByteMap uid_cache_map = new ByteMap(); - uid_cache_map.put(METRICS_QUAL.getBytes(CHARSET), metrics); - uid_cache_map.put(TAG_NAME_QUAL.getBytes(CHARSET), tag_names); - uid_cache_map.put(TAG_VALUE_QUAL.getBytes(CHARSET), tag_values); - UniqueId.preloadUidCache(this, uid_cache_map); -} -``` -从这里我们也可以看到使用这种`递增UID`分配方式,先来的`tagk`必然会分配到数值较小的`UID`,后来的`tagk`会分配到数值较大的`UID`,如此一来结合上文写入的时候`rowkey`中的`tags`会按照`tagk_uid`的byte数组进行排序,就能得出最先写入的`tagk`是排在`rowkey`中较为靠前的位置,那么知道了这种规则,在某些情况下对于查询优化有没有帮助呢? -## opentsdb 查询细节(Reading) -相关文档: -`http://opentsdb.net/docs/build/html/user_guide/query/index.html` - -查询放在这个地方讲是因为我们只有弄清楚数据是怎么存的,才会明白如何取。通过前文我们知道写入的时候`rowkey`中的`tags`会按照`tagk_uid`的byte数组进行排序,那么同样从`HBase`读数据的时候讲道理也应该这样排序是不是。来看`QueryUtil#setDataTableScanFilter()` -但是,正常情况下的`scan`(除非查询的时候设置`explicit_tags`为`true`),对于`tag`的过滤并不是直接拼在`rowkey`中,而是放在`scanner.setFilter(regex_filter)` -```java -final byte[] start_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; -final byte[] end_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; -scanner.setStartKey(start_row); -scanner.setStopKey(end_row); - -// 关于regex_filter生成下面有简单例子 -if (!(explicit_tags && enable_fuzzy_filter)) { - scanner.setFilter(regex_filter); - return; -} -``` -`QueryUtil#getRowKeyUIDRegex()` -```sh -// Generate a regexp for our tags. Say we have 2 tags: { 0 0 1 0 0 2 } -// and { 4 5 6 9 8 7 }, the regexp will be: -// "^.{7}(?:.{6})*\\Q\000\000\001\000\000\002\\E(?:.{6})*\\Q\004\005\006\011\010\007\\E(?:.{6})*$" -``` -官方对查询设置`explicit_tags`为`true`的介绍: -`http://opentsdb.net/docs/build/html/user_guide/query/filters.html#explicit-tags` -意思我已经知道了要查询的`metric`明确只有这些`tags`,想查询的时序数据不会出现其他`tag`,这样`opentsdb`就会把用户过滤的`tag`直接拼到`rowkey`中,一定程度上优化了查询。见代码 -```java -if (explicit_tags && enable_fuzzy_filter) { - fuzzy_key = new byte[prefix_width + (row_key_literals.size() * - (name_width + value_width))]; - fuzzy_mask = new byte[prefix_width + (row_key_literals.size() * - (name_width + value_width))]; - System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, - scanner.getCurrentKey().length); - - // 因为已经明确了只有哪些指定的tag,这个时候才会把tags直接拼到startKey中 - scanner.setStartKey(fuzzy_key); -} -``` -`explicit_tags`为`true`的情况下,会用`FuzzyRowFilter`,看一下源码中的描述 -```java -/** - * FuzzyRowFilter is a server-side fast-forward filter that allows skipping - * whole range of rows when scanning. The feature is available in HBase - * 0.94.5 and above. - *

- * It takes two byte array to match a rowkey, one to hold the fixed value - * and one to hold a mask indicating which bytes of the rowkey must match the - * fixed value. The two arrays must have the same length. - *

- * Bytes in the mask can take two values, 0 meaning that the corresponding byte - * in the rowkey must match the corresponding fixed byte and 1 meaning that the - * corresponding byte in the rowkey can take any value. - *

- * One can combine several {@link FuzzyFilterPair} to match multiple patterns at - * once. - *

- * Example : - * You store logs with this rowkey design : - * group(3bytes)timestamp(4bytes)severity(1byte) - * - * You want to get all FATAL("5") logs : - * * Build a FuzzyFilterPair with - * - rowkey : "????????5" - * - fuzzy mask : "111111110" - * And CRITICAL("4") logs only for web servers : - * * Add another FuzzyFilterPair with - * - rowkey : "web????4" - * - fuzzy mask : "00011110" - * - * @since 1.7 - */ -public final class FuzzyRowFilter extends ScanFilter { -// ... -} -``` -总结一下就是,如果你明确你要查的数据有哪几个`tag`,建议查询的时候指定`explicit_tags`为`true`,有助于查询优化。 -```sh -# Example 1: -http://host:4242/api/query?start=1h-ago&m=sum:explicit_tags:sys.cpu.system{host=web01} - -# Example 2: -http://host:4242/api/query?start=1h-ago&m=sum:explicit_tags:sys.cpu.system{host=*}{dc=*} - -# Example 3: -{ - "start":1584408560754, - "end":1584409460754, - "msResolution":false, - "queries":[ - { - "aggregator":"avg", - "metric":"metric.test", - "downsample":"5m-avg", - "explicitTags":true, - "filters":[ - { - "type":"literal_or", - "tagk":"instance", - "filter":"total", - "groupBy":true - }, - { - "type":"literal_or", - "tagk":"ip", - "filter":"192.168.1.1", - "groupBy":true - } - ] - } - ] -} -``` -关于`tsd.storage.use_otsdb_timestamp`这个配置与`HBase`特性有关。下篇写优化的时候再讲,这里提出来放在这里。`TsdbQuery#getScanner(final int salt_bucket)` -```java -// tsd.storage.use_otsdb_timestamp -if (tsdb.getConfig().use_otsdb_timestamp()) { - long stTime = (getScanStartTimeSeconds() * 1000); - long endTime = end_time == UNSET ? -1 : (getScanEndTimeSeconds() * 1000); - if (tsdb.getConfig().get_date_tiered_compaction_start() <= stTime && - rollup_query == null) { - // TODO - we could set this for rollups but we also need to write - // the rollup columns at the proper time. - scanner.setTimeRange(stTime, endTime); - } -} -``` - - - -## rowkey中加salt的情况(Salting) -相关文档: -`http://opentsdb.net/docs/build/html/user_guide/writing/index.html#salting` - -时序数据的写入,`写热点`是一个不可规避的问题,当某个`metric`下数据点很多时,则该`metric`很容易造成写入热点,即往一个`region server`写,甚至同一个`region`,如果这样,对这部分数据的读写都会落到`HBase`集群中的一台机器上,无法发挥集群的处理能力,甚至直接将某个`region server`压垮。加`salt`就是为了将时序数据的`rowkey`打散,从而分配到不同的`region`中,以均衡负载。 -> When enabled, a configured number of bytes are prepended to each row key. Each metric and combination of tags is then hashed into one "bucket", the ID of which is written to the salt bytes -从2.2开始,`OpenTSDB`采取了允许将`metric`加`salt`,加`salt`后的变化就是在`rowkey`前会拼上一个桶编号(`bucket index`)。 - -> To `enable salting` you must modify the config file parameter `tsd.storage.salt.width` and optionally `tsd.storage.salt.buckets`. We recommend setting the `salt width` to `1` and determine the number of `buckets` based on a factor of the number of `region servers` in your cluster. Note that at `query` time, the TSD will fire `tsd.storage.salt.buckets` number of `scanners` to fetch data. The proper number of salt buckets must be determined through experimentation as at some point `query performance` may suffer due to having too many scanners open and collating the results. In the future the salt width and buckets may be configurable but we didn't want folks changing settings on accident and losing data. - -对上面的描述解释一下: -`tsd.storage.salt.width`:`rowkey`加多少个byte前缀(默认0(即不开启),如果启用的话 建议1) -`tsd.storage.salt.buckets`:分桶数(默认20,建议根据region servers数确定) - -- 写入的时候如果启用了`salt`,则根据`metric_uid`+所有`[tagK+tagV]uid`组成的byte数组,计算`hashcode`值,对`分桶数`取模,得出`salt`位 - -`RowKey#prefixKeyWithSalt`(注意:取的是关键代码,去除了干扰信息) -```java -public static void prefixKeyWithSalt(final byte[] row_key) { - // tsd.storage.salt.width - if (Const.SALT_WIDTH() > 0) { - final int tags_start = Const.SALT_WIDTH() + TSDB.metrics_width() + - Const.TIMESTAMP_BYTES; - - // we want the metric and tags, not the timestamp - final byte[] salt_base = - new byte[row_key.length - Const.SALT_WIDTH() - Const.TIMESTAMP_BYTES]; - System.arraycopy(row_key, Const.SALT_WIDTH(), salt_base, 0, TSDB.metrics_width()); - System.arraycopy(row_key, tags_start,salt_base, TSDB.metrics_width(), - row_key.length - tags_start); - // 这里通过对salt_buckets取模得出salt位的数值 - int modulo = Arrays.hashCode(salt_base) % Const.SALT_BUCKETS();// tsd.storage.salt.buckets - - final byte[] salt = getSaltBytes(modulo); - // 填充salt位的byte - System.arraycopy(salt, 0, row_key, 0, Const.SALT_WIDTH()); - } -} -``` - -- 这个时候大多数人就会疑惑了,在`rowkey`前加了`salt`位,那么查询的时候怎么搞? - 客户端查询`OpenTSDB`一条数据,`OpenTSDB`将这个请求拆成`分桶数`个查询到`HBase`,然后返回桶数个结果集到`OpenTSDB`层做合并。对`HBase`并发请求相应的也会桶数倍的扩大。见`TsdbQuery#findSpans()` -```java -if (Const.SALT_WIDTH() > 0) { - final List scanners = new ArrayList(Const.SALT_BUCKETS()); - for (int i = 0; i < Const.SALT_BUCKETS(); i++) { - // 构建出等于分桶数大小个scanner - scanners.add(getScanner(i)); - } - scan_start_time = DateTime.nanoTime(); - return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, - delete, rollup_query, query_stats, query_index, null, - max_bytes, max_data_points).scan(); -} -``` -在每一个`scanner`的`rowkey`前面填充`bucket index`作为`salt`位,这样才能去`hbase`中`scan`到完整的结果,见`QueryUtil#getMetricScanner()` -```java -public static Scanner getMetricScanner(final TSDB tsdb, final int salt_bucket, - final byte[] metric, final int start, final int stop, - final byte[] table, final byte[] family) { - - if (Const.SALT_WIDTH() > 0) { - final byte[] salt = RowKey.getSaltBytes(salt_bucket); - // 这里把salt_bucket填充到rowkey中 - System.arraycopy(salt, 0, start_row, 0, Const.SALT_WIDTH()); - System.arraycopy(salt, 0, end_row, 0, Const.SALT_WIDTH()); - } - return scanner; -} -``` -## 其他配置(Configuration) -相关文档: -- `opentsdb`的配置:`http://opentsdb.net/docs/build/html/user_guide/configuration.html` -- `AsyncHBase client`的配置:`http://opentsdb.github.io/asynchbase/docs/build/html/configuration.html` - -`opentsdb`使用的`hbase client`是`http://opentsdb.github.io/asynchbase/` -```java -public TSDB(final HBaseClient client, final Config config) { - this.config = config; - if (client == null) { - final org.hbase.async.Config async_config; - if (config.configLocation() != null && !config.configLocation().isEmpty()) { - try { - // AsyncHBase client读取和opentsdb一样的文件 - // 所以 有一些需要设置AsyncHBase client的地方直接写在opentsdb的配置文件就能生效 - async_config = new org.hbase.async.Config(config.configLocation()); - } catch (final IOException e) { - throw new RuntimeException("Failed to read the config file: " + - config.configLocation(), e); - } - } else { - async_config = new org.hbase.async.Config(); - } - async_config.overrideConfig("hbase.zookeeper.znode.parent", - config.getString("tsd.storage.hbase.zk_basedir")); - async_config.overrideConfig("hbase.zookeeper.quorum", - config.getString("tsd.storage.hbase.zk_quorum")); - this.client = new HBaseClient(async_config); - } else { - this.client = client; - } -} -``` -性能优化的一方面可能与参数调优有关,有些与启动参数,操作系统设置等有关,有些参数就是写在配置文件的(比如说最大连接数、超时时间等等) - -这里提一下前面没有讲到的与`opentsdb`相关的两个配置。 -- `tsd.query.skip_unresolved_tagvs`:默认为false,查询的时候遇到不存在的tagv时候是否跳过,true则跳过,false则抛出异常,个人感觉这个默认false极不友好。`TagVFilter#resolveTags()#TagVErrback` -```java -/** - * Allows the filter to avoid killing the entire query when we can't resolve - * a tag value to a UID. - */ -class TagVErrback implements Callback { - @Override - public byte[] call(final Exception e) throws Exception { - if (config.getBoolean("tsd.query.skip_unresolved_tagvs")) { - LOG.warn("Query tag value not found: " + e.getMessage()); - return null; - } else { - // 默认情况下直接抛出异常 - throw e; - } - } -} -``` -- `AsyncHBase Configuration`中的`hbase.rpc.timeout`:How long, in milliseconds, to wait for a response to an RPC from a region server before failing the RPC with a RpcTimedOutException. This value can be overridden on a per-RPC basis. A value of 0 will not allow RPCs to timeout - -## http接口(HTTP API) -相关文档: -`http://opentsdb.net/docs/build/html/api_http/index.html` - -常用: -- put:`http://opentsdb.net/docs/build/html/api_http/put.html` -- query:`http://opentsdb.net/docs/build/html/api_http/query/index.html` -- uid:`http://opentsdb.net/docs/build/html/api_http/uid/index.html` -- stats:`http://opentsdb.net/docs/build/html/api_http/stats/index.html` - -同时我们注意到:`OpenTSDB3.0`相关的工作正在进行中(`work-in-progress`),详情:`http://opentsdb.net/docs/3x/build/html/index.html` - -## opentsdb连接Kerberos认证的HBase(非重点,仅顺手记录于此) -相关文档: -`http://opentsdb.github.io/asynchbase/docs/build/html/authentication.html` - -`http://opentsdb.github.io/asynchbase/docs/build/html/configuration.html`(搜`kerberos`关键字) - -相关问题讨论: -`https://github.com/OpenTSDB/opentsdb/issues/491` - -参考带有`Kerberos`认证hbase docker镜像`Dockerfile`项目: -`https://github.com/Knappek/docker-phoenix-secure` -该项目中`bootstrap-phoenix.sh`、`docker-compose.yml`以及`config_files`下的配置文件很有参考价值 - -### 具体操作 -1. 根据实际情况在`/etc/opentsdb/opentsdb.conf`配置 末尾添加: -```sh -hbase.security.auth.enable=true -hbase.security.authentication=kerberos -hbase.sasl.clientconfig=Client -hbase.kerberos.regionserver.principal=hbase/_HOST@EXAMPLE.COM -``` - -2. 根据实际情况新建`hbase-client.jaas`文件,文件内容基本如下样子 -```sh -Client { - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - useTicketCache=false - keyTab="/etc/security/keytabs/hbase.keytab" - principal="hbase/phoenix.docker.com"; -}; -``` - -3. 修改`/usr/share/opentsdb/etc/init.d/opentsdb`文件,修改启动参数 -```sh -# start command的位置(约第78行处)加上 -Djava.security.auth.login.config=hbase-client.jaas文件路径 -# 注意:如果Zookeeper没有加Kerberos认证,再加一个参数 -Dzookeeper.sasl.client=false -JVMARGS="-Djava.security.auth.login.config=/.../jaas.conf" -``` -重启`opentsdb`,如果成功,则能看到如下示例日志: -> 13:31:55.045 INFO [`ZooKeeperSaslClient`.run] - Client will use `GSSAPI as SASL mechanism`. -13:31:55.062 INFO [Login.getRefreshTime] - TGT valid starting at: Fri Apr 03 13:31:54 CST 2020 -13:31:55.062 INFO [Login.getRefreshTime] - TGT expires: Sat Apr 04 13:31:54 CST 2020 - -> 13:31:55.255 INFO [`KerberosClientAuthProvider`.run] - Client will use `GSSAPI as SASL mechanism`. -13:31:55.269 INFO [RegionClient.channelConnected] - Initialized security helper: org.hbase.async.SecureRpcHelper96@6471f1e for region client: RegionClient@63709091(chan=null, #pending_rpcs=2, #batched=0, #rpcs_inflight=0) -13:31:55.276 INFO [SecureRpcHelper96.handleResponse] - `SASL client context established`. Negotiated QoP: auth on for: RegionClient@63709091(chan=null, #pending_rpcs=2, #batched=0, #rpcs_inflight=0) - - -## 写在后面 -阅读、探索的过程很累,遇到不太理解的地方又会很困惑,但柳暗花明又一村,凌绝顶一览众山小的喜悦却难以言表。另外,整理的过程也挺烦人,既然花时间整理了,我尽量让感兴趣的读者能从中有一丝收获。当然了,整理的过程也锻炼了我学习知识、解决问题的思路与能力。由于本人能力之有限、理解之不透彻,文中如有错误的理解、不恰当的描述,衷心希望朋友提出一起讨论! \ No newline at end of file diff --git a/DesignPattern/adapter-pattern.md b/DesignPattern/adapter-pattern.md deleted file mode 100644 index 5a9491c..0000000 --- a/DesignPattern/adapter-pattern.md +++ /dev/null @@ -1,250 +0,0 @@ -# 适配器模式(Adapter Pattern)——不兼容结构的协调 - - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213`。 - -## 模式概述 - -### 模式定义 - -与电源适配器相似,在适配器模式中引入了一个被称为适配器(`Adapter`)的包装类,而它所包装的对象称为适配者(`Adaptee`),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。 - -> 适配器模式(`Adapter Pattern`): 将一个接口转换成期望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(`Wrapper`)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。 - -*注意:在适配器模式定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合。* - -### 模式结构图 - -在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为`对象适配器`和`类适配器`两种,在`对象适配器`模式中,适配器与适配者之间是`关联关系`;在`类适配器`模式中,适配器与适配者之间是`继承`关系。在实际开发中,对象适配器的使用频率更高,对象适配器模式结构如图所示 - -![对象适配器结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200517093507246-1923277557.png) - -在对象适配器模式结构图中包含如下几个角色: -- `Target(目标抽象类)`:目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。 -- `Adapter(适配器类)`:适配器可以调用另一个接口,作为一个转换器,对`Adaptee`和`Target`进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承(或者实现)`Target`并关联一个`Adaptee`对象使二者产生联系。 -- `Adaptee(适配者类)`:适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。 - -### 模式伪代码 - -在`对象适配器`中,客户端需要调用`request()`方法,而适配者类`Adaptee`没有该方法,但是它所提供的`specificRequest()`方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类`Adapter`,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的`request()`方法中调用适配者的`specificRequest()`方法。因为适配器类与适配者类是`关联关系`(也可称之为委派关系),所以这种适配器模式称为`对象适配器模式`。典型的对象适配器代码如下所示: -```java -public class Adapter implements Target { - // 维持一个对适配者对象的引用 - private Adaptee adaptee; - - // 构造注入适配者 - public Adapter(Adaptee adaptee) { - this.adaptee = adaptee; - } - - @Override - public void request() { - // 转发调用 - adaptee.specificRequest(); - } -} -``` - -### 类适配器,双向适配器,缺省适配器 - -#### 类适配器 -`类适配器`模式和`对象适配器`模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是`关联关系`,而类适配器模式中适配器和适配者是`继承关系` - -![类适配器结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200517095252755-1268131042.png) - -适配器类实现了抽象目标类接口`Target`,并继承了适配者类,在适配器类的`request()`方法中调用所继承的适配者类的`specificRequest()`方法,实现了适配。 -典型代码实现如下: -```java -public class Adapter extends Adaptee implements Target { - @Override - public void request() { - specificRequest(); - } -} -``` -由于`Java`、`C#`等语言不支持多重类继承,因此`类适配器`的使用受到很多限制,例如如果目标抽象类`Target`不是接口,而是一个类,就无法使用类适配器;此外,如果适配者`Adaptee`为最终(`final`)类,也无法使用类适配器。在Java等面向对象编程语言中,大部分情况下我们使用的是对象适配器,`类适配器`较少使用。 - -#### 双向适配器 - -`双向适配器`: 在`对象适配器`的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器。 - -![双向适配器结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200517100001199-2025299022.png) - -典型代码实现如下: -```java -public class Adapter implements Target, Adaptee { - //同时维持对抽象目标类和适配者的引用 - private Target target; - private Adaptee adaptee; - - public Adapter(Target target) { - this.target = target; - } - - public Adapter(Adaptee adaptee) { - this.adaptee = adaptee; - } - - @Override - public void request() { - adaptee.specificRequest(); - } - - @Override - public void specificRequest() { - target.request(); - } -} -``` -在实际开发中,我们很少使用`双向适配器`。违背了`单一职责`原则,相当于一个适配器承担了两个适配器的职责。 - -#### 缺省适配器 - -`缺省适配器`模式是适配器模式的一种变体,其应用也较为广泛。 -> `缺省适配器模式(Default Adapter Pattern)`:当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为`单接口适配器模式`。 - -![缺省适配器结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200517101208598-111312955.png) - -典型代码实现如下: -```java -public abstract class Adapter implements Target { - @Override - public void request1() { - // 空实现,让具体实现类去有选择地实现 - } - - @Override - public void request2() { - // 空实现,让具体实现类去有选择地实现 - } - - @Override - public void request3() { - // 空实现,让具体实现类去有选择地实现 - } -} - -public class ConcreteAdapter extends Adapter { - // 维持一个对适配者对象的引用 - private Adaptee adaptee; - - // 构造注入适配者 - public Adapter(Adaptee adaptee) { - this.adaptee = adaptee; - } - - @Override - public void request1() { - // 只实现request1 - adaptee.specificRequest(); - } -} -``` - -## 模式应用 - -### 模式在JDK中的应用 -在JDK中,`IO`类中也大量使用到了适配器模式。比如说`StringReader`将`String`适配到`Reader`,`InputStreamReader`将`InputStream`适配到`Reader`等等。 - -这里用`StringReader`来说明。这里的`StringReader`相当于上述的`Adapter`,`Reader`相当于上述的`Target`,`String`相当于上述的`Adaptee` -```java -public class StringReader extends Reader { - - // 维持对adaptee对象的引用 - private String str; - - private int length; - private int next = 0; - private int mark = 0; - - /** - * 构造注入一个String用于之后的read操作 - */ - public StringReader(String s) { - this.str = s; - this.length = s.length(); - } - - // 这里相当于是在做适配操作,转为目标对象所期望的请求 - public int read() throws IOException { - synchronized (lock) { - ensureOpen(); - if (next >= length) - return -1; - return str.charAt(next++); - } - } -} -``` - -### 模式在开源项目中的应用 - -其实不只是开源项目,我们自己写的项目很多地方都是隐含着适配器模式,只是有时候这种特性表现的不是很明显(因为我们很自然去使用),以至于我们都没有给类名命成`XxxAdapter`,比如说我们使用第三方库,第三方库某方法名太长或者参数过多,或者调用过于复杂了,我们可能会对第三方库再次做个封装,把适合自己项目当前业务逻辑的默认参数,默认实现补充完整,让其他地方很方便调用,举个具体例子,项目中可能经常会用到`HttpClient`,大多数情况下,对现有的`HttpClient`再次封装(比如client的创建、http响应结果的统一处理等等),封装成方便自己项目使用的`SpecialHttpClient`,如果你还想切换不同的底层`HttpClient`实现,还可以对`SpecialHttpClient`抽出来一个接口,通过不同的`Adapter`来注入不同的`HttpClient`(比如apache的`HttpClient`、`OkHttpClient`、Spring的`RestTemplate`以及`WebClient`等等)来实现,这种很自然的思想 个人觉得本质上也用到了`适配器模式`,相当于是把第三方的`HttpClient`适配成了自己的`SpecialHttpClient`。 - -当转换的源不是单一的时候,这种`适配器`思想就凸显出来了(对应上面的例子就是说 项目中需要同时用到apache的`HttpClient`、Spring的`RestTemplate`以及`WebClient`等)。 - -这里举个`Spring`中的例子。在`Spring`的`AOP`中,由于`Advisor`需要的是`MethodInterceptor`对象,所以每一个`Advisor`中的`Advice`都要适配成对应的`MethodInterceptor`对象 -```java -public interface AdvisorAdapter { - - boolean supportsAdvice(Advice advice); - - MethodInterceptor getInterceptor(Advisor advisor); -} - - -class ThrowsAdviceAdapter implements AdvisorAdapter, Serializable{ - - @Override - public boolean supportsAdvice(Advice advice) { - return (advice instanceof ThrowsAdvice); - } - - @Override - public MethodInterceptor getInterceptor(Advisor advisor) { - return new ThrowsAdviceInterceptor(advisor.getAdvice()); - } - -} -``` - -## 模式总结 - -适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用。 - -### 主要优点 - -无论是对象适配器模式还是类适配器模式都具有如下优点: - -(1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构,提高了扩展性,符合“开闭原则” -(2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。 - -具体来说,`类适配器`模式还有如下优点: - -由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。 - -`对象适配器`模式还有如下优点: - -(1) 一个对象适配器可以把多个不同的适配者适配到同一个目标; - -(2) 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。 - -### 主要缺点 - -`类适配器`模式的缺点如下: - -(1) 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者; - -(2) 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类; - -(3) 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。 - -对象适配器模式的缺点如下: - -与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦(比如说适配者类中的某些方法是`protected`,而我们做适配的时候刚好需要用到)。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。 - -### 适用场景 - -在以下情况下可以考虑使用适配器模式: - -系统需要使用(复用)一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码等等,可使用适配器模式协调诸多不兼容结构的场景。 diff --git a/DesignPattern/bridge-pattern.md b/DesignPattern/bridge-pattern.md deleted file mode 100644 index fe9d3bd..0000000 --- a/DesignPattern/bridge-pattern.md +++ /dev/null @@ -1,61 +0,0 @@ -# 桥接模式(Bridge Pattern)——处理多维度变化 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -在正式介绍桥接模式之前,先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。 - -假如我们需要大中小3种型号的画笔,能够绘制12种不同的颜色,如果使用蜡笔,需要准备3×12 = 36支,但如果使用毛笔的话,只需要提供3种型号的毛笔,外加12个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与36支蜡笔同样的功能。 - -如果增加一种新型号的画笔,并且也需要具有12种颜色,对应的蜡笔需增加12支,而毛笔只需增加一支。 - -为什么会这样呢? - -通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。 - -如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,而毛笔很好地将二者解耦,使用起来非常灵活,扩展也更为方便。在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情况,即本章将要介绍的桥接模式。 - -## 模式概述 - -桥接模式是一种很实用的结构型设计模式。 - -如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。 - -与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。 - -### 模式定义 - -桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。 - -> 桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式。 - -### 模式结构图 - -在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。 - -通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。 - -例如: 对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。 - -在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,结构示意图如下图所示: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211101115223016-1034333776.png) - - -## 模式总结 - -桥接模式是设计Java虚拟机和实现JDBC等驱动程序的核心模式之一,应用较为广泛。 - -在软件开发中如果一个类或一个系统有多个变化维度时,都可以尝试使用桥接模式对其进行设计。 - -桥接模式为多维度变化的系统提供了一套完整的解决方案,并且降低了系统的复杂度。 - -### 优点 - -1. 分离抽象接口及其实现部分。 -2. 桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。 -3. 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。 - -### 适用场景 - -1. 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。 -2. 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 diff --git a/DesignPattern/builder-pattern.md b/DesignPattern/builder-pattern.md deleted file mode 100644 index 32fa8be..0000000 --- a/DesignPattern/builder-pattern.md +++ /dev/null @@ -1,271 +0,0 @@ -# 建造者模式(Builder Pattern)——复杂对象的组装与创建 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -### 模式定义 - -没有人买车会只买一个轮胎或者方向盘,大家买的都是一辆包含轮胎、方向盘和发动机等多个部件的完整汽车。如何将这些部件组装成一辆完整的汽车并返回给用户,这是`建造者模式`需要解决的问题。建造者模式又称为生成器模式,它是一种较为复杂、使用频率也相对较低的创建型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。 - -它将客户端与包含多个组成部分(或部件)的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。 - -> `建造者模式(Builder Pattern)`:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。 - -建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。 - -### 模式结构图 - -建造者模式结构图如下所示: - -![建造者模式结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200531203645693-665857209.png) - -建造者模式结构图中包含如下几个角色: -- `Builder(抽象建造者)`:它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。 - -- `ConcreteBuilder(具体建造者)`:它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。 - -- `Product(产品角色)`:它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。 - -- `Director(指挥者)`:指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。 - -在建造者模式的定义中提到了复杂对象,那么什么是`复杂对象`?简单来说,复杂对象是指那些包含多个非简单类型的成员属性,这些成员属性也称为部件或零件,如汽车包括方向盘、发动机、轮胎等部件,电子邮件包括发件人、收件人、主题、内容、附件等部件。 - -### 模式伪代码 - -定义`产品角色`,典型代码如下: -```java -public class Product { - // 定义部件,部件可以是任意类型,包括值类型和引用类型 - private String partA; - - private String partB; - - private String partC; - - // getter、setter方法省略 -} -``` - -在`抽象建造者`中定义了产品的创建方法和返回方法,其典型代码如下 -```java -public abstract class Builder { - // 创建产品对象 - protected Product product = new Product(); - - public abstract void buildPartA(); - - public abstract void buildPartB(); - - public abstract void buildPartC(); - - // 返回产品对象 - public Product getResult() { - return product; - } -} -``` - -在抽象类`Builder`中声明了一系列抽象的`buildPartX()`方法用于创建复杂产品的各个部件,具体建造过程在`ConcreteBuilder`中实现,此外还提供了工厂方法`getResult()`,用于返回一个建造好的完整产品。 - -在`ConcreteBuilder`中实现了`buildPartX()`方法,通过调用`Product`的`setPartX()`方法可以给产品对象的成员属性设值。不同的具体建造者在实现`buildPartX()`方法时将有所区别。 - -在建造者模式的结构中还引入了一个指挥者类`Director`,该类主要有两个作用:一方面它隔离了客户与创建过程;另一方面它控制产品的创建过程,包括某个`buildPartX()`方法是否被调用以及多个`buildPartX()`方法调用的先后次序等。指挥者针对`抽象建造者`编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。在实际生活中也存在类似指挥者一样的角色,如一个客户去购买电脑,电脑销售人员相当于指挥者,只要客户确定电脑的类型,电脑销售人员可以通知电脑组装人员给客户组装一台电脑。指挥者类的代码示例如下: -```java -public class Director { - - private Builder builder; - - public Director(Builder builder) { - this.builder = builder; - } - - public void setBuilder(Builder builder) { - this.builder = builer; - } - - //产品构建与组装方法 - public Product construct() { - - builder.buildPartA(); - builder.buildPartB(); - builder.buildPartC(); - - return builder.getResult(); - } -} -``` - -在指挥者类中可以注入一个`抽象建造者`类型的对象,其核心在于提供了一个建造方法`construct()`,在该方法中调用了builder对象的构造部件的方法,最后返回一个产品对象。 - -对于客户端而言,只需关心具体的建造者即可,代码片段如下所示: -```java -public static void main(String[] args) { - Builder builder = new ConcreteBuilder(); - - Director director = new Director(builder); - - Product product = director.construct(); -} -``` - -### 模式简化 -在有些情况下,为了简化系统结构,可以将`Director`和抽象建造者`Builder`进行合并,在`Builder`中提供逐步构建复杂产品对象的`construct()`方法。 -```java -public abstract class Builder { - // 创建产品对象 - protected Product product = new Product(); - - public abstract void buildPartA(); - - public abstract void buildPartB(); - - public abstract void buildPartC(); - - // 返回产品对象 - public Product construct() { - buildPartA(); - buildPartB(); - buildPartC(); - return product; - } -} -``` -## 模式应用 - -### 模式在JDK中的应用 -`java.util.stream.Stream.Builder` -```java -public interface Builder extends Consumer { - /** - * Adds an element to the stream being built. - */ - default Builder add(T t) { - accept(t); - return this; - } - - /** - * Builds the stream, transitioning this builder to the built state - */ - Stream build(); -} -``` -### 模式在开源项目中的应用 - -看下`Spring`是如何构建`org.springframework.web.servlet.mvc.method.RequestMappingInfo` -```java -/** - * Defines a builder for creating a RequestMappingInfo. - * @since 4.2 - */ -public interface Builder { - /** - * Set the path patterns. - */ - Builder paths(String... paths); - - /** - * Set the request method conditions. - */ - Builder methods(RequestMethod... methods); - - /** - * Set the request param conditions. - */ - Builder params(String... params); - - /** - * Set the header conditions. - *

By default this is not set. - */ - Builder headers(String... headers); - - /** - * Build the RequestMappingInfo. - */ - RequestMappingInfo build(); -} -``` - -`Builder`接口的默认实现,如下: -```java -private static class DefaultBuilder implements Builder { - - private String[] paths = new String[0]; - - private RequestMethod[] methods = new RequestMethod[0]; - - private String[] params = new String[0]; - - private String[] headers = new String[0]; - - public DefaultBuilder(String... paths) { - this.paths = paths; - } - - @Override - public Builder paths(String... paths) { - this.paths = paths; - return this; - } - - @Override - public DefaultBuilder methods(RequestMethod... methods) { - this.methods = methods; - return this; - } - - @Override - public DefaultBuilder params(String... params) { - this.params = params; - return this; - } - - @Override - public DefaultBuilder headers(String... headers) { - this.headers = headers; - return this; - } - - @Override - public RequestMappingInfo build() { - ContentNegotiationManager manager = this.options.getContentNegotiationManager(); - - PatternsRequestCondition patternsCondition = new PatternsRequestCondition( - this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(), - this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(), - this.options.getFileExtensions()); - - return new RequestMappingInfo(this.mappingName, patternsCondition, - new RequestMethodsRequestCondition(this.methods), - new ParamsRequestCondition(this.params), - new HeadersRequestCondition(this.headers), - new ConsumesRequestCondition(this.consumes, this.headers), - new ProducesRequestCondition(this.produces, this.headers, manager), - this.customCondition); - } -} -``` -`Spring`框架中许多`构建类`的实例化使用了类似上面方式,总结有以下特点: -1. `Builder`大多是`构建类`的内部类,`构建类`提供了一个静态创建`Builder`的方法 -2. `Builder`返回`构建类的实例`,大多通过`build()`方法 -3. 构建过程有`大量参数`,除了几个必要参数,用户可根据自己所需选择设置其他参数实例化对象 -## 模式总结 - -建造者模式的核心在于如何一步步构建一个包含多个组成部件的完整对象,使用相同的构建过程构建不同的产品,在软件开发中,如果我们需要创建复杂对象并希望系统具备很好的灵活性和可扩展性可以考虑使用建造者模式。 - -`建造者模式`与`抽象工厂模式`有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。**如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。** - -### 主要优点 - -1. 将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象 -2. 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。 - -### 适用场景 - -(1) 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。 - -(2) 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。 - -(3) 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。 \ No newline at end of file diff --git a/DesignPattern/chain-of-responsibility-pattern.md b/DesignPattern/chain-of-responsibility-pattern.md deleted file mode 100644 index 615ff37..0000000 --- a/DesignPattern/chain-of-responsibility-pattern.md +++ /dev/null @@ -1,105 +0,0 @@ -# 职责链模式(Chain of Responsibility Pattern)——请求的链式处理 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -“一对二”,“过”,“过”……这声音熟悉吗?你会想到什么? - -对!纸牌。在类似“斗地主”这样的纸牌游戏中,某人出牌给他的下家,下家看看手中的牌,如果要不起上家的牌则将出牌请求再转发给他的下家,其下家再进行判断。 - -一个循环下来,如果其他人都要不起该牌,则最初的出牌者可以打出新的牌。 - -在这个过程中,牌作为一个请求沿着一条链在传递,每一位纸牌的玩家都可以处理该请求。 - -在设计模式中,我们也有一种专门用于处理这种请求链式传递的模式,它就是本章将要介绍的职责链模式。 - -## 模式概述 - -很多情况下,在一个软件系统中可以处理某个请求的对象不止一个。 - -职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。 - -链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理。 - -客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上即可,实现请求发送者和请求处理者解耦。 - -### 模式定义 - -> 职责链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。 - -### 模式结构图 - -职责链模式结构的核心在于引入了一个抽象处理者。职责链模式结构如下图所示: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211101153436048-527226444.png) - -在职责链模式结构图中包含如下几个角色: - -- Handler(抽象处理者) - -它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。 - -- ConcreteHandler(具体处理者) - -它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。 - -### 模式伪代码 - -在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。 - -请求在这个链上传递,直到链上的某一个对象决定处理此请求。 - -发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。 - -职责链模式的核心在于抽象处理者类的设计,抽象处理者的典型代码如下所示: -```java -public abstract class Handler { - // 维持对下家的引用 - protected Handler successor; - - // 抽象处理请求的方法 - public abstract void handleRequest(Object request); -} - -public class ConcreteHandler extends Handler { - @Override - public void handleRequest(Object request) { - if (请求满足条件) { - // 处理请求 - } else { - // 转发请求 - this.successor.handleRequest(request); - } - } -} -``` - -注意,创建职责链伪代码这里没有写。其实很简单,先创建这些具体的处理器对象,然后依次把这些处理器对象串起来。 - -职责链模式降低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。 - -### 纯与不纯的职责链模式 - -职责链模式可分为纯的职责链模式和不纯的职责链模式两种: - -- 纯的职责链模式 - -一个纯的职责链模式要求一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况。 - -而且在纯的职责链模式中,要求一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况。 - -- 不纯的职责链模式 - -在一个不纯的职责链模式中允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,而且一个请求可以最终不被任何处理者对象所接收。 - - -## 模式总结 - -职责链模式通过建立一条链来组织请求的处理者,请求将沿着链进行传递,请求发送者无须知道请求在何时、何处以及如何被处理,实现了请求发送者与处理者的解耦。 - -在软件开发中,如果遇到有多个对象可以处理同一请求时可以应用职责链模式,例如在Web应用开发中创建一个过滤器(`Filter`)链来对请求数据进行过滤,在工作流系统中实现公文的分级审批等等,使用职责链模式可以较好地解决此类问题。 - -### 适用场景 - -- 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。 -- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 -- 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。 diff --git a/DesignPattern/command-pattern.md b/DesignPattern/command-pattern.md deleted file mode 100644 index 67685d6..0000000 --- a/DesignPattern/command-pattern.md +++ /dev/null @@ -1,105 +0,0 @@ -# 命令模式(Command Pattern)——请求发送者与接收者解耦 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -生活中,在购买开关时,我们并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、排气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制排气扇或者其他电器设备。开关与电器之间通过电线建立连接,如果开关打开,则电线通电,电器工作;反之,开关关闭,电线断电,电器停止工作。相同的开关可以通过不同的电线来控制不同的电器,如下图所示: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211104161259081-777714571.png) - -图中,我们可以将开关理解成一个请求的发送者,用户通过它来发送一个“开灯”请求,而电灯是“开灯”请求的最终接收者和处理者,在图中,开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者,只需更换一根电线,相同的发送者(开关)即可对应不同的接收者(电器)。 - -在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式为此类问题提供了一个较为完美的解决方案。 - -### 模式定义 - -命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。 - -> 命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。 - -### 模式结构图 - -命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如下图所示: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211104170233163-1408925910.png) - -### 模式伪代码 - -命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的`execute()`方法,每个具体命令类将一个`Receiver`类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了`execute()`方法的不同实现,并调用不同接收者的请求处理方法。 - -```java -/** - * 抽象命令类 - */ -public abstract class Command { - /** - * 执行命令 - */ - abstract void execute(); - - /** - * 撤销命令 - */ - abstract void undo(); -} - - -/** - * 请求接收者 - */ -public class Receiver { - - public void executeAction() { - // 具体执行操作 - } - - public void undoAction() { - // 具体撤销操作 - } -} - - -/** - * 具体的命令 - */ -public class ConcreteCommand extends Command { - // 维持一个对请求接收者对象的引用 - private Receiver receiver; - - @Override - public void execute() { - // 接收者的业务操作 - receiver.executeAction(); - } - - @Override - void undo() { - // 接收者的撤销操作 - receiver.undoAction(); - } -} -``` - -当然你在实际开源项目中看到运用命令模式的代码可能并不是这样的,我见过的大多是抽象出`Command`类和`Executor`类。设计模式本来就是一种内功心法,重意不重形,学习设计模式主要是看前辈们是如何进行抽象、解耦,使得软件设计最大程序符合单一职责、开闭等原则。 - - -## 模式总结 - -命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。 - -命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。 - -### 主要优点 - -- 降低系统的耦合度,请求可以独立扩展 -- 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案 - -### 主要缺点 - -使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。 - -### 适用场景 - -- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。 -- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。 diff --git a/DesignPattern/composite-pattern.md b/DesignPattern/composite-pattern.md deleted file mode 100644 index e95c73e..0000000 --- a/DesignPattern/composite-pattern.md +++ /dev/null @@ -1,262 +0,0 @@ -# 组合模式(Composite Pattern)——树形结构的处理 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -`树形结构`在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是`组合模式`需要解决的问题。组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,也可以一致性地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。 - -### 模式定义 - -> `组合模式(Composite Pattern)`:组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。 - -### 模式结构图 - -组合模式结构图如下所示: - -![组合模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200620095658054-568368664.png) - -在组合模式结构图中包含如下几个角色: - -- `Component`(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。 - -- `Leaf`(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。 - -- `Composite`(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。 - -> 组合模式的关键是定义了一个`抽象构件类`,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。 - -### 模式伪代码 - -对于客户端而言,一般针对抽象构件编程,而无须关心其具体子类是容器构件还是叶子构件。抽象构建类典型代码如下: -```java -public abstract class Component { - public abstract void add(Component c); //增加成员 - - public abstract void remove(Component c); //删除成员 - - public abstract Component getChild(int i); //获取成员 - - public abstract void operation(); //业务方法 -} -``` - -如果继承抽象构件的是叶子构件,则其典型代码如下所示: - -```java -public class Leaf extends Component { - @Override - public void add(Component c) { - //异常处理或错误提示 - } - - @Override - public void remove(Component c) { - //异常处理或错误提示 - } - - @Override - public Component getChild(int i) { - //异常处理或错误提示 - return null; - } - - @Override - public void operation() { - //叶子构件具体业务方法的实现 - } -} -``` - -如果继承抽象构件的是容器构件,则其典型代码如下所示: -```java -public class Composite extends Component { - - private List list = new ArrayList<>(); - - @Override - public void add(Component c) { - list.add(c); - } - - @Override - public void remove(Component c) { - list.remove(c); - } - - @Override - public Component getChild(int i) { - return (Component) list.get(i); - } - - @Override - public void operation() { - //容器构件具体业务方法的实现 - //递归调用成员构件的业务方法 - for (Object obj : list) { - ((Component) obj).operation(); - } - } -} -``` - -客户端对抽象构件类进行编程 -```java -public class Client { - public static void main(String[] args) { - Component component; - component = new Leaf(); - //component = new Composite(); - - // 无须知道到底是叶子还是容器 - // 可以对其进行统一处理 - component.operation(); - } -} -``` - -## 模式简化 - -### 透明组合模式 - -> 透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式。 - -透明组合模式的完整结构图如下: - -![透明组合模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200620104846072-1966986837.png) - -也可以将叶子构件的`add()`、`remove()`等方法的实现代码移至`Component`中,由`Component`提供统一的默认实现,这样子类就不必强制去实现管理子Component。代码如下所示: -```java -public abstract class Component { - public void add(Component c) { - throw new RuntimeException("不支持的操作"); - } - - public void remove(Component c) { - throw new RuntimeException("不支持的操作"); - } - - public Component getChild(int i) { - throw new RuntimeException("不支持的操作"); - } - - public abstract void operation(); //业务方法 -} -``` - -`透明组合模式`的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供add()、remove()以及getChild()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。 - -### 安全组合模式 - -> 安全组合模式中,在抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法。 - - -安全组合模式的完整结构图如下: - -![安全组合模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200620105205351-475574714.png) - -此时`Component`就应该这样定义了 -```java -public abstract class Component { - // 业务方法 - public abstract void operation(); -} -``` - -`安全组合模式`的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。在实际应用中,安全组合模式的使用频率也非常高,在`Java AWT中`使用的组合模式就是安全组合模式。 - -## 模式应用 - -### 模式在JDK中的应用 - -`Java SE`中的`AWT`和`Swing`包的设计就基于组合模式,在这些界面包中为用户提供了大量的容器构件(如`Container`)和成员构件(如`Checkbox`、`Button`和`TextComponent`等),其结构如下图所示 - -![AWT组合模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200620111836722-1427412672.png) - -`Component`类是抽象构件,`Checkbox`、`Button`和`TextComponent`是叶子构件,而`Container`是容器构件,在`AWT`中包含的叶子构件还有很多。在一个容器构件中可以包含叶子构件,也可以继续包含容器构件,这些叶子构件和容器构件一起组成了复杂的`GUI`界面。除此以外,在`XML解析`、`组织结构树处理`、`文件系统设计`等领域,组合模式都得到了广泛应用。 - -### 模式在开源项目中的应用 - -`Spring`中`org.springframework.web.method.support.HandlerMethodArgumentResolver`使用了安全组合模式。提取关键代码如下: -```java -public interface HandlerMethodArgumentResolver { - - boolean supportsParameter(MethodParameter parameter); - - Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; - -} -``` -再看下它的一个实现类`org.springframework.web.method.support.HandlerMethodArgumentResolverComposite` -```java -public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { - - - private final List argumentResolvers = new LinkedList<>(); - - /** - * Add the given {@link HandlerMethodArgumentResolver}. - */ - public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) { - this.argumentResolvers.add(resolver); - return this; - } - - /** - * Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. - */ - public HandlerMethodArgumentResolverComposite addResolvers( - @Nullable HandlerMethodArgumentResolver... resolvers) { - - if (resolvers != null) { - Collections.addAll(this.argumentResolvers, resolvers); - } - return this; - } - - /** - * Clear the list of configured resolvers. - */ - public void clear() { - this.argumentResolvers.clear(); - } - - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return getArgumentResolver(parameter) != null; - } - - - @Override - public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { - - HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); - if (resolver == null) { - throw new IllegalArgumentException("Unsupported parameter type [" + - parameter.getParameterType().getName() + "]. supportsParameter should be called first."); - } - return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); - } -} -``` - -## 模式总结 - -### 主要优点 - -1. 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。 - -2. 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。 - -3. 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。 - -### 适用场景 - -(1) 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。 - -(2) 在一个使用面向对象语言开发的系统中需要处理一个树形结构。 - -(3) 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。 \ No newline at end of file diff --git a/DesignPattern/decorator-pattern.md b/DesignPattern/decorator-pattern.md deleted file mode 100644 index 9873168..0000000 --- a/DesignPattern/decorator-pattern.md +++ /dev/null @@ -1,233 +0,0 @@ -# 装饰模式(Decorator Pattern)——扩展系统功能 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -对新房进行装修并没有改变房屋用于居住的本质,但它可以让房子变得更漂亮、更温馨、更实用、更能满足居家的需求。在软件设计中,我们也有一种类似新房装修的技术可以对已有对象(新房)的功能进行扩展(装修),以获得更加符合用户需求的对象,`使得对象具有更加强大的功能`。这种技术对应于一种被称之为`装饰模式`的设计模式。 - -`装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为`,在现实生活中,这种情况也到处存在,例如一张照片,我们可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。 - -### 模式定义 - -如何让系统中的类可以进行扩展但是又不会导致类数目的急剧增加?`根据“合成复用原则”,在实现功能复用时,我们要多用关联,少用继承。` - -`装饰模式是一种用于替代继承的技术`,它通过一种无须定义子类的方式来给对象`动态`增加职责,使用对象之间的`关联关系`取代类之间的`继承关系`。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。 - -> `装饰模式(Decorator Pattern)`:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。 - -### 模式结构图 - -在装饰模式中,为了让系统具有更好的灵活性和可扩展性,通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类,装饰模式结构图如下所示: - -![装饰模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200621104327217-499925517.png) - -在装饰模式结构图中包含如下几个角色: - -- `Component`(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。 - -- `ConcreteComponent`(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。 - -- `Decorator`(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。 - -- `ConcreteDecorator`(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。 - -由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。 - -### 模式伪代码 - -装饰模式的核心在于`抽象装饰类`的设计,其典型代码如下所示: -```java -public interface Component { - void operation(); -} - -public class Decorator implements Component { - // 维持一个对抽象构件对象的引用 - private Component component; - - public Decorator(Component component) { - this.component = component; - } - - @Override - public void operation() { - component.operation(); - } -} -``` -值得注意的是: -- 在`Decorator`中并未真正实现`operation()`方法,而只是调用原有`component`对象的`operation()`方法,它没有真正实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。 - -- 由于在抽象装饰类`Decorator`中注入的是`Component`类型的对象,因此可以将一个具体构件对象注入其中,再通过具体装饰类来进行装饰;此外,我们还可以将一个已经装饰过的`Decorator`子类的对象再注入其中进行`多次装饰`,从而对原有功能的多次扩展。 - -在`Decorator`的子类即具体装饰类中将继承`operation()`方法并根据需要进行扩展,典型的具体装饰类代码如下: -```java -public class ConcreteDecorator extends Decorator { - - public ConcreteDecorator(Component component) { - super(component); - } - - @Override - public void operation() { - // 调用原有业务方法 - super.operation(); - // 调用新增业务方法 - addedBehavior(); - } - - // 新增业务方法 - public void addedBehavior() { - - } -} -``` -具体装饰类中可以调用到抽象装饰类的`operation()`方法,同时可以定义新的业务方法,如`addedBehavior()` - -## 透明装饰模式 vs 半透明装饰模式 - -上面介绍的装饰模式就是`透明(Transparent)装饰模式`,也即标准装饰模式。 - -透明装饰模式,客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。对于客户端而言,具体构件对象和具体装饰对象没有任何区别。也就是应该使用如下代码: -```java -// 透明装饰模式应当使用抽象构件类型定义对象 -Component c, c1; -c = new ConcreteComponent(); -c1 = new ConcreteDecorator(c); - -// **注意:透明装饰模式不应该使用具体类型来声明对象,比如下面的代码** -// ConcreteComponent c = new ConcreteComponent(); -// ConcreteDecorator c1 = new ConcreteDecorator(c); -``` - -但是在实际使用过程中,由于新增行为可能需要单独调用(即`客户端想自己单独调用装饰类中新增的方法来控制是否以及如何增强`),因此这种形式的装饰模式也经常出现,这种装饰模式被称为`半透明(Semi-transparent)装饰模式` - -`半透明装饰模式`中的具体装饰类对应的实现,即变为如下: -```java -public class ConcreteDecorator extends Decorator { - - public ConcreteDecorator(Component component) { - super(component); - } - - //@Override - //public void operation() { - // 调用原有业务方法 - // super.operation(); - // 调用新增业务方法 - //addedBehavior(); - //} - - // 新增业务方法 - public void addedBehavior() { - - } -} -``` -**注意 `半透明装饰模式`中的`ConcreteDecorator`类继承了父装饰类`Decorator`的`operation()`方法但并没有重写`operation()`方法,并新增了业务方法`addedBehavior()`,但这两个方法是完全独立的,没有任何调用关系。** - -即 `半透明装饰模式`中的`ConcreteDecorator`中的`operation()`并没有对注入的`Component`进行增强,只是增加了额外的方法`addedBehavior()`,这样一来是否增强`Component`就取决于客户端是否调用`addedBehavior()`方法。 - -客户端想增强`Component`,就需要分别调用这两个方法,代码片段如下: -```java -// 原有构件 -Component component = new ConcreteComponent(); - -// 用装饰器包装原有构件 -ConcreteDecorator enhancedComponent = new ConcreteDecorator(component); - -// 调用原有业务方法 -enhancedComponent.operation(); -// 调用新增业务方法,从而实现增强 -enhancedComponent.addedBehavior(); -``` - -显然`半透明装饰模式`最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象。 - -## 模式应用 - -### 模式在JDK中的应用 - -`Java`中的`java.io`包下io流的设计就充分使用了装饰模式。举个具体的例子,`BufferedInputStream`就是一个具体装饰者,它能为一个原本没有缓冲(`buffer`)功能的`InputStream`增加缓冲的功能。下面的代码应该司空见惯。 -```java -BufferedInputStream reader = new BufferedInputStream(new FileInputStream(new File("/tmp/1.txt"))); -``` -`FileInputStream`本没有缓冲功能,每次调用`read`方法,都会发起系统调用读数据。用`BufferedInputStream`来装饰它,那么每次调用`read`方法,会向操作系统多读一定量数据进入内存的`buf[]`,这样就提高了读的效率,避免频繁发起系统调用。 - -`BufferedInputStream`构造器中注入了`InputStream` -```java -public BufferedInputStream(InputStream in, int size) { - super(in); - if (size <= 0) { - throw new IllegalArgumentException("Buffer size <= 0"); - } - buf = new byte[size]; -} -``` -`BufferedInputStream`继承了父装饰器`FilterInputStream`,维持了对`InputStream`的引用 -```java -public class FilterInputStream extends InputStream { - - protected volatile InputStream in; - - protected FilterInputStream(InputStream in) { - this.in = in; - } -} -``` -`BufferedInputStream`重写了`read()`从而实现对引用的`InputStream`增强。有兴趣可以读读相关源码。 - -### 模式在开源项目中的应用 - -`mybatis`中`org.apache.ibatis.session.Configuration#newExecutor`有这样一段代码 -```java -public Executor newExecutor(Transaction transaction, ExecutorType executorType) { - executorType = executorType == null ? defaultExecutorType : executorType; - executorType = executorType == null ? ExecutorType.SIMPLE : executorType; - Executor executor; - if (ExecutorType.BATCH == executorType) { - executor = new BatchExecutor(this, transaction); - } else if (ExecutorType.REUSE == executorType) { - executor = new ReuseExecutor(this, transaction); - } else { - executor = new SimpleExecutor(this, transaction); - } - if (cacheEnabled) { - executor = new CachingExecutor(executor); - } - executor = (Executor) interceptorChain.pluginAll(executor); - return executor; -} -``` -上面的代码可以看到,如果开启了二级缓存则装饰原先的`Executor`,其实`org.apache.ibatis.executor.CachingExecutor`就是一个装饰器 -```java -public class CachingExecutor implements Executor { - - private final Executor delegate; - private final TransactionalCacheManager tcm = new TransactionalCacheManager(); - - public CachingExecutor(Executor delegate) { - this.delegate = delegate; - delegate.setExecutorWrapper(this); - } -} -``` - -## 模式总结 - -装饰模式降低了系统的耦合度,可以动态增强对象的功能。 - -### 主要优点 - -(1) 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。 - -(2) 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。 - -(3) 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。 - -### 适用场景 - -(1) 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。 - -(2) 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如`Java`语言中的`final`类)。 \ No newline at end of file diff --git a/DesignPattern/facade-pattern.md b/DesignPattern/facade-pattern.md deleted file mode 100644 index 2f6b09d..0000000 --- a/DesignPattern/facade-pattern.md +++ /dev/null @@ -1,153 +0,0 @@ -# 外观模式(Facade Pattern)——提供统一的入口 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -绝大多数`B/S`系统都有一个首页或者导航页面,大部分`C/S`系统都提供了菜单或者工具栏,在这里,首页和导航页面就充当了B/S系统的外观角色,而菜单和工具栏充当了C/S系统的外观角色,通过它们用户可以快速访问子系统,增强了软件的易用性。 - -在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现,由于涉及到的类比较多,导致使用时代码较为复杂,此时,特别需要一个类似服务员一样的角色,由它来负责和多个业务类进行交互,而客户类只需与该类交互。`外观模式`通过引入一个外观角色(`Facade`)来简化客户端与子系统(`Subsystem`)之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,使得客户端调用非常方便。 - -### 模式定义 - -外观模式中,一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。 - -> 外观模式(`Facade Pattern`):为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用 - -外观模式又称为门面模式,它是一种对象结构型模式。外观模式是`迪米特法则`的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。 - -### 模式结构图 - -外观模式没有一个一般化的类图描述,下图所示的类图也可以作为描述外观模式的结构图: - -![外观模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200625114616509-1602626714.png) - -外观模式包含如下两个角色: - -- Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。 - -- SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。 - -### 模式伪代码 - -外观模式中所指的`子系统`是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。子系统类通常是一些业务类,实现了一些具体的、独立的业务功能,其典型代码如下: - -```java -public class SubSystemA { - - public void methodA() { - //业务实现代码 - } -} - -public class SubSystemB { - - public void methodB() { - //业务实现代码 - } -} - -public class SubSystemC { - - public void methodC() { - //业务实现代码 - } -} -``` - -引入外观类,与子系统业务类之间的交互统一由外观类来完成 -```java -public class Facade { - private SubSystemA obj1 = new SubSystemA(); - private SubSystemB obj2 = new SubSystemB(); - private SubSystemC obj3 = new SubSystemC(); - - public void method() { - obj1.methodA(); - obj2.methodB(); - obj3.methodC(); - } -} -``` - -由于在外观类中维持了对子系统对象的引用,客户端可以通过外观类来间接调用子系统对象的业务方法,而无须与子系统对象直接交互。引入外观类后,客户端代码变得非常简单,典型代码如下: -```java -public static void main(String[] args) { - Facade facade = new Facade(); - facade.method(); -} -``` - -## 模式改进 - -在标准的外观模式中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则,因此可以通过引入抽象外观类来对系统进行改进,在一定程度上可以解决该问题。在引入抽象外观类之后,客户端可以针对抽象外观类进行编程,对于新的业务需求,不需要修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象。 - -定义抽象外观类 -```java -public abstract class AbstractFacade { - public abstract void method(); -} -``` -根据具体的场景,实现具体的外观类 -```java -public class Facade1 extends AbstractFacade { - - private SubSystemA obj1 = new SubSystemA(); - private SubSystemB obj2 = new SubSystemB(); - - @Override - public void method() { - obj1.methodA(); - obj2.methodB(); - } -} - -public class Facade2 extends AbstractFacade { - - private SubSystemB obj1 = new SubSystemB(); - private SubSystemC obj2 = new SubSystemC(); - - @Override - public void method() { - obj1.methodB(); - obj2.methodC(); - } -} -``` - -客户端针对抽象外观类进行编程,代码片段如下: -```java -public static void main(String[] args) { - AbstractFacade facade = new Facade1(); - // facade = new Facade2(); - facade.method(); -} -``` - -## 模式应用 - -个人认为外观模式某些情况下可以看成是对既有系统的再次封装,所以各种类库、工具库(比如`hutool`)、框架基本都有外观模式的影子。外观模式让调用方更加简洁,不用关心内部的实现,与此同时,也让越来越多的程序猿多了个`调包侠`的昵称(当然了这其中也包括笔者●´ω`●行无际)。 - -所以,你可能在很多开源代码中看到类似`XxxBootstrap`、`XxxContext`、`XxxMain`等类似的`Class`,再追进去看一眼,你可能发现里面关联了一大堆的复杂的对象,这些对象对于外层调用者来说几乎是透明的。 - -例子太多,以致于不知道举啥例子(实际是偷懒的借口O(∩_∩)O哈哈~)。 - -## 模式总结 - -外观模式并不给系统增加任何新功能,它仅仅是简化调用接口。在几乎所有的软件中都能够找到外观模式的应用。所有涉及到与多个业务对象交互的场景都可以考虑使用外观模式进行重构。 - -### 主要优点 - -(1) 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。 - -(2) 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。 - -(3) 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。 - -### 适用场景 - -(1) 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。 - -(2) 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。 - -(3) 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。 \ No newline at end of file diff --git a/DesignPattern/factory-pattern.md b/DesignPattern/factory-pattern.md deleted file mode 100644 index 81255ee..0000000 --- a/DesignPattern/factory-pattern.md +++ /dev/null @@ -1,239 +0,0 @@ -# 工厂模式三兄弟(Factory Pattern) - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -`工厂模式`是最常用的一类创建型设计模式,通常我们所说的工厂模式是指`工厂方法模式`,它也是使用频率最高的工厂模式。`简单工厂模式`是工厂方法模式的“小弟”,它不属于GoF23种设计模式,但在软件开发中应用也较为频繁,通常将它作为学习其他工厂模式的入门。此外,工厂方法模式还有一位“大哥”——`抽象工厂模式`。这三种工厂模式各具特色,难度也逐个加大,在软件开发中它们都得到了广泛的应用,成为面向对象软件中常用的创建对象的工具。 - -## 简单工厂模式 -简单工厂模式并不属于GoF 23个经典设计模式,但通常将它作为学习其他工厂模式的基础,它的设计思想很简单。 - -### 模式定义 - -> `简单工厂模式(Simple Factory Pattern)`:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。 - -简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。 - -### 模式结构图 - -简单工厂模式结构图如下所示: - -![简单工厂模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200610222117573-1259462893.png) - -### 模式伪代码 - -在使用`简单工厂模式`时,首先需要对`产品类`进行重构,不能设计一个包罗万象的产品类,而需根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现,典型的抽象产品类代码如下所示: -```java -public abstract class Product { - // 所有产品的公共属性 - - // 所有产品类的公共业务方法 - public void methodSame() { - //公共方法的实现 - } - - // 声明抽象业务方法 - public abstract void methodDiff(); -} -``` - -在`具体产品类`中实现了抽象产品类中声明的抽象业务方法。 -```java -public class ConcreteProduct extends Product { - @Override - public void methodDiff() { - // 具体产品业务方法的实现 - } -} -``` - -简单工厂模式的核心是`工厂类`,在没有工厂类之前,客户端一般会使用`new`关键字来直接创建产品对象,而在引入工厂类之后,客户端可以通过工厂类来创建产品,在简单工厂模式中,工厂类提供了一个静态工厂方法供客户端使用,根据所传入的参数不同可以创建不同的产品对象,典型的工厂类代码如下所示: -```java -public class Factory { - //静态工厂方法 - public static Product getProduct(String arg) { - Product product = null; - if (arg.equalsIgnoreCase("A")) { - product = new ConcreteProductA(); - //初始化设置product - } else if (arg.equalsIgnoreCase("B")) { - product = new ConcreteProductB(); - //初始化设置product - } - return product; - } -} -``` -在`客户端`代码中,我们通过调用工厂类的工厂方法即可得到产品对象,典型代码如下所示: -```java -public class Client { - public static void main(String[] args) { - Product product; - product = Factory.getProduct("A"); //通过工厂类创建产品对象 - product.methodSame(); - product.methodDiff(); - } -} -``` - -### 模式简化 -有时候,为了简化简单工厂模式,我们可以将`抽象产品类`和`工厂类`合并,将静态工厂方法移至抽象产品类中,如下图所示。 - -![简化的简单工厂模式](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200614180924146-772071609.png) - -客户端可以通过产品父类的静态工厂方法,根据参数的不同创建不同类型的产品子类对象,这种做法在JDK等类库和框架中也广泛存在。 -比如:`java.nio.charset.Charset` -```java -public abstract class Charset { - - /** - * Returns a charset object for the named charset. - */ - public static Charset forName(String charsetName) { - java.nio.charset.Charset cs = lookup(charsetName); - if (cs != null) - return cs; - throw new UnsupportedCharsetException(charsetName); - } -} -``` - -### 模式小结 - -简单工厂模式提供了专门的工厂类用于创建对象,将对象的创建和对象的使用分离开,它作为一种最简单的工厂模式在软件开发中得到了较为广泛的应用。 - -使用场景: -1. 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。 -2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。 - -## 工厂方法模式 - -`简单工厂模式`虽然简单,但存在一个很严重的问题。当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?工厂方法模式应运而生。 - -### 模式定义 -在`工厂方法模式`中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。工厂方法模式定义如下: - -> `工厂方法模式(Factory Method Pattern)`:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。 - -### 模式结构图 -工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。工厂方法模式结构如图所示: - -![工厂方法模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200614183211331-269191986.png) - -在工厂方法模式结构图中包含如下几个角色: -- Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。 -- ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。 -- Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。 -- ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。 - -### 模式伪代码 -与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示: -```java -public interface Factory { - Product factoryMethod(); -} -``` -在抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责,`客户端针对抽象工厂编程`,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品,其典型代码如下所示: -```java -public class ConcreteFactory implements Factory { - @Override - public Product factoryMethod() { - return new ConcreteProduct(); - } -} -``` -在客户端代码中,只需关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端类代码片段如下所示: -```java -public class Client { - public static void main(String[] args) { - // 确定是哪个工厂可得到产品 - Factory factory = new ConcreteFactory(); - // 获取产品 - Product product = factory.factoryMethod(); - } -} -``` -### 模式简化 -有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,`在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法创建产品`,直接通过工厂即可使用所创建的对象中的业务方法。 -```java -// 改为抽象类 -public class AbstractFactory { - // 在工厂类中直接调用产品类的业务方法 - public void productMethod() { - Product product = this.createProduct(); - product.method(); - } - - public abstract Product createProduct(); -} -``` -通过将业务方法的调用移入工厂类,可以直接使用工厂对象来调用产品对象的业务方法,客户端无须直接调用工厂方法,在客户端并不关心Product细节的情况下使用这种设计方案会更加方便。 -### 模式小结 - -工厂方法模式能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。基于`工厂角色`和`产品角色`的多态性设计是工厂方法模式的关键。 - -## 抽象工厂模式 - -`工厂方法模式`通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个`产品族`,由同一个工厂来统一生产,这就是我们本文将要学习的抽象工厂模式的基本思想。 - -这里我斗胆举个例子来说明一下吧,如果不恰当欢迎指出。 - -众所周知,国内知名的电器厂有海尔、海信(姑且就认为是2个),电器厂会生产电视机、电冰箱、空调(姑且就认为是3种产品)。 -- 使用工厂方法模式:工厂方法模式中每个工厂只生产一类产品,那么就必须要有`海尔电视机厂`、`海尔电冰箱厂`、`海尔空调厂`、`海信电视机厂`、`海信电冰箱厂`、`海信空调厂` -- 使用抽象工厂模式:抽象工厂中每个工厂生产由多种产品组成的"产品族",那么就只需要有`海尔工厂`、`海信工厂`就够了,每个工厂可生产自家的电视机、电冰箱、空调。 - -由此看出使用`抽象工厂模式`极大地减少了系统中类的个数。 - -### 模式定义 - -抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。抽象工厂模式定义如下: - -> 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。 - -### 模式结构图 -在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族,抽象工厂模式结构如图所示: - -![抽象工厂模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200614194007106-1386712790.png) - -在抽象工厂模式结构图中包含如下几个角色: -- AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。 -- ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。 -- AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。 -- ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。 - -### 模式伪代码 - -在抽象工厂中声明了多个工厂方法,用于创建不同类型的产品,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示: -```java -public abstract class AbstractFactory { - - public abstract AbstractProductA createProductA(); - - public abstract AbstractProductB createProductB(); - - public abstract AbstractProductC createProductC(); -} -``` - -具体工厂实现了抽象工厂,每一个具体的工厂方法可以返回一个特定的产品对象,而同一个具体工厂所创建的产品对象构成了一个产品族。对于每一个具体工厂类,其典型代码如下所示: -```java -public class ConcreteFactory1 extends AbstractFactory { - @Override - public AbstractProductA createProductA() { - return new ConcreteProductA1(); - } - - @Override - public AbstractProductB createProductB() { - return new ConcreteProductB1(); - } - - @Override - public AbstractProductC createProductC() { - return new ConcreteProductC1(); - } -} -``` - -### 模式小结 - -如果一开始就学习`抽象工厂模式`估计很难理解为什么这样设计,按次序学习分析`简单工厂模式`、`工厂方法模式`、`抽象工厂模式`基本就顺理成章了。实际开发中,可能并不是照搬照套工厂模式三兄弟的伪代码,大多会简化其中的部分实现。本来学习设计模式就是重思想,学习如何用抽象类、接口、拆分、组合等将软件解耦合,并增强系统可扩展性,这才是最关键的。 \ No newline at end of file diff --git a/DesignPattern/flyweight-pattern.md b/DesignPattern/flyweight-pattern.md deleted file mode 100644 index b306522..0000000 --- a/DesignPattern/flyweight-pattern.md +++ /dev/null @@ -1,155 +0,0 @@ -# 享元模式(Flyweight Pattern)——实现对象的复用 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -当一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等问题。例如在一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间,那么我们如何去避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作?享元模式正为解决这一类问题而诞生。享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象,这个对象可以出现在一个字符串的不同地方,相同的字符对象都指向同一个实例,在享元模式中,存储这些共享实例对象的地方称为享元池(`Flyweight Pool`)。我们可以针对每一个不同的字符创建一个享元对象,将其放在享元池中,需要时再从享元池取出。如下图所示: - -![字符享元对象示意图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200626163522301-282076962.png) - -### 模式定义 - -享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(`Intrinsic State`)和外部状态(`Extrinsic State`)。下面将对享元的内部状态和外部状态进行简单的介绍: - -- 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。如字符的内容,不会随外部环境的变化而变化,无论在任何环境下字符“a”始终是“a”,都不会变成“b”。 - -- 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。如字符的颜色,可以在不同的地方有不同的颜色,例如有的“a”是红色的,有的“a”是绿色的,字符的大小也是如此,有的“a”是五号字,有的“a”是四号字。而且字符的颜色和大小是两个独立的外部状态,它们可以独立变化,相互之间没有影响,客户端可以在使用时将外部状态注入享元对象中。 - -正因为区分了`内部状态`和`外部状态`,我们可以将具有相同内部状态的对象存储在`享元池`中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。 - -> 享元模式(`Flyweight Pattern`):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。 - -### 模式结构图 - -享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类,其结构图如下图所示: - -![享元模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200626164350988-1867807125.png) - -在享元模式结构图中包含如下几个角色: -- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。 - -- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。 - -- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。 - -- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。 - -### 模式伪代码 - -在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。典型的享元工厂类的代码如下: -```java -public class FlyweightFactory { - //定义一个HashMap用于存储享元对象,实现享元池 - private HashMap flyweights = new HashMap(); - - public Flyweight getFlyweight(String key) { - //如果对象存在,则直接从享元池获取 - if (flyweights.containsKey(key)) { - return (Flyweight) flyweights.get(key); - } else {//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回 - Flyweight fw = new ConcreteFlyweight(); - flyweights.put(key, fw); - return fw; - } - } -} -``` - -享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量(`通常占较大内存,否则也不必使用享元模式了`,这里用`Object`表示),而外部状态通过方法参数传入控制行为。典型的享元类代码如下所示: -```java -public class Flyweight { - // 内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的 - protected Object intrinsicState; - - public void setIntrinsicState(Object intrinsicState) { - this.intrinsicState = intrinsicState; - } - - // 外部状态extrinsicState在使用时由外部设置,不保存在享元对象中 - // 即使是同一个对象,在每一次调用时也可以传入不同的外部状态 - public void operation(Object extrinsicState) { - // 结合内部状态intrinsicState以及方法参数传入的extrinsicState完成具体逻辑 - } -} - -// 具体享元类 -public class ConcreteFlyweight extends Flyweight { - @Override - public void operation(Object extrinsicState) { - // 结合内部状态intrinsicState以及方法参数传入的extrinsicState完成具体逻辑 - } -} -``` -## 模式应用 - -### 模式在JDK中的应用 - -`java`中的`Integer`就用到了享元池,缓存了一定量的对象。 -``` -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -``` -再看下`IntegerCache`的实现 -```java -private static class IntegerCache { - static final int low = -128; - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - try { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } catch( NumberFormatException nfe) { - // If the property cannot be parsed into an int, ignore it. - } - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - - // range [-128, 127] must be interned (JLS7 5.1.7) - assert IntegerCache.high >= 127; - } - - private IntegerCache() {} -} -``` - -## 模式总结 - -当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。 - -### 主要优点 - -(1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。 - -(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。 - -### 主要缺点 - -(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,使得程序的逻辑复杂化,增加了维护外部状态的成本。 - -(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。 - -### 适用场景 - -(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。 - -(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。 - -(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。 \ No newline at end of file diff --git a/DesignPattern/interpreter-pattern.md b/DesignPattern/interpreter-pattern.md deleted file mode 100644 index 5709493..0000000 --- a/DesignPattern/interpreter-pattern.md +++ /dev/null @@ -1,145 +0,0 @@ -# 解释器模式(Interpreter Pattern)——自定义语言的实现 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。 - -在某些情况下,为了更好地描述某一些特定类型的问题,我们可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。此时,可以使用解释器模式来设计这种新的语言。对解释器模式的学习能够加深我们对面向对象思想的理解,并且掌握编程语言中文法规则的解释过程。 - -### 模式定义 - -解释器模式定义如下: - -> 解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。 - -### 模式结构图 - -由于表达式可分为终结符表达式和非终结符表达式,因此解释器模式的结构与组合模式的结构有些类似,但在解释器模式中包含更多的组成元素,它的结构如下图所示: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211110151111558-1489644000.png) - -在解释器模式结构图中包含如下几个角色: - -- AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。 -- TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。 -- NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。 -- Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。 - -### 模式伪代码 - -通常在解释器模式中提供了一个环境类`Context`,用于存储一些全局信息,通常在`Context`中包含了一个`HashMap`或`ArrayList`等类型的集合对象,存储一系列公共信息,用于在进行具体的解释操作时从中获取相关信息。其典型代码片段如下: - -```java -import java.util.HashMap; -import java.util.Map; - -public class Context { - - private Map map = new HashMap<>(); - - public void assign(String key, Object value) { - // 往环境类中设值 - } - - public Object lookup(String key) { - // 获取存储在环境类中的值 - return null; - } -} -``` - -在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。 - -对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类,其典型代码如下: - -```java -public abstract class AbstractExpression { - - abstract void interpret(Context ctx); - -} -``` - -终结符表达式和非终结符表达式类都是抽象表达式类的子类,对于终结符表达式,其代码很简单,主要是对终结符元素的处理,其典型代码如下所示: - -```java -public class TerminalExpression extends AbstractExpression { - @Override - void interpret(Context ctx) { - // 终结符表达式的解释操作 - } -} -``` - -对于非终结符表达式,其代码相对比较复杂,因为可以通过非终结符将表达式组合成更加复杂的结构,对于包含两个操作元素的非终结符表达式类,其典型代码如下: - -```java -public class NonterminalExpression extends AbstractExpression { - - private AbstractExpression left; - private AbstractExpression right; - - @Override - void interpret(Context ctx) { - // 递归调用每一个组成部分的interpret()方法 - // 在递归调用时指定组成部分的连接方式,即非终结符的功能 - } -} -``` - -## 模式实例 - -没有具体的例子总会让人感到抽象、难以理解。下面通过三个例子来体会解释器模式的应用。 - -### 加减法解释器 - -> 需求: 当输入字符串表达式为“1+2+3–4+1”时,将输出计算结果为3。 - -先来学习如何表示一个语言的文法规则以及如何构造一棵抽象语法树。 - -每一个输入表达式,例如“1+2+3–4+1”,都包含了三个语言单位,可以使用如下文法规则来定义: - -```text -expression ::= value | operation - -operation ::= expression '+' expression | expression '-' expression - -value ::= an integer //一个整数值 -``` - -其中`|`表示或,如文法规则`boolValue ::= 0 | 1`表示终结符表达式`boolValue`的取值可以为`0`或者`1`。 - - -上面文法规则包含三条语句,第一条表示表达式的组成方式,其中`value`和`operation`是后面两个语言单位的定义,每一条语句所定义的字符串如`operation`和`value`称为语言构造成分或语言单位,符号`::=`表示`定义为`的意思,其左边的语言单位通过右边来进行说明和定义,语言单位对应终结符表达式和非终结符表达式。 - -- 本规则中的`operation`是非终结符表达式,它的组成元素仍然可以是表达式,可以进一步分解; -- `value`是终结符表达式,它的组成元素是最基本的语言单位,不能再进行分解。 - -除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为抽象语法树(`Abstract Syntax Tree, AST`)的图形方式来直观地表示语言的构成,每一棵抽象语法树对应一个语言实例,如加法/减法表达式语言中的语句“1+2+3–4+1”,可以通过如下图所示抽象语法树来表示: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211110164752865-1754398807.png) - -在该抽象语法树中,可以通过终结符表达式`value`和非终结符表达式`operation`组成复杂的语句,每个文法规则的语言实例都可以表示为一个抽象语法树,即每一条具体的语句都可以用类似上图所示的抽象语法树来表示。 - -## 模式总结 - -解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。 - -虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。 - -### 主要优点 - -- 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。 -- 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。 - -### 主要缺点 - -- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。 -- 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。 - -### 适用场景 - -- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树 -- 一个语言的文法较为简单 diff --git a/DesignPattern/iterator-pattern.md b/DesignPattern/iterator-pattern.md deleted file mode 100644 index 74203ab..0000000 --- a/DesignPattern/iterator-pattern.md +++ /dev/null @@ -1,201 +0,0 @@ -# 迭代器模式(Iterator Pattern)——遍历聚合对象中的元素 - - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213`。 - -## 模式概述 - -### 模式定义 - -在软件开发中,经常需要使用聚合对象来存储一系列数据。聚合对象有两个职责: -1. 存储数据 -2. 遍历数据 - -从依赖性来看,前者是聚合对象的基本职责,而后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为`迭代器`的对象中,由`迭代器`来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合单一职责的要求。 - -> 迭代器模式(`Iterator Pattern`): 提供一种方法来访问聚合对象,而不用暴露这个对象的内部存储。迭代器模式是一种对象行为型模式。 - -### 模式结构图 -在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了`工厂方法`模式,其模式结构如图所示。 - -![迭代器模式结构图](https://img2020.cnblogs.com/blog/1546632/202004/1546632-20200419102650275-1791408314.png) - -在迭代器模式结构图中包含如下几个角色: -- `Iterator`(`抽象迭代器`):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的`first()`方法,用于访问下一个元素的`next()`方法,用于判断是否还有下一个元素的`hasNext()`方法,用于获取当前元素的`currentItem()`方法等,在具体迭代器中将实现这些方法。 -- `ConcreteIterator`(`具体迭代器`):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。 -- `Aggregate`(`抽象聚合类`):它用于存储和管理元素对象,声明一个`createIterator()`方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。 -- `ConcreteAggregate`(`具体聚合类`):它实现了在抽象聚合类中声明的`createIterator()`方法,该方法返回一个与该具体聚合类对应的具体迭代器`ConcreteIterator`实例。 - -### 模式伪代码 - -在`抽象迭代器`中声明了用于遍历聚合对象中所存储元素的方法 -```java -interface Iterator { - public void first(); //将游标指向第一个元素 - public void next(); //将游标指向下一个元素 - public boolean hasNext(); //判断是否存在下一个元素 - public Object currentItem(); //获取游标指向的当前元素 -} -``` -在`具体迭代器`中将实现`抽象迭代器`声明的遍历数据的方法 -```java -class ConcreteIterator implements Iterator { - private ConcreteAggregate objects; //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据 - private int cursor; //定义一个游标,用于记录当前访问位置 - public ConcreteIterator(ConcreteAggregate objects) { - this.objects=objects; - } - - public void first() { ...... } - - public void next() { ...... } - - public boolean hasNext() { ...... } - - public Object currentItem() { ...... } -} -``` -`聚合类`用于存储数据并`负责创建迭代器对象`,最简单的`抽象聚合类`代码如下所示 -```java -interface Aggregate { - Iterator createIterator(); -} -``` -具体聚合类作为抽象聚合类的子类,一方面负责存储数据,另一方面实现了在抽象聚合类中声明的工厂方法`createIterator()`,用于返回一个与该具体聚合类对应的具体迭代器对象,代码如下所示 -```java -class ConcreteAggregate implements Aggregate { - //...... - public Iterator createIterator() { - return new ConcreteIterator(this); - } - //...... -} -``` - -### 模式改进 -在迭代器模式结构图中,我们可以看到具体`迭代器类`和具体`聚合类`之间存在双重关系,其中一个关系为`关联关系`,在具体`迭代器`中需要维持一个对具体聚合对象的引用,该关联关系的目的是`访问存储在聚合对象中的数据`,以便迭代器能够对这些数据进行遍历操作。 - -除了使用`关联关系`外,为了能够让迭代器可以访问到聚合对象中的数据,我们还可以将迭代器类设计为聚合类的`内部类`,JDK中的迭代器类就是通过这种方法来实现的,如下`AbstractList`类代码片段所示 -```java -package java.util; - -public abstract class AbstractList extends AbstractCollection implements List { - //... - public boolean add(E e) {...} - abstract public E get(int index); - public E set(int index, E element) { - throw new UnsupportedOperationException(); - } - //... - - public Iterator iterator() { - return new Itr(); - } - - // 这里用内部类可直接访问到聚合对象中的数据 - private class Itr implements Iterator { - - int cursor = 0; - int lastRet = -1; - - int expectedModCount = modCount; - - public boolean hasNext() { - return cursor != size(); - } - - public E next() { - //... - } - - public void remove() { - //... - } - } -} -``` - - -## 模式应用 - -### 模式在JDK中的应用 -在JDK中,`Collection`接口和`Iterator`接口充当了迭代器模式的抽象层,分别对应于`抽象聚合类`和`抽象迭代器`,而`Collection`接口的子类充当了`具体聚合类`,如图列出了JDK中部分与List有关的类及它们之间的关系。 - -![Java集合框架中部分类结构图](https://img2020.cnblogs.com/blog/1546632/202004/1546632-20200419111205586-566474649.png) - -在JDK中,实际情况比上图要复杂很多,`List`接口除了继承`Collection`接口的`iterator()`方法外,还增加了新的工厂方法`listIterator()`,专门用于创建`ListIterator`类型的迭代器,在`List`的子类`LinkedList`中实现了该方法,可用于创建具体的`ListIterator`子类`ListItr`的对象。 - -既然有了`iterator()`方法,为什么还要提供一个`listIterator()`方法呢?这两个方法的功能不会存在重复吗?干嘛要多此一举?由于在`Iterator`接口中定义的方法太少,只有三个,通过这三个方法只能实现`正向遍历`,而有时候我们需要对一个聚合对象进行`逆向遍历`等操作,因此在JDK的`ListIterator`接口中声明了用于逆向遍历的`hasPrevious()`和`previous()`等方法,如果客户端需要调用这两个方法来实现逆向遍历,就不能再使用`iterator()`方法来创建迭代器了,因为此时创建的迭代器对象是不具有这两个方法的。具体可细读`java.util.ListIterator` - -### 模式在开源项目中的应用 - -开源项目中用到遍历的地方,很多地方都会用到迭代器设计模式。这里只提一个`org.apache.kafka.clients.consumer.ConsumerRecords`。详情可以找`kafka client`源码阅读。 -```java -public class ConsumerRecords implements Iterable> { - @Override - public Iterator> iterator() { - return new ConcatenatedIterable<>(records.values()).iterator(); - } - - private static class ConcatenatedIterable implements Iterable> { - - private final Iterable>> iterables; - - public ConcatenatedIterable(Iterable>> iterables) { - this.iterables = iterables; - } - - @Override - public Iterator> iterator() { - return new AbstractIterator>() { - Iterator>> iters = iterables.iterator(); - Iterator> current; - - public ConsumerRecord makeNext() { - while (current == null || !current.hasNext()) { - if (iters.hasNext()) - current = iters.next().iterator(); - else - return allDone(); - } - return current.next(); - } - }; - } - } -} -``` - -## 模式总结 - -迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。 - -1. *主要优点* - -迭代器模式的主要优点如下: - -(1) 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。 - -(2) 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。 - -(3) 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足`开闭原则`的要求。 - -(4) 不管实现如何变化,都可以使用`Iterator`,引入`Iterator`后可以将遍历与实现分离开来。对于遍历者来说我们可能只会用到`hasNext()`和`next()`方法,如果底层数据存储结构变了(举个例子原来用`List`存储,需求变动后改为用`Map`存储),对于上层调用者来说可能完全是透明的,遍历者并不关心你具体如何存储。 - -2. *主要缺点* - -迭代器模式的主要缺点如下: - -(1) 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。 - -(2) 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器`Iterator`就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类`ListIterator`等来实现,而`ListIterator`迭代器无法用于操作`Set`类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。 - -3. *适用场景* - -在以下情况下可以考虑使用迭代器模式: - -(1) 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。 - -(2) 需要为一个聚合对象提供多种遍历方式。 - -(3) 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。 diff --git a/DesignPattern/mediator-pattern.md b/DesignPattern/mediator-pattern.md deleted file mode 100644 index 14803eb..0000000 --- a/DesignPattern/mediator-pattern.md +++ /dev/null @@ -1,132 +0,0 @@ -# 中介者模式(Mediator Pattern)——协调多个对象之间的交互 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -很多在一线城市漂泊的朋友或多或少都会遇到租房的难题,你是怎样找到物美价廉的房子的呢,可以在评论区分享经验哦。相信大多数小伙伴是通过`中介`找房子的,实话说,通过中介,只要说出你的预算以及大致需求(比如`单间带独卫`、`朝南大卧室带阳台`等),中介会快速提供符合你情况房源。这里可以看出,`中介者`协调了房东与租客之间错综复杂的关系,将一个网状的关系结构变成一个以中介者为中心的星形结构,让多对多的关系更容易维护。 - -未引入中介者,对象之间(这些对象称为`同事对象`,它们之间通过彼此的相互作用实现系统的行为)的关系图如下所示: - -![对象之间存在复杂关系的网状结构](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200625220550529-1223591117.png) - -引入中介者可以使对象之间的关系数量急剧减少。在这个星形结构中,对象不再直接与另一个对象联系,它通过中介者对象与另一个对象发生相互作用。 - -![引入中介者对象的星型结构](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200625221110744-82013038.png) - -### 模式定义 - -如果在一个系统中对象之间存在多对多的相互关系,我们可以将对象之间的一些交互行为从各个对象中分离出来,并集中封装在一个中介者对象中,并由该中介者进行统一协调,这样对象之间多对多的复杂关系就转化为相对简单的一对多关系。通过引入中介者来简化对象之间的复杂交互,中介者模式是“迪米特法则”的一个典型应用。 - -> 中介者模式(`Mediator Pattern`):用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。 - -### 模式结构图 - -在中介者模式中,引入了用于协调其他对象/类之间相互调用的中介者类,为了让系统具有更好的灵活性和可扩展性,通常还提供了抽象中介者,其结构图如下图所示: - -![中介者模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200625221449414-1079230543.png) - -在中介者模式结构图中包含如下几个角色: - -- Mediator(抽象中介者):它定义一个接口,该接口用于与各同事对象之间进行通信。 - -- ConcreteMediator(具体中介者):它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,它维持了对各个同事对象的引用。 - -- Colleague(抽象同事类):它定义各个同事类公有的方法,并声明了一些抽象方法来供子类实现,同时它维持了一个对抽象中介者类的引用,其子类可以通过该引用来与中介者通信。 - -- ConcreteColleague(具体同事类):它是抽象同事类的子类;每一个同事对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中声明的抽象方法。 - - -中介者模式的核心在于中介者类的引入,在中介者模式中,中介者类承担了两方面的职责: - -(1) 中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,可通过中介者来实现间接调用。该中转作用属于中介者在结构上的支持。 - -(2) 协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致的和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。 - -### 模式伪代码 - -典型的抽象中介者类、抽象同事类典型代码如下: -```java -// 抽象中介者类 -public abstract class Mediator { - // 用于存储同事对象 - protected List colleagues = new ArrayList<>(); - - // 注册方法,用于增加同事对象 - public void register(Colleague colleague) { - colleagues.add(colleague); - } - - // 声明抽象的业务方法 - public abstract void operation(); -} - -// 抽象同事类 -public abstract class Colleague { - // 维持一个抽象中介者的引用 - protected Mediator mediator; - - public Colleague(Mediator mediator) { - this.mediator = mediator; - } - - // 声明自身方法,处理自己的行为 - public abstract void method1(); - - // 定义依赖方法,与中介者进行通信 - public void method2() { - mediator.operation(); - } -} -``` - -具体中介者类、具体同事类实现这些抽象方法,典型代码如下: -```java -// 具体同事类 -public class ConcreteColleague extends Colleague { - - public ConcreteColleague(Mediator mediator) { - super(mediator); - } - - @Override - public void method1() { - // 实现自身方法 - } -} - -// 具体中介者类 -public class ConcreteMediator extends Mediator { - @Override - public void operation() { - // 通过调用同事类的方法,并增加其他业务逻辑来控制同事之间的交互 - } -} -``` -## 模式应用 - -待完善... - -## 模式总结 - -中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。中介者模式在`事件驱动`类软件中应用较为广泛,特别是基于`GUI`(`Graphical User Interface`,图形用户界面)的应用软件,此外,在类与类之间存在错综复杂的关联关系的系统中,中介者模式都能得到较好的应用。 - -### 主要优点 - -(1) 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。 - -(2) 中介者模式可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合“开闭原则”。 - -(3) 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展。 - -### 主要缺点 - -中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。 - -### 适用场景 - -(1) 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。 - -(2) 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。 - -(3) 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的具体中介者类。 \ No newline at end of file diff --git a/DesignPattern/object-oriented-design-principles.md b/DesignPattern/object-oriented-design-principles.md deleted file mode 100644 index 332d38e..0000000 --- a/DesignPattern/object-oriented-design-principles.md +++ /dev/null @@ -1,103 +0,0 @@ -# 面向对象设计原则 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213`。 - -如何同时提高一个软件系统的`可维护性`和`可复用性`是面向对象设计需要解决的核心问题之一。 - -在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。 - -面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。 - -面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一,在设计模式的学习中,大家经常会看到诸如“XXX模式符合XXX原则”、“XXX模式违反了XXX原则”这样的语句。 - -最常见的7种面向对象设计原则如下表所示: - -| 设计原则名称 | 含义 | 使用频率 | -| ------------------------------------------------------------ | -------- | -------- | -| 单一职责原则(`Single Responsibility Principle, SRP`) | 一个类只负责一个功能领域中的相应职责 | ★★★★☆ | -| 开闭原则(`Open-Closed Principle, OCP`) | 软件实体应对扩展开放,而对修改关闭 | ★★★★★ | -| 里氏代换原则(`Liskov Substitution Principle, LSP`) | 所有引用基类对象的地方能够透明地使用其子类的对象 | ★★★★★ | -| 依赖倒转原则(`Dependence Inversion Principle, DIP`) | 抽象不应该依赖于细节,细节应该依赖于抽象 | ★★★★★ | -| 接口隔离原则(`Interface Segregation Principle, ISP`) | 使用多个专门的接口,而不使用单一的总接口 | ★★☆☆☆ | -| 合成复用原则(`Composite Reuse Principle, CRP`) | 尽量使用对象组合,而不是继承来达到复用的目的 | ★★★★☆ | -| 迪米特法则(`Law of Demeter, LoD`) | 一个软件实体应当尽可能少地与其他实体发生相互作用 | ★★★☆☆ | - - -## 单一职责原则 - -> 单一职责原则(`Single Responsibility Principle, SRP`):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 - -单一职责原则告诉我们:`一个类不能太“累”`! - -在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。 - -单一职责原则是实现`高内聚、低耦合`的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。 - - -## 开闭原则 - -> 开闭原则(`Open-Closed Principle, OCP`):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。 - -软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。 - -任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。 - -为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。 - - -## 里氏代换原则 - -> 里氏代换原则(`Liskov Substitution Principle, LSP`):所有引用基类(父类)的地方必须能透明地使用其子类的对象。 - -里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。 - -在使用里氏代换原则时需要注意如下几个问题: - -- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。 -- 我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。 - -## 依赖倒转原则 - -> 依赖倒转原则(`Dependency Inversion Principle, DIP`):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。 - -依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。 - -在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。 - -在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(`DependencyInjection, DI`)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(`setter`注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过`setter`方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。 - -> 开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。 - - -## 接口隔离原则 - -> 接口隔离原则(`Interface Segregation Principle, ISP`):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。 - -根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。 - -每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。 - -在使用接口隔离原则时,我们需要注意控制接口的粒度: -- 接口不能太小,如果太小会导致系统中接口泛滥,不利于维护; -- 接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。 - -一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。 - -## 合成复用原则 - -> 合成复用原则(`Composite Reuse Principle, CRP`):尽量使用对象组合,而不是继承来达到复用的目的。 - -就是说,复用时要尽量使用组合/聚合关系(`关联关系`),少用继承。 - -由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。 - -一般而言,如果两个类之间是`Has-A`的关系应使用组合或聚合,如果是`Is-A`关系可使用继承。`Is-A`是严格的分类学意义上的定义,意思是一个类是另一个类的"一种";而`Has-A`则不同,它表示某一个角色具有某一项责任。 - - -## 迪米特法则 - -> 迪米特法则(`Law of Demeter, LoD`):一个软件实体应当尽可能少地与其他实体发生相互作用。 - -迪米特法则告诉我们,尽量 **不要和陌生人说话** 。 - -如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。 diff --git a/DesignPattern/overview.md b/DesignPattern/overview.md deleted file mode 100644 index 667f1b9..0000000 --- a/DesignPattern/overview.md +++ /dev/null @@ -1,112 +0,0 @@ - -# 设计模式概述 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -关于金庸小说中到底是招式重要还是内功重要的争论从未停止,我们在这里并不分析张无忌的九阳神功和令狐冲的独孤九剑到底哪个更厉害,但我想每个江湖高手梦寐以求的应该是既有淋漓的招式又有深厚的内功。 - -看到这里大家可能会产生疑问了?搞什么,讨论什么招式与内功,我只是个软件工程师。别急,正因为你是软件工程师我才跟你谈这个,因为我们的软件开发技术也包括一些招式和内功:`Java`、`Python`、`Golang`等编程语言,微服务、中间件、大数据、人工智能等开发方向,` Spring Cloud`、`Flink`、`Spark`、`PyTorch`、`K8s`等框架技术,所有这些我们都可以认为是招式;而数据结构、算法、设计模式、重构、软件工程、操作系统、网络等则为内功。招式可以很快学会,但是内功的修炼需要更长的时间。我想每一位软件开发人员也都希望成为一名兼具淋漓招式和深厚内功的“上乘”软件工程师,而对设计模式的学习与领悟将会让你“内功”大增,再结合你日益纯熟的“招式”,你的软件开发“功力”一定会达到一个新的境界。既然这样,还等什么,赶快行动吧。下面就让我们正式踏上神奇而又美妙的设计模式之旅。 - -## 设计模式是什么 - -> 站在别人的肩膀上,我们会看得更远。 - -设计模式的出现可以让我们站在前人的肩膀上,通过一些成熟的设计方案来指导新项目的开发和设计,以便于我们开发出具有更好的灵活性和可扩展性,也更易于复用的软件系统。 - -设计模式的一般定义如下: -> 设计模式(`Design Pattern)`是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。 - -与很多软件工程技术一样,模式起源于建筑领域。 - -在经典著作《建筑的永恒之道》中,有关于模式的定义: -> A pattern is a successful or efficient solution to a recurring problem within a context. - -也就是说 **模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。** - -每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,通过这种方式,我们可以无数次地重用那些已有的成功的解决方案,无须再重复相同的工作。 - -最早将模式的思想引入软件工程方法学的是1991-1992年以“四人组(`Gang of Four`,简称`GoF`,分别是`Erich Gamma`, `Richard Helm`, `Ralph Johnson`和`John Vlissides`)”自称的四位著名软件工程学者,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟。 - -`GoF`将模式的概念引入软件工程领域,这标志着软件模式的诞生。软件模式(`Software Patterns`)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。 - -软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。 - -在软件模式中,设计模式是研究最为深入的分支,设计模式用于在特定的条件下为一些重复出现的软件设计问题提供合理的、有效的解决方案,它融合了众多专家的设计经验,已经在成千上万的软件中得以应用。 - -## 设计模式的分类 - -虽然`GoF`设计模式只有`23`个,但是它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。 - -根据用途来分,设计模式可分为: -- 创建型(`Creational`):主要用于描述如何创建对象 -- 结构型(`Structural`):主要用于描述如何实现类或对象的组合 -- 行为型(`Behavioral`):主要用于描述类或对象怎样交互以及怎样分配职责 - -在`GoF`23种设计模式中包含`5`种创建型设计模式、`7`种结构型设计模式和`11`种行为型设计模式。 - -值得一提的是,有一个设计模式虽然不属于`GoF` 23种设计模式,但一般在介绍设计模式时都会对它进行说明,它就是`简单工厂模式`,也许是太“简单”了,`GoF`并没有把它写到那本经典著作中,不过现在大部分的设计模式书籍都会对它进行专门的介绍。 - -### 创建型模式 - -| 模式名称 | 学习难度 | 使用频率 | -| ------------------------------------------------------------ | -------- | -------- | -| [单例模式(Singleton Pattern)——确保对象的唯一性](DesignPattern/singleton-pattern.md) | ★☆☆☆☆ | ★★★★☆ | -| [简单工厂模式(Simple Factory Pattern)](DesignPattern/factory-pattern.md?id=简单工厂模式) | ★★☆☆☆ | ★★★☆☆ | -| [工厂方法模式(Factory Method Pattern)](DesignPattern/factory-pattern.md?id=工厂方法模式) | ★★☆☆☆ | ★★★★★ | -| [抽象工厂模式(Abstract Factory Pattern)](DesignPattern/factory-pattern.md?id=抽象工厂模式) | ★★★★☆ | ★★★★★ | -| [原型模式(Prototype Pattern)——对象的克隆](DesignPattern/prototype-pattern.md) | ★★★☆☆ | ★★★☆☆ | -| [建造者模式(Builder Pattern)——复杂对象的组装与创建](DesignPattern/builder-pattern.md) | ★★★★☆ | ★★☆☆☆ | - - -### 结构型模式 - -| 模式名称 | 学习难度 | 使用频率 | -| ------------------------------------------------------------ | -------- | -------- | -| [适配器模式(Adapter Pattern)——不兼容结构的协调](DesignPattern/adapter-pattern.md) | ★★☆☆☆ | ★★★★☆ | -| [桥接模式(Bridge Pattern)——处理多维度变化](DesignPattern/bridge-pattern.md) | ★★★☆☆ | ★★★☆☆ | -| [组合模式(Composite Pattern)——树形结构的处理](DesignPattern/composite-pattern.md) | ★★★☆☆ | ★★★★☆ | -| [装饰模式(Decorator Pattern)——扩展系统功能](DesignPattern/decorator-pattern.md) | ★★★☆☆ | ★★★☆☆ | -| [外观模式(Facade Pattern)——提供统一的入口](DesignPattern/facade-pattern.md) | ★☆☆☆☆ | ★★★★★ | -| [享元模式(Flyweight Pattern)——实现对象的复用](DesignPattern/flyweight-pattern.md) | ★★★★☆ | ★☆☆☆☆ | -| [代理模式(Proxy Pattern)——对象的间接访问](DesignPattern/proxy-pattern.md) | ★★★☆☆ | ★★★★☆ | - - -### 行为型模式 - -| 模式名称 | 学习难度 | 使用频率 | -| ------------------------------------------------------------ | -------- | -------- | -| [职责链模式(Chain of Responsibility Pattern)——请求的链式处理](DesignPattern/chain-of-responsibility-pattern.md) | ★★★☆☆ | ★★☆☆☆ | -| [命令模式(Command Pattern)——请求发送者与接收者解耦](DesignPattern/command-pattern.md) | ★★★☆☆ | ★★★★☆ | -| [解释器模式(Interpreter Pattern)——自定义语言的实现](DesignPattern/interpreter-pattern.md) | ★★★★★ | ★☆☆☆☆ | -| [迭代器模式(Iterator Pattern)——遍历聚合对象中的元素](DesignPattern/iterator-pattern.md) | ★★★☆☆ | ★★★★★ | -| [中介者模式(Mediator Pattern)——协调多个对象之间的交互](DesignPattern/mediator-pattern.md) | ★★★☆☆ | ★★☆☆☆ | -| [备忘录模式(Memento Pattern)——撤销功能的实现](DesignPattern/memento-pattern.md) | ★★☆☆☆ | ★★☆☆☆ | -| [观察者模式(Observer Pattern)——对象间的联动](DesignPattern/observer-pattern.md) | ★★★☆☆ | ★★★★★ | -| [状态模式(State Pattern)——处理对象的多种状态及其相互转换](DesignPattern/state-pattern.md) | ★★★☆☆ | ★★★☆☆ | -| [策略模式(Strategy Pattern)——算法的封装与切换](DesignPattern/strategy-pattern.md) | ★☆☆☆☆ | ★★★★☆ | -| [模板方法模式(Template Method Pattern)——复杂流程步骤的设计](DesignPattern/template-method-pattern.md) | ★★☆☆☆ | ★★★☆☆ | -| [访问者模式(Visitor Pattern)——操作复杂对象结构](DesignPattern/visitor-pattern.md) | ★★★★☆ | ★☆☆☆☆ | - -此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为: -- 类模式:处理类之间的关系 -- 对象模式:处理对象之间的关系 - -我们经常将两种分类方式结合使用,如单例模式是对象创建型模式,模板方法模式是类行为型模式。 - -## 学习建议 - -- 掌握设计模式并不是件很难的事情,关键在于多思考,多实践,不要听到人家说懂几个设计模式就很“牛”,只要用心学习,设计模式也就那么回事,你也可以很“牛”的,一定要有信心。 - -- 在学习每一个设计模式时至少应该掌握如下几点:这个设计模式的意图是什么,它要解决一个什么问题,什么时候可以使用它;它是如何解决的,掌握它的结构图,记住它的关键代码;能够想到至少两个它的应用实例,一个生活中的,一个软件中的;这个模式的优缺点是什么,在使用时要注意什么。当你能够回答上述所有问题时,恭喜你,你了解一个设计模式了,至于掌握它,那就在开发中去使用吧,用多了你自然就掌握了。 - -- “如果想体验一下运用模式的感觉,那么最好的方法就是运用它们”。正如在本章最开始所说的,设计模式是“内功心法”,它还是要与“实战招式”相结合才能够相得益彰。学习设计模式的目的在于应用,如果不懂如何使用一个设计模式,而只是学过,能够说出它的用途,绘制它的结构,充其量也只能说你了解这个模式,严格一点说:不会在开发中灵活运用一个模式基本上等于没学。所以一定要做到:少说多做。 - -- 千万不要滥用模式,不要试图在一个系统中用上所有的模式,也许有这样的系统,但至少目前我没有碰到过。每个模式都有自己的适用场景,不能为了使用模式而使用模式?滥用模式不如不用模式,因为滥用的结果得不到“艺术品”一样的软件,很有可能是一堆垃圾代码。 - -- 如果将设计模式比喻成“三十六计”,那么每一个模式都是一种计策,它为解决某一类问题而诞生,不管这个设计模式的难度如何,使用频率高不高,我建议大家都应该好好学学,多学一个模式也就意味着你多了“一计”,说不定什么时候一不小心就用上了,。因此,模式学习之路上要不怕困难,勇于挑战,有的模式虽然难一点,但反复琢磨,反复研读,应该还是能够征服的。 - -- 设计模式的“上乘”境界:“手中无模式,心中有模式”。模式使用的最高境界是你已经不知道具体某个设计模式的定义和结构了,但你会灵活自如地选择一种设计方案【其实就是某个设计模式】来解决某个问题,设计模式已经成为你开发技能的一部分,能够手到擒来,“内功”与“招式”已浑然一体,要达到这个境界并不是看完某本书或者开发一两个项目就能够实现的,它需要不断沉淀与积累,所以,对模式的学习不要急于求成。 - -- 最后一点来自`GoF`已故成员、我个人最尊敬和崇拜的软件工程大师之一`John Vlissides`的著作《设计模式沉思录》(`Pattern Hatching Design Patterns Applied`):模式从不保证任何东西,它不能保证你一定能够做出可复用的软件,提高你的生产率,更不能保证世界和平。模式并不能替代人来完成软件系统的创造,它们只不过会给那些缺乏经验但却具备才能和创造力的人带来希望。 - - diff --git a/DesignPattern/prototype-pattern.md b/DesignPattern/prototype-pattern.md deleted file mode 100644 index 309ce13..0000000 --- a/DesignPattern/prototype-pattern.md +++ /dev/null @@ -1,137 +0,0 @@ -# 原型模式(Prototype Pattern)——对象的克隆 - - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213`。 - -## 模式概述 - -### 模式定义 - -我们平时经常进行的两个电脑基本操作:复制和粘贴,快捷键通常为`Ctrl+C`和`Ctrl+V`,通过对已有对象的复制和粘贴,我们可以创建大量的相同对象。如何在一个面向对象系统中实现对象的复制和粘贴呢?`原型模式`正为解决此类问题而诞生。 - -> `原型模式(Prototype Pattern)`:使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。 - -需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。 - -### 模式结构图 - -原型模式结构图如下所示: - -![原型模式结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200530123209513-298312089.png) - -原型模式结构图中包含如下几个角色: -- `Prototype(抽象原型类)`:它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。 - -- `ConcretePrototype(具体原型类)`:它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。 - -- `Client(客户类)`:让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。 - -原型模式的核心在于如何实现克隆方法。 - -### 模式伪代码 -定义接口的`clone()`方法,具体原型类(`ConcretePrototype`)实现克隆方法 -```java -public interface Prototype { - Prototype clone(); -} - -public class ConcretePrototype implements Prototype { - //成员属性 - private String attr; - - @Override - public Prototype clone() { - ConcretePrototype prototype = new ConcretePrototype(); - prototype.setAttr(this.attr); - return prototype; - } - - // 无参构造 - public ConcretePrototype() { - - } - - // 带参构造 - public ConcretePrototype(String attr) { - this.attr = attr; - } - - // getter and setter - public String getAttr() { - return attr; - } - - public void setAttr(String attr) { - this.attr = attr; - } -} -``` -在客户类中我们只需要创建一个`ConcretePrototype`对象作为原型对象,然后调用其`clone()`方法即可得到对应的克隆对象 -```java -public static void main(String[] args) { - Prototype obj1 = new ConcretePrototype("行无际"); - Prototype obj2 = obj1.clone(); -} -``` -这是`原型模式`的通用实现,它与编程语言特性无关,任何面向对象语言都可以使用这种形式来实现对原型的克隆。 - -## 模式应用 - -### 模式在JDK中的应用 -`Java`语言提供了`clone()`方法。`Cloneable`接口是一个标记接口,也就是没有任何内容,定义如下: -```java -/** - * A class implements the Cloneable interface to - * indicate to the {@link java.lang.Object#clone()} method that it - * is legal for that method to make a - * field-for-field copy of instances of that class. - * @see java.lang.CloneNotSupportedException - * @see java.lang.Object#clone() - */ -public interface Cloneable { -} -``` -`clone()`方法是在`Object`中定义的,而且是`protected`型的,只有实现了这个`Cloneable`接口,才可以在该类的实例上调用`clone`方法,否则会抛出`CloneNotSupportException`。`Object`中默认的实现是一个浅拷贝,如果需要深拷贝的话,需要自己重写`clone`方法或者把`对象序列化再反序列化得到新对象`或借助第三方的库实现深拷贝。 -```java -public class Object { - protected native Object clone() throws CloneNotSupportedException; -} - -public class Item implements Cloneable { - - private String name; - - private Object obj; - - public static void main(String[] args) throws Exception { - Item item = new Item("行无际", new Object()); - Item replicaItem = (Item) item.clone(); - - // true,表明默认浅拷贝 - System.out.println(item.obj == replicaItem.obj); - } -} -``` - -### 模式在开源项目中的应用 - -项目中我们可能会结合一些工具库,如`BeanUtils.copyProperties()`来实现对象的克隆。这里也就没必要举例子,其实就是把对象的属性等拷贝一份,但是要根据实际需求来决定是深拷贝还是浅拷贝。另外`Spring`容器中的`Bean`的`scope`有点这里的味道。 -1. 当bean的scope为`singleton`时,Spring容器仅创建类的一个实例对象,当下次获取的时候直接返回已创建出来的对象,即单例 - -2. 当bean的scope为`prototype`时,用户每次获取对象时都创建一个全新的对象返回。 - -## 模式总结 - -原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制(`Ctrl+C`)和粘贴(`Ctrl+V`)操作就是原型模式的典型应用,下面对该模式的使用效果和适用情况进行简单的总结。 - -### 主要优点 - -当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。 - -### 适用场景 - -(1) 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。 - -(2) 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。 - -(3) 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。 diff --git a/DesignPattern/proxy-pattern.md b/DesignPattern/proxy-pattern.md deleted file mode 100644 index df57cb4..0000000 --- a/DesignPattern/proxy-pattern.md +++ /dev/null @@ -1,126 +0,0 @@ -# 代理模式(Proxy Pattern)——对象的间接访问 - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213` - -## 模式概述 - -近年来,代购已逐步成为电子商务的一个重要分支。何谓代购,简单来说就是找人帮忙购买所需要的商品,当然你可能需要向实施代购的人支付一定的费用。代购通常分为两种类型:一种是因为在当地买不到某件商品,又或者是因为当地这件商品的价格比其他地区的贵,因此托人在其他地区甚至国外购买该商品,然后通过快递发货或者直接携带回来;还有一种代购,由于消费者对想要购买的商品相关信息的缺乏,自已无法确定其实际价值而又不想被商家宰,只好委托中介机构帮其讲价或为其代买。 - -在软件开发中,也有一种设计模式可以提供与代购类似的功能。由于某些原因,客户端无法直接访问某个对象或访问某个对象存在困难时,可以通过一个称之为“代理”的第三者来实现间接访问,该方案对应的设计模式被称为代理模式。 - -### 模式定义 - -> 代理模式(`Proxy Pattern`):给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。 - -代理模式是一种对象结构型模式。在代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到中介的作用,它去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。 - -### 模式结构图 - -代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层,代理模式结构如下图所示: - -![代理模式结构图](https://img2020.cnblogs.com/blog/1546632/202006/1546632-20200626205846504-1037997627.png) - -代理模式包含如下三个角色: - -- Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。 - -- Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。 - -- RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。 - -### 模式伪代码 - -代理模式的结构图比较简单,但是在真实的使用和实现过程中要复杂很多,特别是代理类的设计和实现。 - -`抽象主题类`声明了真实主题类和代理类的公共方法,它可以是接口、抽象类或具体类,客户端针对抽象主题类编程,一致性地对待真实主题和代理主题,典型的抽象主题类代码如下: -```java -public interface Subject { - void request(); -} -``` - -`真实主题类`实现了抽象主题类,提供了业务方法的具体实现,其典型代码如下: -```java -public class RealSubject implements Subject { - @Override - public void request() { - //业务方法具体实现代码 - } -} -``` - -`代理类`也是抽象主题类的子类,它维持一个对真实主题对象的引用,调用在真实主题中实现的业务方法,在调用时可以在原有业务方法的基础上附加一些新的方法来对功能进行扩充或约束,最简单的代理类实现代码如下: -```java -public class Proxy implements Subject { - // 维持一个对真实主题对象的引用 - private RealSubject realSubject; - - public Proxy(RealSubject realSubject) { - this.realSubject = realSubject; - } - - public void preRequest() { - // ... - } - - public void postRequest() { - // ... - } - - @Override - public void request() { - preRequest(); - // 调用真实主题对象的方法 - realSubject.request(); - postRequest(); - } -} -``` - -## 模式应用 - -像上面代理类所实现的接口和所代理的方法都在代码中写死,被称之为`静态代理`。如果要为不同类的不同方法生成静态代理,代理类的数量将会发生爆炸。Java中也提供了对`动态代理`的支持。所谓动态代理(`Dynamic Proxy`),是指系统运行时动态生成代理类。 - -JDK中提供的动态代理只能代理一个或者多个接口,如果需要动态代理具体类或者抽象类,可以使用`CGLib`(Code Generation Library)等工具。CGLib是一个功能较为强大、性能也较好的代码生成包,在许多`AOP`框架中得到广泛应用。后面我会专门写一篇探究动态代理实现的博客,将全面细致地介绍动态代理。 - -补充:[一文读懂Java中的动态代理](https://www.cnblogs.com/itwild/p/13302241.html) - -## 模式总结 - -代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。代理模式类型较多,其中远程代理、虚拟代理、保护代理等在软件开发中应用非常广泛。 - -### 主要优点 - -(1) 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。 - -(2) 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。 - -此外,不同类型的代理模式也具有独特的优点,例如: - -(1) 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。 - -(2) 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。 - -(3) 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。 - -(4) 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。 - -### 主要缺点 - -(1) 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。 - -(2) 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。 - -### 适用场景 - -代理模式的类型较多,不同类型的代理模式有不同的优缺点,它们应用于不同的场合: - -(1) 当客户端对象需要访问远程主机中的对象时可以使用远程代理。 - -(2) 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。 - -(3) 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。 - -(4) 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。 - -(5) 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。 \ No newline at end of file diff --git a/DesignPattern/singleton-pattern.md b/DesignPattern/singleton-pattern.md deleted file mode 100644 index 9f35ba3..0000000 --- a/DesignPattern/singleton-pattern.md +++ /dev/null @@ -1,182 +0,0 @@ -# 单例模式(Singleton Pattern)——确保对象的唯一性 - - -说明:设计模式系列文章是读`刘伟`所著`《设计模式的艺术之道(软件开发人员内功修炼之道)》`一书的阅读笔记。个人感觉这本书讲的不错,有兴趣推荐读一读。详细内容也可以看看此书作者的博客`https://blog.csdn.net/LoveLion/article/details/17517213`。 - -## 模式概述 - -### 模式定义 - -实际开发中,我们会遇到这样的情况,为了节约系统资源或者数据的一致性(比如说全局的`Config`、携带上下文信息的`Context`等等),有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。 - -> 单例模式(`Singleton Pattern`): 确保某一个类只有一个实例,而且自己实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。 - -单例模式有三个要点: -1. 某个类只能有一个实例 -2. 它必须自行创建这个实例 -3. 它必须自行向整个系统提供这个实例 - -### 模式结构图 - -单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。单例模式结构图如下所示: - -![单例模式结构图](https://img2020.cnblogs.com/blog/1546632/202005/1546632-20200523104505352-451913605.png) - -单例模式结构图中只包含一个单例角色: -- `Singleton(单例)`:在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。 - -### 饿汉式单例与懒汉式单例 - -#### 饿汉式单例 - -`饿汉式单例`类是实现起来最简单的单例类。由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,典型代码如下: - -```java -public class EagerSingleton { - private static final EagerSingleton instance = new EagerSingleton(); - private EagerSingleton() { } - - public static EagerSingleton getInstance() { - return instance; - } -} -``` - -#### 懒汉式单例 - -`懒汉式单例`在第一次调用`getInstance()`方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(`Lazy Load`)或者懒加载技术,即需要的时候再加载实例,为避免`多线程`环境下同时调用`getInstance()`方法从而生成多个实例,需要确保线程安全,相应实现也就有多种方式。 - -`第一种`方法可以使用关键字`synchronized`,代码实现如下: -```java -public class LazySingleton { - private static LazySingleton instance = null; - - private LazySingleton() { } - - public synchronized static LazySingleton getInstance() { - if (instance == null) { - instance = new LazySingleton(); - } - return instance; - } -} -``` -在`getInstance()`方法前面增加了关键字`synchronized`进行同步,以处理多线程同时访问的安全问题。我们知道使用`synchronized`关键字最好是在离共享资源最近的位置加锁,这样同步带来的性能影响会减小。所以`让人感觉`上面的实现可以优化为如下代码: - -```java -public static LazySingleton getInstance() { - if (instance == null) { - synchronized (LazySingleton.class) { - instance = new LazySingleton(); - } - } - return instance; -} -``` -问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。原因如下: -假如在某一瞬间`线程A`和`线程B`都在调用`getInstance()`方法,此时`instance`对象为`null`值,均能通过`instance == null`的判断。由于实现了`synchronized`加锁机制,`线程A`进入`synchronized`修饰的代码块中执行实例创建代码,`线程B`处于排队等待状态,必须等待`线程A`执行完毕后才可以进入`synchronized`修饰的代码块。但当`A`执行完毕时,`线程B`并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在`synchronized`中再进行一次`(instance == null)`判断,这种方式称为`双重检查锁定(Double-Check Locking)`。使用双重检查锁定实现的懒汉式单例类典型代码如下所示: - -```java -public class LazySingleton { - private volatile static LazySingleton instance = null; - - private LazySingleton() { } - - public static LazySingleton getInstance() { - // 第一重判断 - if (instance == null) { - // 使用synchronized关键字加锁 - synchronized (LazySingleton.class) { - //第二重判断 - if (instance == null) { - instance = new LazySingleton(); //创建单例实例 - } - } - } - return instance; - } -} -``` -需要注意的是,如果使用`双重检查锁定`来实现懒汉式单例类,最好在静态成员变量`instance`之前增加修饰符`volatile`,被`volatile`修饰的变量可以保证多线程环境下的可见性以及禁止指令重排序。由于`volatile`关键字会屏蔽Java虚拟机所做的一些优化,可能对执行效率稍微有些影响,因此使用双重检查锁定来实现单例模式也不一定是最完美的实现方式。 - -如果是`java`语言的程序,还可以使用`静态内部类`的方式实现。代码如下: -```java -public class Singleton { - private Singleton() { - } - - private static class HolderClass { - final static Singleton instance = new Singleton(); - } - - public static Singleton getInstance() { - return HolderClass.instance; - } -} -``` -由于静态单例对象没有作为`Singleton`的成员变量直接实例化,因此`类加载`时不会实例化`Singleton`,第一次调用`getInstance()`时将加载内部类`HolderClass`,在该内部类中定义了一个`static`类型的变量`instance`,此时会首先初始化这个变量,由`Java虚拟机`来保证其线程安全性,确保该成员变量只初始化一次。 - -## 模式应用 - -### 模式在JDK中的应用 -在JDK中,`java.lang.Runtime`使用了`饿汉式单例`,如下: -```java -public class Runtime { - private static Runtime currentRuntime = new Runtime(); - - public static Runtime getRuntime() { - return currentRuntime; - } - - /** Don't let anyone else instantiate this class */ - private Runtime() {} -} -``` - -### 模式在开源项目中的应用 - -`Spring`框架中许多地方使用了`单例模式`,这里随便举个例子,如`org.springframework.aop.framework.ProxyFactoryBean`中的部分代码如下: -```java -/** - * Return the singleton instance of this class's proxy object, - * lazily creating it if it hasn't been created already. - * @return the shared singleton proxy - */ -private synchronized Object getSingletonInstance() { - if (this.singletonInstance == null) { - this.targetSource = freshTargetSource(); - if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) { - // Rely on AOP infrastructure to tell us what interfaces to proxy. - Class targetClass = getTargetClass(); - if (targetClass == null) { - throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy"); - } - setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader)); - } - // Initialize the shared singleton instance. - super.setFrozen(this.freezeProxy); - this.singletonInstance = getProxy(createAopProxy()); - } - return this.singletonInstance; -} -``` - -## 模式总结 - -单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。 - -### 主要优点 - -(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。 - -(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。 - -(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。 - -### 适用场景 - -在以下情况下可以考虑使用单例模式: - -(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。 - -(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。 diff --git a/Interview/non-technical-interview-questions.md b/Interview/non-technical-interview-questions.md deleted file mode 100644 index 2fa1b6d..0000000 --- a/Interview/non-technical-interview-questions.md +++ /dev/null @@ -1,915 +0,0 @@ -# 非技术面试题汇总 - -原文链接:`https://blog.csdn.net/weixin_40845165/article/details/89852397` - -说明:原文是浏览网页时无意间看到的。扫了一眼,总结得还不错,感谢原文作者分享,这里转载一下,做个备份。一方面送给正在找工作的朋友,另一方面,把原文排版简单调整了一下,生成目录,方便查找。 - -## 常见初级面试题 - -1、谈谈你自己的情况? - -> 【解答思路】:建议大家用2分钟得自我介绍,面试官较喜欢的自我介绍 -1、有亮点,每一小段都有一个亮点,而不是平铺直叙 -2、有互动,每一小段都会和面试官互动,而不是自说自话,但是切记,这种互动并不需要面试官配合,绝对不要总是直勾勾地盯着面试官的眼睛逼着人家配合你,要知道面试官最恐怖的经历就是被申请人从头盯到尾! -3、 相关性强, 每一小段都和所申请的职位有关系,而不是流水账。 - -2、你对自己的学习成绩满意吗 - -> 【解答思路】:有的毕业生成绩比较好,这样的问题就很好回答,但对于那些成绩不太好的毕业生,可以表明自己的态度,并给予一个合适的理由,但不能找客观原因,如“老师教得不好”,显得你是推卸责任的人,同时最好突出一个自己好的方面,以免让人觉得你一无是处。 -正确回答:比较满意,我想我在学校学到的不仅是课本知识,还学到很多为人处世的经验。 - -3、你有什么优缺点 - ->【解答思路】:优点在这个问题上,面试官关注的问题有两点。第一,申请人没有撒谎, 而是真实地阐述了自己的优点。第二,他所阐述的优点,恰好是这个职位所需要的素质。有很多时候,对于一个岗位而言的优点,会成为另一个岗位的缺点。比如说,如果你具备很强的领导能力,往往不适合从事秘书、助理、客户服务代表等以细节和服务他人为主的工作。所以,在回答这个问题的时候,要遵从以下步骤: -1、找出自己的三至五个优点; -2、每个优点找出N多个例子,举例最好来自学习、工作和生活等三个方面,而不是仅仅来自一个方面; -3、在这三到五个优点之中,精选出一两个和所申请职位最吻合的优点。缺点认识自己的缺点是一个巨大的优点,当HR问到你缺点的时候,你的机会来了,请快展示你的自知之明吧!你想把优点故意包装成缺点?比方说把"对工作负责"这个优点伪装成"对自己和他人要求过高?" 面试官会不屑一顾地批注:这是我五年前玩剩下的把戏,此人土冒之极。你想完全实话实说:应聘会计的人说自己粗心?应聘销售的人说自己容易紧张?面试官会无限惋惜地批注:此人心眼好,老实,但不适合我们的工作,推荐到"实话实说"栏目组任职去吧!对待这个问题,惟一的对策就是真诚地暴露自己的弱点,只要这个弱点不是你所申请职位的"致命伤"即可。 - -4、你觉得大学生活使你收获了什么 - ->【解答思路】:这是一个总体概括性的问题,很明显,面试官期待你给出一个总体概括性的回答。 -范例1:我觉得大学生活使我学会了与人沟通,可能您会觉得,十个大学生有九个会强调自己善于与人沟通,不过我依然觉得这是我大学里面最大的收获。您从简历上看得出来,我大学里在学生会工作了两年半,从干事一直到副主席,这使我有机会锻炼与年龄、背景完全不同的人交流,从学生到老师,从学校的领导到校外的公司,每一种沟通的方式和方法都不同,的确使我学到了很多。 -范例2:我觉得大学是我迄今为止成长最快的时期,有很多收获。首先是知识和技能方面的,我修了"地理科学"和"管理学"两个专业,虽然我不打算从事地理科学方面的工作, 但是我掌握了很多必要的工作技能,比如搜索信息、分析信息、独立思考等。除了知识, 我还提升了自己的综合素质。就拿我担任班长这件事来说,我觉得提升很快,首先是要竞选,竞选成功以后要策划吸引人的班级活动,策划活动的过程中要调动同学一块参与等等,每一个环节都很锻炼人。 - -5、谈一谈你的一次失败经历 - -> 【解答思路】: -1、不宜说自己没有失败的经历; -2、不宜把那些明显的成功说成是失败; -3、不宜说出严重影响所应聘工作的失败经历; -4、所谈经历的结果应是失败的; -5、宜说明失败之前自己曾信心白倍、尽心尽力; -6、说明仅仅是由于外在客观原因导致失败; -7、失败后自己很快振作起来,以更加饱满的热情面对以后的工作。 -如果是外企,你就可以说失败的经历就是追女孩子失败之类; -如果是国企,最好编一个故事,随便编什么,比如说学校的运动会,你参加800米的比赛啊,你准备成绩跑到多少秒,之前也进行了精心准备,练习,但最后没有跑到那个成绩,所以你发愤练习,终于在第二年的运动会跑进了前三,破了开始预定的目标,我的意思就是告诉你,虽然让你讲失败的经历,但你讲了之后要让人家听了,虽然失败了,但是你很有精神,另外一种意义上是一种成功,明白了吧,但要注意哦,要编的圆满一点,特别涉及倒数字,一定要准确,不要闹笑话。 - -6、如果你在大学(学院)做过兼职工作的话,你认为哪种兼职工作对你最有意思?为什么? - -> 【解答思路】:当面试刚刚走出校门的毕业生时(就是那些几乎没有工作经验的应聘者),企业希望录用那些要么学习很快,要么有领导(管理)潜力的毕业生。希望对方有决定能力、毅力(时间加努力等于成功),或是能够看清人的能力,在介绍兼职时着重突出你的学习能力、工作能力、工作业绩等。 - -7、最能概括你自己的三个词是什么? - ->【解答思路】:概括自己得三个词要结合你所应聘岗位来写,然后用一个自身例子分贝来来解释这三个词,比如:学习能力强、创新能力强、有团队精神、有责任心等 - -8、你的实力是什么? - ->【解答思路】:描述你得专业技能、两三项和该工作最相关的工作经验或项目经验。避免老生常谈或过分笼统,应提供详细的例子。描述能把这些技能运用于新职位的新方法。例如,指出你仅有他们要求的工作经验是明智的,面试官肯定很重视这些,另外你不仅指出你以前的技能、相关得工作经验,还要说明你能胜任这份工作。 - -9、你的私人生活是否存在对你的职业造成影响的问题? - -> 【解答思路】:使面试官深信你能把私人的感受和职业行为分开来。例如,假如你不能积极处理好危急关头的事情,你就不可能真正帮助你的病人面对创伤问题,因为那会引发你太多个人的强烈情绪。 -正确回答:我总是很注意把自己的私人生活与职业生活分开。我认为在给病人治病时,重要的是保持客观。一个临床医生应该能够控制自己的感情而不带偏见地给病人看病。假如你把个人的问题或感受发泄到病人的身上,那只能对病人造成伤害。这个问题对于在医疗服务领域工作的人尤为重要。面试官想知道作为一个独立个体,你是如何完善你的人格的。假如你有不能克服的个人问题,这些问题就会影响你和你病人的关系,也有可能影响你对病人的诊断,同时影响你的治疗方案和治疗建议。 - -10、告诉我你从最近读的书中学到什么? - ->【解答思路】:面试官想知道求职者是否与公司的其他人有着共同的兴趣。你利用业余时间有效、轻松地学习新的东西吗?你生性对什么好奇? -正确回答:我喜欢读人物传记,尤其是那些生活在不同时代人的传记。最近,我读了丘吉尔的传记,这本书教会了我很多领导的价值和重重压力下获得的良好的公共关系。 - -11、你是否有过工作经验? - -> 【解答思路】:最好的回答是如实回答自己的真实情况。如果学员没有工作经验的话,依照岗位要求的技术方向以及岗位职责,把重点转移到做过的类似的项目经验上去,让面试官确信你的技术能够胜任这个岗位。有过工作经验的学员,要把以前的工作经验描述的和应聘的岗位有相同之处。 -正确回答:之前曾经在XX网络科技公司做过网络管理员这个职位,在工作中我充分利用所学知识,对公司的网络环境进行了净化,同时对公司的网络进行了安全加固……,在工作当我学到了很多的东西,甚至包括之前我所没有接触过的方面。 - -12、谈谈你的家庭情况 - -> 【解答思路】: -1、了解家庭情况对于了解应聘者的性格、观念、心态等有一定的作用,这是招聘单位问该问题的主要原因。 -2、简单地罗列家庭人口。 -3、宜强调温馨和睦的家庭氛围。 -4、宜强调父母对自己教育的重视。 -5、宜强调各位家庭成员的良好状况。 -6、宜强调家庭成员对自己工作的支持。 -7、宜强调自己对家庭的责任感。 -正确回答:我父亲是位铁路工作者,母亲是做会计工作的。我从他们身上学到了严谨还有细心,………… - -13、你的朋友如何评价你 - ->【解答思路】:这个问题是招聘方考察你的个人表达能力和认识能力的问题。你可以突出自己的一些优点,但是不要说得过于直白,同时要强调自己的能力比较适合做工作,这里技巧很重要。同时说话要机智,也可以适当加些小幽默,显示你天生有与人交际的能力和创新能力,这对于工作来说是很重要的。 - -14、你最崇拜的人是谁 - ->【解答思路】:对于这个问题,最好是说一个本行业的知名人士,你能从这个知名人士身上能学到什么东西。 - -15、你的专业课程中,哪些课程最让你感兴趣了? - -> 【解答思路】:你在学校最喜欢的科目一般而言也是你最学有所长、学有所得的科目。如果你觉得这个科目能对你当前应聘的工作产生积极作用,就抓住机会予以强调,作深入细致阐述,否则不妨淡化处理。在回答在校期间最不喜欢的科目时注意把握如下原则: -第一,要懂得如何避重就轻; -第二,假如你不喜欢的科目恰好与所应聘的工作密切相关,那你就需要巧妙改变主题; -第三,要有幽默感。 -关于这类问题,面试应届大学毕业生和毕业研究生时主考官常会提起,因此,如果你是一个青年学生,在面试前就不妨琢磨琢磨,用心准备一番。 - -16、我想知道,你在大学时遇到的最有挑战性的事情是什么?为什么你认为那件事对你最具有挑战性? - -> 【解答思路】:面试者往往通过这个问题考查你的信心,信心是应聘者在面试者面前是否具有吸引力的一个非常重要的因素。有信心的人往往在办事、说话和判断中,以及在对自己的能力方面表现出强烈的信心。有信心的人善于对他们自己的决定和行为的后果承担责任。此外,他们往往把冲突视为是发展的机会。 - -17、介绍一下你的课外活动。你为什么愿意从事那些课外活动?通过那些课外活动,你都学了些什么?为那件事对你最具有挑战性? - -> 【解答思路】:这个问题考查是你的业余倾向,还有你的学习能力,切记不要说一些娱乐的业余课外活动,选一些积极向上的比如:学习小组、志愿者等等,其次叙述你在其中体现了哪些人身价值,从中锻炼了哪方面,积累了哪些经验,学到了什么! - -18.大学时,你为什么选择专业? - ->【解答思路】:如果你应聘得岗位和你所学得专业对口的话很容易回答,但如果和所学专业不对口得话,回答起来就要慎重看了。比如说是父母选的或是其他同学都添的这个专业等,但上学后觉得不适合自己,在休完本专业的情况下,努力学习适合自己的动画行业,毕业之后又到专门的培训机构学习,并有项目经验等。 - -## 企业忠诚度考察面试题 - -19、之前的薪水有多少? - -> 【参考回答】:在以前的单位,差不多一个月薪水是在1500到2000之间吧,因为奖金是看绩效考核的,不过我一般情况的薪水都在1800到1900之间。 -【解答思路】:之前的薪水可据实以报,切勿写不实数据,因为有些公司会去查证,万一得知所言不实,就可能会丧失工作机会。 - -20、希望待遇多少? - ->【参考回答】:刚到公司,还是按照公司的岗位工资我就满意了,后期我想咱们单位应该是能力与待遇成正比的,对吗? -【解答思路】:常被问到希望待遇时,最好能诚实回答,考虑年龄、经验及能力等客观条件来决定,对某些企业而言,这也是评论应征者的能力及经验的参考,一般要求比以前一工作薪水高出百分之十是合理范围。 - -21、除了本公司外,还应征了哪些公司? - ->【参考回答】:是我离职以来应聘的第一家单位,之前还没有过求职 -【解答思路】:这是相当多公司会问的问题,其用意是要概略知道应征者的求职志向,所以这并非绝对是负面答案,就算不便说出公司名称,也应回答“销售同种产品的公司”,如果应征的其他公司是不同业界,容易让人产生无法信任的感觉。 - -22、何时可以到职? - ->【参考回答】:如果被录用的话,可按公司规定时间上班。 -【解答思路】:大多数企业会关心就职时间,最好是回答“如果被录用的话,可按公司规定时间上班,”但如果还未辞去上一个工作、上班时间又太近,似乎有些强人所难,因为交接至少要一个月的时间,应进一步说明原因,录取公司应该会通融的 - -23、你了解我们单位吗 - ->【参考回答】:之前我在网络上以及朋友的口中了解过咱们单位,咱们单位在网络行业是xxx巨头,……… -【解答思路】:只要毕业生提前做些准备,从多种途径收集用人单位的信息,这样的问题就比较容易回答,如果答非所问或张口结舌,场面可能会很尴尬。 - -24、希望工作地点在哪里? - ->【参考回答】:至于工作地点,我会按照单位的需求而定 -【解答思路】:这是有数个分公司及营业场所的企业会问到的问题,如果有希望的工作地点,可据实说出来,如:现在虽然希望在某营业场所工作,但也可有“将来还是希望能到总公司服务”之类的要求。 - -25、你为什么应聘我们单位 - ->【参考回答】:有两点,一是我对这个行业很感兴趣,二是咱们单位在这个行业里属于领军式企业,对我吸引力很打。 -【解答思路】:毕业生可以从该单位在行业中的地位、自己的兴趣、能力和日后的发展前景等角度回答此问题 - -26、你找工作最重要的考虑因素是什么 - ->【参考回答】:我觉得应该是发展和提升,再者就是公司的培训,每个人都有自己的职业规划和定位,二我的规划就是在这段时间内无论是外职业生涯还是内职业生涯,我希望都有所发展。 -【解答思路】:可以结合你正在应聘的工作,侧重谈你的兴趣、你对于取得事业上的成就的渴望、施展你的才能的可能性、未来的发展前景等方面来谈 - -27、你认为你适合什么样的工作 - -> 【参考回答】:我大学的专业是计算机应用,主要学习的是网络方面的知识,在我毕业以后我又进行了网络工程师系统的培训,所以我觉得在网络方面的工作我会做的很好 -【解答思路】:结合你的长处或者专业背景回答,也许单位是结合未来的工作安排来提问,也许只是一般性地了解你对自己的评价,不要说不知道,也不要说什么都行 - -28、你为什么选择我们公司? - -> 【参考回答】:在几家应聘单位中我之所以选择咱们公司,是因为看重咱们公司在业内的影响力以及咱们公司的实力 -【解答思路】: -1、面试官试图从中了解你求职的动机、愿望以及对此项工作的态度。 -2、建议从行业、企业和岗位这三个角度来回答。 -3、参考答案——“我十分看好贵公司所在的行业,我认为贵公司十分重视人才,而且这项工作很适合我,相信自己一定能做好。” - -29、您在前一家公司的离职原因是什么? - ->【参考回答】:我离职是因为这家公司倒闭。我在公司工作了三年多,有较深的感情。从去年始,由于市场形势突变,公司的局面急转直下。到眼下这一步我觉得很遗憾,但还要面对显示,重新寻找能发挥我能力的舞台。 -【解答思路】: -1、最重要的是:应聘者要使找招聘单位相信,应聘者在过往的单位的“离职原因”在此家招聘单位里不存在。 -2、避免把“离职原因”说得太详细、太具体。 -3、不能掺杂主观的负面感受,如“太幸苦”、“人际关系复杂”、“管理太混乱”、“公司不重视人才”、“公司排斥我们某某的员工”等。 -4、但也不能躲闪、回避,如“想换换环境”、“个人原因”等。 -5、不能涉及自己负面的人格特征,如不诚实、懒惰、缺乏责任感、不随和等。 -6、尽量使解释的理由为应聘者个人形象添彩。 -7、同一个面试问题并非只有一个答案,而同一个答案并不是在任何面试场合都有效,关键在于应聘者掌握了规律后,对面试的具体情况进行把握,有意识地揣摩面试官提出问题的心理背景,然后投其所好。 - -30、如果我录用你,你认为你在这份工作上会待多久呢? - -> 【参考回答】: -A.这问题可能要等我工作一段时间后,才能比较具体地回答。 -B.一份工作至少要做3年、5年,才能学习到精华的部分。 -C.这个问题蛮难回答的,可能要看当时的情形。 -D.至少2年,2年后我计划再出国深造。 -【解答思路】:选择B最多A次之。B的回答能充分显示出你的稳定性,不过,这必须配合你的履历表上,之前的工作是否也有一致性。A的回答则是非常实际,有些人事主管因为欣赏应征者的坦诚,能够接受这样的回答。 - -31、如果你离开现职,你认为你的老板会有什么反应? - -> 【参考回答】: -A.很震惊,因为老板对我算是很信赖,我就如同他的左右手一样。 -B.还好吧,他大概心里也有数,反正公司现在也不忙。 -C.他大概习惯了,反正他手下的人来来去去已是司空见惯。 -D.我想他一定会生气地破口大骂,他是一个相当情绪化的人。 -【解答思路】:最理想的回答是A。面谈者想借此了解你和前(现)任主管的相处情形,以及你在主管心目中的地位如何 - -32、你为什么还没找到合适的职位呢? - -> 【参考回答】:我是最近刚刚离职 -【解答思路】:别怕告诉他们你可能会有的聘请,千万不要说“我上一次面试弄得一塌糊涂……”。指出这是你第一次面试。 - -33、除了工资,还有什么福利最吸引你? - -> 【参考回答】:对于我来说就是咱们公司的培训。 -【解答思路】:尽可能诚实,如果你做足了功课,你就知道他们会提供什么,回答尽可能和他们提供的相配。如果你觉得自己该得到更多,也可以多要一点。 - -34、你会为公司服务多久? - -> 【参考回答】:只要公司与我双方都觉得我在公司运营中发挥作用,在不断成长成熟,在公司实现预期目标中作出贡献,我乐意为公司效力。 -【解答思路】:把公司的发展与你个人发展结合到一起来说这个问题 - -35、你愿意到另外一个城市定居吗? - -> 【参考回答】:我宁愿留在这里,不过可能的话我会考虑一下。 -【解答思路】:尽管在某些初次面试中,你也许会被问到许多像这种能代表你利益的问题。尽管这样的问题对一个初次面试的人来说不公平,回答不好的话,你也许什么都得不到,甚至还会失去所有的东西。假如你在初次面试时被突然地问到这样的问题,只需说些如“当然有可能”或者“我愿意考虑一下”之类的话。后来,如果你得到了这份工作,你会知道具体的工作条件,然后决定你是否愿意接受这个职位。记住,在提供工作阶段,你有大部分的谈判权利,而且雇主也许会很愿意为你提供你所需要的东西。如果那些不成问题,你也许会想说明一下,经过重新考虑,你已决定了不迁往别处,但是你想让雇主知道可以考虑把你安排在其他的在将来开放的职位上。 - -36、你为什么对我们的工作职位感兴趣? - ->【解答思路】: -1、面试官试图从中了解你求职的动机、愿望以及对此项工作的态度。 -2、建议从行业、企业和岗位这三个角度来回答。 -3、参考答案——“我十分看好贵公司所在的行业,我认为贵公司十分重视人才,而且这项工作很适合我,相信自己一定能做好。” - -37、对这项工作,你有哪些可预见的困难? - ->【解答思路】: -1、不宜直接说出具体的困难,否则可能令对方怀疑应聘者不行。 -2、可以尝试迂回战术,说出应聘者对困难所持有的态度——“工作中出现一些困难是正常的,也是难免的,但是只要有坚忍不拔的毅力、良好的合作精神以及事前周密而充分的准备,任何困难都是可以克服的。” - -38、你能否胜任? - ->【解答思路】: -1、应聘者最好站在招聘单位的角度来回答。 -2、招聘单位一般会录用这样的应聘者:基本符合条件、对这份工作感兴趣、有足够的信心。 -3、如“我符合贵公司的招聘条件,凭我目前掌握的技能、高度的责任感和良好的饿适应能力及学习能力,完全能胜任这份工作。我十分希望能为贵 公司服务,如果贵公司给我这个机会,我一定能成为贵公司的栋梁! - -39、如果MSRA公司和另一家公司同时给你Office,你怎么办? - ->【解答思路】:面试官试图从中了解你求职的动机、愿望以及对此项工作的态度。一般大家都会以公司名气和工资高低作为取舍依据,你如果这样回答:“我会把企业文化、公司发展前景、个人在公司的发展、工作部门、职位、将来的顶头上司和团队成员是什么样的人等因素进行综合分析比较做出结论,决定我的舍取。” 面试官将对你刮目相看。 - -40、未来工作中,你想避免些什么?为什么? - ->【解答思路】: -1、避免出错,尤其是刚开始,慢点没关系,因为开始不熟悉没人怪你,可是你为了追求速度而错误百出就坏了,给人留下老出错的印象后,一有问题大家都会想到你,很痛苦。 -2、避免受欺负,新人都会觉得刚来要和同事处理好关系,受欺负也会忍气吞声,错!既然明知道这个人是欺负你,你就要反击,决不示弱!不是你的错硬要怪你的人得罪就得罪,别人心理有数你这样的反击到底对否,不会对你有坏印象,反而会懂得尊重你。 -3、不会说话就少说,打招呼不知道说些什么没关系,但是看见了一定要热情,有礼貌,哪怕只是微笑一下。 - -41、讲述一些对你的发展贡献最大的事件或事情。请说说你从那些些事件或事故中学到了什么,并且是怎样把所学的知识应用到后来的实际工作中的? - ->【解答思路】:这里你所要讲的事情是你遇到的比较积极的事情,比如你看了一位专业成功人士的个人传记,从他的身上懂得了怎么刻苦学习、为人处事、和其他同事的关系以及对工作的狂热,从而运用到自身,把他的经验运用到自己的工作当中,少走了很多弯路,提高了工作效率。 - -42、从你的前任工作中,你所学到的最有意义的两到三件事是什么? - ->【解答思路】:你这里要说的就是工作心得或是工作经验,比如使你工作效率提高的例子 - -43、你是学生物的,为什么不去做生物和医药? - ->【解答思路】:这个问题提的很尖锐,迫使你不得不暴露自己在专业上的弱点,你可以这样回答:“我虽然学的是生物专业,但我更喜欢计算机,在校期间,我经常自学这方面的知识,而且两年前,就拿到高级程序员证书。这次又通过职业测评,咨询诊断结果我作销售比较合适,而且我性格开朗,亲和力强,所以,我认为我完全胜任贵公司计算机市场开发工作。”这样,既没有说到对生物不感兴趣,没有学好专业等缺欠,又把咨询师给的合理建议端出,使面试官引起重视。 - -44、如果上司对你提出了不公平的批评,你会怎么样? - ->【解答思路】:我觉得无论事情是由谁引起的,如何解决矛盾才是最重要的。因此,在我们都心平气和的时候,我会主动去找我的上司,用他能够接受的方式来表达我的想法。首先,我对他的批评中可能正确的部分表示接受然后再以咨询或征求意见的态度与上司进行沟通,这样矛盾也许会更容易解决一些。” - -45、“你有什么业余爱好?” - -> 【参考回答】:说起业余爱好,我还真不是很多,除了读书,我就比较喜爱打球,在大学的时候我是我们系篮球队的,我打的位置是中锋,不过最近几年因为工作的关系渐渐的打的少了 -【解答思路】: -1、业余爱好能在一定程度上反映应聘者的性格、观念、心态,这是招聘单位问该问题的主要原因。 -2、最好不要说自己没有业余爱好。 -3、不要说自己有那些庸俗的、令人感觉不好的爱好。 -4、最好不要说自己仅限于读书、听音乐、上网,否则可能令面试官怀疑应聘者性格孤。 -5、最好能有一些户外的业余爱好来“点缀”你的形象。 - -46、你最崇拜谁? - ->【参考回答】:我最崇拜马云。马云有着超乎常人的市场敏感度,以及对事物把握的精确度也相当的有造诣 -【解答思路】: -1、最崇拜的人能在一定程度上反映应聘者的性格、观念、心态,这是面试官问该问题的主要原因。 -2、不宜说自己谁都不崇拜。 -3、不宜说崇拜自己。 -4、不宜说崇拜一个虚幻的、或是不知名的人。 -5、不宜说崇拜一个明显具有负面形象的人。 -6、所崇拜的人人最好与自己所应聘的工作能“搭”上关系。 -7、最好说出自己所崇拜的人的哪些品质、哪些思想感染着自己、鼓舞着自己。 - -47、你的座右铭是什么? - -> 【参考回答】:只为成功找方法,不为失败找借口 -【解答思路】: -1、座右铭能在一定程度上反映应聘者的性格、观念、心态,这是面试官问这个问题的主要原因。 -2、不宜说那些医引起不好联想的座右铭。 -3、不宜说那些太抽象的座右铭。 -4、不宜说太长的座右铭。 -5、座右铭最好能反映出自己某种优秀品质。 - -48、谈谈你的缺点 - ->【参考回答】:要说缺点,我觉得我这个人有时候很固执,做一件事情的时候,总想按照预定的方向去把他做。 -【解答思路】: -1、不宜说自己没缺点。 -2、不宜把那些明显的优点说成缺点。 -3、不宜说出严重影响所应聘工作的缺点。 -4、不宜说出令人不放心、不舒服的缺点。 -5、可以说出一些对于所应聘工作“无关紧要”的缺点,甚至是一些表面上看是缺点,从工作的角度看却是优点的缺点。 - -49、谈谈你的家庭 - ->【参考回答】:我的父亲是做教师工作,母亲是做财会工作的,从小父母对我的学习情况就特别重视。 -【解答思路】: -1、了解求职者的性格、观念、心态等,这是该问题的主要作用 -2、简单地罗列家庭人口 -3、强调温馨和睦的家庭氛围 -4、强调父母对自己教育的重视 -5、强调各位家庭成员的良好状况 -6、强调家庭成员对自己工作的支持 -7、强调自己对家庭的责任感 - -50、你会在桌上放什么书? - -> 【参考回答】:我放一本好的字典在桌上,我也会放一本struck and white,这是一本好的参考书,能查找高中或大学里学过但又忘掉的语法知识,或者在遇到特别困难的情形也可使用。 -【解答思路】:招聘者可能有兴趣判定你对你的职业有多重视。一本好字典和好的参考书对于一个从事写作的人来说是手头必备书,你可能也会把喜爱的小说放到桌上。 - -51、作为被面试者给我打一下分 - ->【参考回答】:我觉得您很专业,如果说满分是10分的话 您最少能得9分… -【解答思路】:试着列出四个优点和一个非常非常非常小的缺点,(可以抱怨一下设施,没有明确责任人的缺点是不会有人介意的)。 - -52、告诉我一些有关你自己的但在你的简历上又没有反映出来的东西 - ->【参考回答】:你可能不知道我21岁就开始管理自己的网站,我认为你能理解我对投资销售方面的兴趣是重要的。在过去的2年里我平均每年获得12%的盈利。 -【解答思路】:不要只是重复简历上已有的东西,讲讲那些虽与你过去的从业经验无关但能反映你的个性和经验的独特的天赋或技能 - -53、谈谈你的星座、血型、八字 - -> 【解答思路】:类问题对于了解应聘者的性格、观念、心态等有一定的作用,这是招聘单位提出该问题的主要原因。回答时要说:“职业是人生最大事,要靠科学的职业生涯规划来科学定位,从而找到各阶段的发展平台。不能相信星座、血型、八字等学说,那样会贻误前途,赔上时间成本。” - -54、请讲一下这样一个经历:尽管其他人反对,但是你还是坚持自己的观点,并把事情继续做下去。 - -> 【解答思路】:从自己积极方面回答,比如家人和老师都希望我报考会计专业,而我对会计就是不感兴趣,毅然选择了动漫专业.我现在毕业,专业技能很强,而且有自己成熟的作品,动漫是我的事业,将继续做下去 - -55、你的一位领导脾气比较急,批评下属时常常不留情面,大家的工作情绪经常受到影响。作为职员,你该怎么办? - ->【解答思路】:首先对领导的批评应该认真接受,不能因为领导严厉的批评而产生逆反心理,以致影响工作; 其次可以私下找机会和领导沟通,向领导反映下属因此产生的意见和情绪,婉转地说明这种情绪可能会影响工作的正常开展,至于是否接受建议、改变方法,由领导自己决定。 - -56、在工作中你的同志不如你,你的工作很出色,而他找出了你的缺点向老板汇报。你将怎么样? - -> 【解答思路】:找机会与他沟通,谢谢他帮我找到了缺点,让我可以更加正确全面地认识自己。工作中积极改正缺点,更加精益求精。同时主动帮助他提高工作水平,大家相互学习、共同提高。 - -57、你和一个同学同入一个部门,作出的成绩相同。几年后他升迁了,你没动,如何想? - ->【解答思路】:一方面认真向他学习、向同事请教,并与自己进行对照,找出自己的不足,积极加以改正; 另一方面也要经常与领导和同事沟通,学会正常的人际交往,同时适时地表现自己,争取改变别人对自己的成见。 - -## 工作能力考察面试题 - -58、你了解我们所招聘的岗位吗? - ->【参考回答】:有过了解,网络管理员主要是从事公司的网络管理以及维护,包括电脑单机的维护维修,以及网络的周边设备....... -【解答思路】:毕业生针对这样的问题可以从岗位职责和对应聘者的要求两个方面谈起,很多毕业生在这样的问题面前手足无措,其实只要详细阅读单位的招聘信息即可。 - -59、对这项工作,你有哪些可预见的困难? - ->【参考回答】:工作中出现一些困难是正常的,也是难免的,但是只要有坚忍不拔的毅力、良好的合作精神以及事前周密而充分的准备,任何困难都是可以克服的。” -【解答思路】: -1、不宜直接说出具体的困难,否则可能令对方怀疑应聘者不行。 -2、可以尝试迂回战术,说出应聘者对困难所持有的态度 - -60、与上级意见不一是,你将怎么办? - ->【参考回答】:首先呢,作为一个员工我的是不会和上级产生争执的,如果真有意见不一致的时候,我想我会服从领导安排的。 -【解答思路】: -1、一般可以这样回答“我会给上级以必要的解释和提醒,在这种情况下,我会服从上级的意见。” -2、如果面试你的是总经理,而你所应聘的职位另有一位经理,且这位经理当时不在场,可以这样回答:“对于非原则性问题,我会服从上级的意见,对于涉及公司利益的重大问题,我希望能向更高层领导反映。” - -61、我们为什么要录用你? - ->【参考回答】:我符合贵公司的招聘条件,凭我目前掌握的技能、高度的责任感和良好的饿适应能力及学习能力 ,完全能胜任这份工作。我十分希望能为贵公司服务,如果贵公司给我这个机会,我一定能成为贵公司的栋梁! -【解答思路】: -1、应聘者最好站在招聘单位的角度来回答。 -2、招聘单位一般会录用这样的应聘者:基本符合条件、对这份共组感兴趣、有足够的信心 - -62、你能为我们做什么? - -> 【参考回答】:首先我应聘的是网络管理员,如果贵公司能够录用我的话,首先,我会对公司整个网络环境进行净化,对公司的网络安全状况进行加固………… -【解答思路】: -1、基本原则上“投其所好”。 -2、回答这个问题前应聘者最好能“先发制人”,了解招聘单位期待这个职位所能发挥的作用。 -3、应聘者可以根据自己的了解,结合自己在专业领域的优势来回答这个问题。 - -63、你希望与什么样的上级共事? - ->【参考回答】:做为刚步入社会新人,我应该多要求自己尽快熟悉环境、适应环境,而不应该对环境提出什么要求,只要能发挥我的专长就可以了。 -【解答思路】: -1、通过应聘者对上级的“希望”可以判断出应聘者对自我要求的意识,这既上一个陷阱,又上一次机会。 -2、最好回避对上级具体的希望,多谈对自己的要求。 - -64、你是应届毕业生,缺乏经验,如何能胜任这项工作? - ->【参考回答】:作为应届毕业生,在工作经验方面的确会有所欠缺,因此在读书期间我一直利用各种机会在这个行业里做兼职。我也发现,实际工作远比书本知识丰富、复杂。但我有较强的责任心、适应能力和学习能力,而且比较勤奋,所以在兼职中均能圆满完成各项工作,从中获取的经验也令我受益非浅。请贵公司放心,学校所学及兼职的工作经验使我一定能胜任这个职位。 -【解答思路】: -1、如果招聘单位对应届毕业生的应聘者提出这个问题,说明招聘单位并不真正在乎“经验”,关键看应聘者怎样回答。 -2、对这个问题的回答最好要体现出应聘者的诚恳、机智、果敢及敬业。 - -65、你为什么觉得自己能够在这个职位上取得成就? - ->【参考回答】:从我的经历来看,这是我的职业生涯中最适合我的一份工作。几年来,我一直在研究这个领域并且关注贵公司,一直希望能有这样的面试机会。我拥有必备的技能(简单讲述一个故事来加以说明),我非常适合这一职位,也确实能做好这份工作。 -【解答思路】:这是一个相当宽泛的问题,它给求职者提供了一个机会,可以让求职者表明自己的热情和挑战欲。对这个问题的回答将为面试人在判断求职者是否对这个职位有足够的动力和自信心方面提供关键信息。 - -66、你如何规划你个人的职业生涯 - ->【参考回答】:我对自己进行了一个长期的规划,以及4个短期的规划,现在这个阶段我主要是对自己的内职业生涯进行提升。 -【解答思路】:分长期和短期来进行回答。毕业生在求职前一定要对这样的问题有所考虑,并不仅仅是因为面试时可能被问到,对这个问题的思考有助于为个人树立目标。 - -67、你希望5年后达到什么成就? - ->【参考回答】: -A.做一天和尚敲一天钟,尽人事听天命、顺其自然。 -B.依我的机灵及才干,晋升到部门经理是我的中期目标。 -C.自己独当一面开公司。 -D.“全力以赴”是我的座右铭,希望能随着经验的增加,被赋予更多的职责及挑战。 -解答:最理想的回答是D。 - -68、为什么你认为你对该行业会保持长久的兴趣? - ->【参考回答】:撇开晋升的机会不说,该行业的技术变化得如此快,所以这里有广阔的就业机会。我尤其对多方面应用多媒体作为培训手段感兴趣。 -【解答思路】:你对该行业的发展前景有什么期望或设想?它使你不需晋升就可以获得发展吗?这家企业里什么使你最受鼓舞?你可以提供什么证据来证明你的兴趣来源于你极度的好奇——可能回到几年前的时间去找这个证据——而不是目前这些你可能放弃的一时兴致? - -69、5年之内你想处于什么位置? - ->【参考回答】:我希望有机会在工厂或国内办事处工作。我也希望通过管理一个小团体发展我的管理技能。 -【解答思路】:不要给出具体的时限或工作头衔。谈你喜欢的东西,你天生的技能,实际的问题和在你所选的领域或行业里你希望有什么机会,你希望从那些经验中学点什么。不要谈论你在那些与你所应聘的工作无关的领域或行业里的目标。这是听起来很明显的道理,但是很多求职者会犯这个错误。不经意间你就表现出了对当前的领域或行业缺乏真正的兴趣。不用说,一失言马上就会把你从进一步的考虑中淘汰掉。 - -70、描述你的理想职业 - ->【参考回答】:不管发生什么事情,我都愿意在与IT有关的领域里工作。和在大学中教学的工作相比,我还是对IT感兴趣。但我相信从事IT是我的天性,我擅长销售是因为我愿意花时间去教我的客户。现在我热切盼望我能培训那些新招聘进来的人。 -【解答思路】:谈及你喜欢的东西,你天生的技能,实际的问题或在这份特定工作或行业里你所期盼的机会,你希望从那些经验里学点什么东西。避免谈具体的时限和工作头衔。 - -71、如果你有无限的时间和经济来源,你会怎样使用它们呢? - ->【参考回答】:我希望能参加几次不针对金融专家的有关金融管理的行政研讨会。我还希望能让我的部门放长假,把每一个人都派去参加外界的一些活动。最后,我很可能去旅游并考察一下外国竞争者,同时一路享受当地的美食,您呢? -【解答思路】:虽然娱乐的事情谈起来很有诱惑力,但一定要紧扣工作或与行业相关的事务,或者紧扣与你应聘的这份工作的技能相关的努力上。例如,你正在应聘教书工作,你可能对义务教授成年人读书识字的项目感兴趣。这就证明了你对自己的工作领域的激情,即对教育重要性的一种信仰,即使是作为一种兴趣而毫无报酬都无所谓 - -72、告诉我你了解这家公司的什么 - ->【参考回答】:在这之前,我曾听朋友谈及过咱们公司,对咱们公司的大概情况进行了一个简单的了解。 -【解答思路】:描述你第一次或是近来邂逅该公司或其产品及其服务的情况。是什么促使你想在那里工作而不是在另一家不同的公司里做同样类型的工作呢?招聘者会仔细观察反映你真正兴趣的方面,而不仅是你对该公司所做的一些表面上的调查了解。把公司的年度报告背出来不大可能会给招聘者留下印象,但把来自顾客和员工的一些反馈意见说出来可能会给招聘者留下印象。 - -73、你是怎么知道我们招聘这个职位的呢? - ->【参考回答】:一个朋友告诉我的,他是咱们公司XX部门的,我们是大学同学。 -【解答思路】:如果你是从公司内部某人处打听回来的消息,记得提及他的名字,公司不说偏袒内部关系不代表它不存在。 - -74、这份工作哪些方面吸引你?哪些方面令你感到还不满意? - ->【参考回答】:首先,贵公司在业内是属于领军式的企业,我觉得这样一个企业是值得我去为之而奋斗的……… -【解答思路】:列举3个以上这份工作吸引你的因素;但,对于后一个问题给一个微不足道的小缺陷,轻描淡写,点到即止。 - -75、假如现在是你在我们公司做首次年度总结,我该告诉你一些什么呢? - ->【参考回答】:您要感谢我把工作完成得很好,并说明您渴望能够继续看到我工作的好成绩。更重要的是,我希望您能告诉我,您很欣赏我为一些重要项目加班加点工作的行为,还有我富有创造性的思维是如何有助于对存在的问题提出改革方案的。 -【解答思路】:很明显,在回答该问题时,你想给人留下积极的印象。“但愿您能更准时地出现”绝对不是一个好答案。记住,重点谈一两点你个人的优势。 - -76、为什么你想在这里工作? - ->【参考回答】:我几年前就错过了你们公司的一次招标,之后我意识到电脑产品变得越来越相近了,且零售价格的竞争愈趋激烈,以致服务成为了一家公司在竞争中脱颖而出的最好的方法。贵公司在所有的竞争者中享有最好的服务记录,而我相信从长远看,它将主宰这个行业。你的准备和调查研究工作应在这里明显表现出来。 -【解答思路】:给出一到两个你对该公司感兴趣的原因,并表明什么最激发你的兴趣。什么是你可以叙述来表明你个人对该公司的认识的最有说服力的事情呢?它的产品还是它的员工?答案包括公司的信誉、对该工作本身的描述,或者是跻身于该企业的欲望。 - -77、你从我们公司的顾客、员工,或者别人那儿了解了我们公司的一些什么情况? - ->【参考回答】:我确实给你们小册子上提到的几个客户打过电话。我与之交谈的顾客中有两人解释了他们为什么年复一年地购买你们的产品。你们的供销运作是很棒的,但还有没有一些可提高的服务项目呢? -【解答思路】:叙述通过你个人与公司代表们的接触后,你的兴趣又是如何增长的。为面试进行而富有创造性地思考。例如,在你面试之前,和一些零售商或公司生产线上别的供销点的工人交谈。他们能告诉你一些什么?给出一到两个你所了解到的例子来解释你为什么对这家公司感兴趣。什么是你可以用来证明你兴趣的最有力的例子? - -78、我们为什么要聘请你呢? - ->【参考回答】:“我在这个领域的经验很少人比得上,而且我的适应能力使我确信我能把职责带上一个新的台阶”。 -【解答思路】:这个问题听起来既消极又可疑,或者十分刻薄。但实际上,面试官是想让你帮助他聘用你。你拥有他们需要的才识,经验,能力和技能,他们要找的正是你。 - -79、你对加班的看法。 - ->【参考回答】:首先我想确认下,是何种性质的加班? 如果是我个人的工作量是在规定的时间内没有完成的话,这种情况是不会发生的,我是个注重工作效率的人。其次如果是公司业务量临时增加的话,我会接受加班。 -【解答思路】:首先,明确的告诉对方,如果是因为自己在规定的时间内没有完成工作任务的话 ,需要加班的情况是几乎不可能出现的,“我是个注重工作效率的人”其次,如果是因为公司业务情况或者其他的一些紧急工作的话是可以适应加班的。 - -80、经常需要出差的工作适合你的生活方式吗? - ->【参考回答】:对我或我的家庭来说,这种经常需要出差的顾问工作是完全没有问题的。不会在意出差的艰辛,相反我会以此为荣。我非常喜欢这份工作,我觉得自己更看重的是这一点。 - -81、就你的能力而言,如何让我相信你能够胜任这份工作? - ->【参考回答】:就我之前的工作经验而言,我觉得我能够胜任这份工作,网络管理员主要负责公司内部局域网的………,我在以前的单位,做的正是这些。 -【解答思路】:这个问题要从所应聘的岗位职责和要求入手,阐述你在这方面的实力 - -82、你的应变能力如何? - ->【参考回答】:举个例子:在06年年底, 这是个考察求职者的创造力和主动性的问题。举一个你如何改变计划或方向并且取得同样或更好效果的例子。你要侧重谈你怎样获得至关重要的信息或者是你如何改变个人作风获得与别人合作的机会 - -83、如果我们接受你,你会干多久呀? - ->【参考回答】:没让你愿意把一生中最宝贵的时光花费在不停地寻找工作当中;也不会有人愿意轻易放弃自己喜欢的工作。就拿这份工作来说,如果它能使我学以致用,更好地发挥我的潜能,而我也能从中学到更多的知识与技能,并且能得到相应的回报,那么我没有理由不专心致志地对待我所热爱的工作 - -## 团队面试题 - -84、团结是做好各项工作的基础。你如果是其中一员,对搞好本项目组团结有什么想法和打算? - ->【解答思路】: -1.要搞好一个组的团结,组长是关键。作为一名组员,要把维护和增强项目组团结作为协助组长工作的一项重要内容。 -2.正确处理本项目组组长成员之间的关系,积极搞好项目组的团结,为其他工作人员搞好团结作出表率。 -3.正确运用批评与自我批评的武器,及时纠正错误 - -85、你能给公司带来什么? - ->【解答思路】:一般外企很想知道未来的员工能为企业做什么,求职者应再次重复自己的优势,然后说:“就我的能力,我可以做一个优秀的员工在组织中发挥能力,给组织带来高效率和更多的收益”。外企喜欢求职者就申请的职位表明自己的能力,比如申请营销之类的职位,可以说:“我可以开发大量的新客户,同时,对老客户做更全面周到的服务,开发老客户的新需求和消费。”等等。 - -86、你认为怎样才算一个好的团队者? - ->【解答思路】:大家目标一致、齐心协力、互相学习、互相帮助、为团队贡献自己每一份力量,我认为这样的团队才是一个好的团队 - -87、怎样看待朋友越多越好? - ->【解答思路】:不可否认,就如社会上所言,朋友多了好走路。但我认为,交友要慎,要在本职工作的基础上交朋友。一是要多与领导和同事交朋友,多交流、多谈心,沟通思想,增进了解,互相支持,形成合力,其根本是加强团结,从而促进工作的顺利开展。同时时刻正确衡量自己,职人之长,补己之短。二是要多与工作联系单位的同志交朋友,以诚为本,以诚相待、以礼相待 - -## 工作主动性 - -88、你期望的工资是多少? - ->【解答思路】:企业的工资水平是很灵活的,何种能力拿何种工资。企业喜欢直率的人,但这个问题却不能正面回答,企业希望听到:“以我的能力和我的优势,我完全可以胜任这个职位,我相信我可以做得很好。但是贵公司对这个职位的描述不是很具体,我想还可以延后再讨论”。企业欢迎求职者给其定薪的自由度,而不是咬准一个价码。 - -89、就你申请的这个职位,你认为你还欠缺什么? - ->【解答思路】:企业喜欢问求职者弱点,但精明的求职者一般不直接回答。他们希望看到这样的求职者:继续重复自己的优势,然后说:“对于这个职位和我的能力来说,我相信自己是可以胜任的,只是缺乏经验,这个问题我想我可以进入公司以后以最短的时间来解决,我的学习能力很强,我相信可以很快融入公司的企业文化,进入工作状态。”企业喜欢能够巧妙地躲过难题的求职者。 - -90、说说你对行业、技术发展趋势的看法? - ->【解答思路】:企业对这个问题很感兴趣,只有有备而来的求职者能够过关。求职者可以直接在网上查找对你所申请的行业部门的信息,只有深入了解才能产生独特的见解。企业认为最聪明的求职者是对所面试的公司预先了解很多,包括公司各个部门,发展情况,在面试回答问题的时候可以提到所了解的情况,企业欢迎进入企业的人是“知己”,而不是“盲人”。 - -91、你认为你在学校属于好学生吗? - ->【解答思路】:企业的招聘者很精明,问这个问题可以试探出很多问题:如果求职者学习成绩好,就会说:“是的,我的成绩很好,所有的成绩都很优异。当然,判断一个学生是不是好学生有很多标准,在学校期间我认为成绩是重要的,其他方面包括思想道德、实践经验、团队精神、沟通能力也都是很重要的,我在这些方面也做得很好,应该说我是一个全面发展的学生。”如果求职者成绩不尽理想,便会说:“我认为是不是一个好学生的标准是多元化的,我的学习成绩还可以,在其他方面我的表现也很突出,比如我去很多地方实习过,我很喜欢在快节奏和压力下工作,我在学生会组织过××活动,锻炼了我的团队合作精神和组织能力。” 有经验的招聘者一听就会明白,企业喜欢诚实的求职者。 - -92、有人说“成功是对人有益的”,也有人说“失败是对人有益的”,你怎么看? - ->【解答思路】:成功是对努力的一种回报,一种肯定,能使人们认识到自身的价值,对自身是一种动力,能激发人们继续创新、学习的勇气!当然,成功是对人有益的。 “失败是对人有益的”,俗话说“失败乃成功之母”,它给予人们更多的是经验与坚韧顽强的精神和永不认输的斗志,所以说“失败是对人有益的”。 这类题的应对方法:辩证地看、联系地看,肯定一方但不否定另一方,两者是有机的统一。 - -93、你在办公室工作的时侯,有各类传真电话和公开电话打进来(咨询程序,有外地单位的人员要过来参观学习。。。具体事务记不清了),你如何处理这些电话? - ->【解答思路】:事情的轻重缓急和是否属于自己的职责范围内来处理。 -(1)职责范围内且能够解决的,自己解决。 -(2)职责范围内但不能解决的,请示解决。 -(3)职责范围外不能解决的,转交解决。 - -94、如何主持一次会议? - ->【解答思路】: -(1)确定主题,议题,准备好相关的材料。 -(2)确定时间、地点、参加人及应邀请的领导嘉宾。 -(3)安排议程,顺序,并安排好食宿。 -(4)经领导同意后发邀请函。 -(5)安排好会场(主席台,台标等)及报到处。 -(6)安排好主席人,报告人,发言人,及记录人员。 -(7)会后整理好资料,向领导做一次情况汇报。 - -95、如果我们单位录用了你,但工作一段时间却发现你根本不适合这个职位你怎么办 - ->【解答思路】:一段时间发现工作不适合我,有两种情况: -1.如果你确实热爱这个职业,那你就要不断学习,虚心向领导和同事学习业务知识和处事经验,了解这个职业的精神内涵和职业要求,力争减少差距; -2.你觉得这个职业可有可无,那还是趁早换个职业,去发现适合你的,你热爱的职业,那样你的发展前途也会大点,对单位和个人都有好处。 - -## 适应能力面试题 - -96、请谈谈你个人的最大特色。 - ->【解答思路】: -A.我人缘极佳,连续3年担任班委。 -B.我的坚持度很高,事情没有做到一个令人满意的结果,绝不罢手。 -C.我非常守时,工作以来,我从没有迟到过。 -D.我的个性很随和,是大家公认的好学生。 -这题理想的回答是B。A、C、D虽然都表示出应征者个性上的优点,但只有B的回答,最能和工作结合,能够与工作表现相结合的优点、特质,才是面谈者比较感兴趣的回答。 - -97、你是否愿意从基层做起? - ->【解答思路】:主要考察应聘者是否是一个踏实肯干而非眼高手低的人。职场新人可以表示自己愿意从基层开始锻炼自己 - -98、现代人际关系非常重要,你对此有何看法? - ->【解答思路】:现代人际关系就是搞好工作、实现自我价值的一个重要因素。如果人际关系不好,就会感到苦恼,一旦陷入苦恼中,还会有精力做好工作吗?所以我认为,要搞好工作,决不能忽视人际关系。 - -99、当某件事老是没有结果是,你该怎样做? - ->【解答思路】:首先自己换位思考把前因、过程分析一下,另外请教专业人士。如果自己做法错误马上改正,如果方法不对改变方式 - -100、你如何看待你所应聘的岗位? - ->【解答思路】:各个岗位在责任、权利、利益、分工、合作、技能、技巧等方面,都有明显的要求,而这往往是区别于其他岗位的,每个岗位都有其对员工的特殊性,在专业化日益增强的今天,“万金油”式的人越来越不被看好。所以,你切不可说“我能干这也能干那”,而应明晰在管理的半径、层面、空间上是有着很大差别的。有人往往在此时急求事功,在未详细了解岗位的具体要求情况下仓促应允,以为自己什么都干得了,这不明智,而且容易招致面试方反感。该岗位需要的所有内容,正是面试者想从你这里听到的,你也只需照此行事即可。 - -101、假如让你干一项工作,这个工作估计一周就能够完成。干了几天后,你发现,即使干上三周也没法完成这个任务。你该怎样处理这种情形?为什么? - ->【解答思路】:及时向领导反映,说明其真实情况,如果领导说可以延长那你把大概完成时间向领导汇报,但领导说不能延长时间,那你就请求领导增加人手,确保在要求时间内完成 - -102、 讲一个这样的经历:本来是你自己的工作,但别人却给你提供了很多帮助。 - ->【解答思路】:切记千万不要说因自己无能,别人来帮助;可以说时间紧或让提前完成,没有办法请他人帮助 - -## 信心面试题 - -103、你的好友怎样评价你? - ->【解答思路】:通过这个问题可以了解求职者的个性。这个问题看起来与求职者的潜能无关,但它反映了一种趋势,那就是企业倾向于雇用有高尚道德标准和高超技能的人。 我的朋友对我很重要。在与朋友的交往中,最重要的是,彼此之间有互相依赖的感觉。我们都很忙,并不能经常会面,但在我可以称为亲密朋友的几个人中,我们都知道,大家随时可以互相依赖。 - -104、上下级之间应该怎样交往? - ->【解答思路】:我认为,能在企业各个层面上清楚地进行交流,这对企业的生存至关重要。我认为自己已经在这个方面培养了很强的能力。从上下级关系来说,我认为最重要的是应该意识到每个人以及每种关系都是不同的。对我来说最好的方式就是始终不带任何成见地来对待这种关系的发展 - -105、在做口头表达方面你有哪些经验?你怎样评价自己的口头表达能力? - ->【解答思路】:这个问题旨在测评你的公共演讲能力,同时也可以了解你对演讲能力的自我评价 正确回答 我曾经看到一篇文章,说公共演讲是美国人最害怕的事情。我认识到,如果大多数人都害怕做公共演讲,那么在克服自己的恐惧并掌握口头陈述技能之后,我就能够在竞争中更胜一筹。因此我抓住所有的机会做演讲,而且我发现,做的演讲越多,就越对演讲感到轻松自如——当然也做得更好。 - -106、你怎样影响其他人接受你的看法? - ->【解答思路】:你的回答将告诉面试人,首先,你对影响别人有什么看法。其次,你影响别人的能力究竟有多大 这是多年来我一直非常努力探索的一个领域。对于好的想法,甚至是伟大的想法,人们有时并不接受。我现在认识到这样一个事实,那就是你表达想法的方式同想法本身一样重要。当我试图影响别人时,我一般会假设自己处在他们的位置上,让自己从他们的角度来看待问题。然后我就能够以一种更可能成功的方式向他们陈述我的想法。 - -107、你曾经参加过哪些竞争活动?这些活动值得吗? - ->【解答思路】:通过调查你经历过的实际竞争场景,可以反映你对竞争环境的适应程度,也可以反映你的自信心。当竞争成为关键因素时,正是讨论小组活动或企业业务的一个绝好机会。 我喜欢小组运动,我一直都尽我所能参加这些活动。我过去经常打篮球,现在有时候也打。同小组一起工作、为实现共同目标而努力、在竞争中争取胜利……这些事情确实非常令人兴奋 - -108、是否有教授或者咨询师曾经让你处于尴尬境地,还让你感到不自信?在这种情况下,你是怎样回应的? - ->【解答思路】:这个问题考查的是求职者在陌生领域工作的能力。通过这个问题,面试人可以了解到,当所给的任务超过自己目前的能力水平时,求职者解决问题的意愿和能力。 在我当学生的这几年中,我尽自己所能多学习知识,经常选择一些不熟悉的课程,因此往往会受到教授的质疑。不管什么时候,当我觉得自己对这个科目知之甚少时,我就尝试预见一些问题,为回答问题做些准备。当我被难住时,我尽可能做出科学合理的猜测,承认我不知道的东西,并且从不懂的地方开始学习。(如果可能,你可以举出一个例子……) - -109、你最大的长处和弱点分别是什么?这些长处和弱点对你在企业的业绩会有什么样的影响? - ->【解答思路】:这个问题的最大陷阱在于,第一个问题实际上是两个问题,而且还要加上一个后续问题。这两个问题的陷阱并不在于你是否能认真地看待自己的长处,也不在于你是否能正确认识自己的弱点。记住,你的回答不仅是向面试人说明你的优势和劣势,也能在总体上表现你的价值观和对自身价值的看法。长处来说,我相信我最大的优点是我有一个高度理性的头脑,能够从混乱中整理出头绪来。我最大的弱点是,对那些没有秩序感的人,可能缺乏足够的耐心。我相信我的组织才能可以帮助企业更快地实现目标,而且有时候,我处理复杂问题的能力也能影响我的同事。 - -110、你为什么觉得自己能够在这个职位上取得成就? - ->【解答思路】:这是一个相当宽泛的问题,它给求职者提供了一个机会,可以让求职者表明自己的热情和挑战欲。对这个问题的回答将为面试人在判断求职者是否对这个职位有足够的动力和自信心方面提供关键信息。 我的经历来看,这是我的职业生涯中最适合我的一份工作。几年来,我一直在研究这个领域并且关注贵公司,一直希望能有这样的面试机会。我拥有必备的技能(简单讲述一个故事来加以说明),我非常适合这一职位,也确实能做好这份工作。 - -111、除了工资,还有什么福利最吸引你? - ->【解答思路】:尽可能诚实,如果你做足了功课,你就知道他们会提供什么,回答尽可能和他们提供的相配。如果你觉得自己该得到更多,也可以多要一点。 - -## 灵活多变性面试题 - -112、有人说,善意的谎言是对的,你如何看? - ->【解答思路】:这个问题不能一概而论的,它仅仅动机是善意的,但是造成的后果好不好呢,如果反而引起更大的伤害,那么就得不偿失了;其次是对象,如果对象意志毅力很强,能够接受突如其来的打击,并且不喜欢别人骗他哪怕是善意的,那么善意的谎言便毫无意义,有时反而造成误会。但是善意的谎言在更多程度上都是对的可以接受的,它可以最大地减少不必要的痛苦,能够起到积极的作用。 - -113、为什么不讲一讲你个人的情况? - ->【解答思路】: -分析:一个好的面谈者很少这样直接地提出这个问题,通过随意的、友好的谈话也可以得到想了解的情况。在大多数情况下,面谈者会竭力地打探证明你不稳定或不可靠的信息 -回答对策:还有其它一些可能使某个雇主关注的问题,以上问题只是对某些性格的人的推测。这都是些不相关的问题,但是,如果雇主想以此来了解你可否可靠,你就得全力以赴地去应付了。要记住即使是随意地闲谈也要避免提及隐晦的问题。在回答个人情况时,要态度友好而且自信。 -回答样板:年轻、单身:“我没有结婚,即使结婚,我也不会改变做专职工作的打算,我可以把全部精力用在工作上。” 新搬来的:“我决定在Depression Culch长期居住下来,我租了一套公寓,搬家公司的六辆车正在卸家俱。” 抚养人:“我有个愉快的童年,我父母住的地方离我只需一小时汽车的路程,我一年去看他们几次。” 闲暇时间:“在我不去上班时,我主要呆在家里。我爱参加社区组织的活动,我每周都要在福利院参加活动。” - -114、领导要你4天完成一件工作,突然要你2天完成,你该怎么办? - ->【解答思路】: -1.首先分析一下提前完成工作的可能性。 -2.如果确定完不成的,那么去跟领导详谈,跟他讲道理摆事实,说明没法完成的理由。一定要有充足的理由,才能说服他。 -3.如果可以完成,但是需要其他条件的配合的,那么找领导说明情况。请领导给于支持。 -4.如果经过自己努力可以完成的,那么就努力完成吧。 - -115、你是怎么知道我们招聘这个职位的呢? - ->【解答思路】:你是从公司内部某人处打听来的消息,记得提及他的名字,公司不说偏袒内部关系不代表它不存在。 - -116、你为什么来应聘这份工作?(或为什么你想到这里来工作?) - ->【解答思路】:"我来应聘是因为我相信自己能为公司做出贡献,我在这个领域的经验很少人比得上,而且我的适应能力使我确信我能把职责带上一个新的台阶"应证者为了表明应征原因及工作意愿,回答时答案最好是能与应征公司的产品及企业相关的,最好不要回答:因为将来有发展性、因为安定等答案,要表现出有充分研究过企业的样子。 - -117、你为什么要找这样的职位?为什么是在这里 - ->【解答思路】: -分析:雇主想了解是否你是那种无论什么公司有活就行的人。果真如此,他或她就不会对你感兴趣。雇主想找那种想解决工作中问题的人。他们有理由认为这样的人工作起来更努力,更有效率,而那些想去特别的公司工作的人也是如此。 -回答对策:事先了解哪些工作适合你的技能和兴趣非常重要。要回答这个问题,就要谈到你选择工作目标的动机,那项工作要求的而你又具备的技能,各种专门培训,或与职务有关的教育证书。这个问题实际上有两方面的含意。一是为什么选择这个职位,二是为什么选择这个公司。如果你有选择这个公司的理由,或选择这个公司是你最大愿望,你就要准备回答为什么。如果可能的话,在面谈前,你要事先尽可能地对它进行了解。与别人联系得到详细的情报,或到图书馆查阅,看公司的年度报告,或任何能使你了解情况的方法都是必要的。 -回答样板:“我花费了很多时间考虑各种职业的可能性,我认为这方面的工作最适合我,原因是这项工作要求的许多技能都是我擅长的。举例来说,分析问题和解决问题是我的强项,在以前的工作中我能比别人更早发现和解决问题。有一次,我提出一项计划使得租借设备的退货率减少了15%,这听起来不算高,但是取得了年增长25000美元的好效益。而成本仅为100美元。目前你们公司似乎是能让我施展解决问题能力的地方。这个公司工作运行良好,发展迅速,善于接受新思想。你们的销售去年上涨了30%,而且你们准备引进几项大型新产品。如果我在这里努力工作,证实我自身的价值,我感到我有机会与公司共同发展。 - -118、你在大学期间最喜欢的老师是谁? - ->【解答思路】:有人曾答得很好:“教我们广告营销的教授,他能使课堂充满生气。通过实例让学生把知识和现实紧密结合,而不是死读课本,我想我从他身上得到的最多”。 - -119、你认为自己最大的弱点是什么? - ->【解答思路】:不要自作聪明的回答"我最大的缺点是过于追求完美",有的人以为这样回答会显得自己比较出色,但事实上,他已经岌岌可危了。 - -120、我们为什么要雇请你呢? - ->【解答思路】:有的面试只有这么一个问题。话虽简单,可是难度颇高。主要是测试你的沉静与自信。给一个简短、有礼貌的回答:“我能做好我要做得事情,我相信自己,我想得到这份工作”。根据自己的实际情况,好好想想把,看怎么说才具有最高说服力。 - -121、谈谈你对跳槽的看法? - ->【解答思路】: -1)正常的"跳槽"能促进人才合理流动,应该支持; -2)频繁的跳槽对单位和个人双方都不利,应该反对 - -122、如果你离开现职,你认为你的老板会有什么反应? - ->【解答思路】: -A.很震惊,因为老板对我算是很信赖,我就如同他的左右手一样。 -B.还好吧,他大概心里也有数,反正公司现在也不忙。 -C.他大概习惯了,反正他手下的人来来去去已是司空见惯。 -D.我想他一定会生气地破口大骂,他是一个相当情绪化的人。 -解答:最理想的回答是A。面谈者想借此了解你和前(现)任主管的相处情形,以及你在主管心目中的地位如何 - -## 持续学习面试题 - -123、你对新工作有何长短计划? - ->【解答思路】:参加新工作是件让人高兴的事情,我觉得在正式工作前对自己进行能力评估,定个长远计划来评比自己工作后的工作表现,可以激励自己更加进步。 第一,我的短期计划,尽快熟悉新单位的工作环境、工作节奏和认识同事,让自己能够很快上手工作。在工作过程中,虚心向同事学习,与同事融洽相处。 第二、我的长期计划,就是继续深造自己,更新自己的知识结构,学海无涯,不能够自满于现在的知识量。 - -124、你对人们追求时尚有何看法? - ->【解答思路】:时尚的产生是社会发展的必然,那么追求时尚也应该是一种必然。现代人生活条件好了,视野也宽了,追求时尚也就成了正常现象。但时尚有好坏之分,所以在追随的过程中要有分辨能力,不能盲目。作为年轻人应该追求一些对自身提高或对工作、事业有帮助的时尚,比如学英语,学计算机等等。同时,追求时尚不能脱离客观现实,更不能脱离自身所处的环境(包括身份)及经济条件的现实。 - -125、我想知道,遇到挫折你会怎么做? - ->【解答思路】: -1、要对挫折的原因进行分析,弄清楚是主观原因还是客观原因造成的,然后对症下药,用正确的方法解决它; -2、同时调整心态,必要时改变一下工作方法,使当前工作得以正常开展。 - -126、过去三年里,你为自我发展订立了什么样的目标?为什么要订立那样的目标? - ->【解答思路】:我的职业规划很清晰,就是要做一辈子建模师。我现在申请的职位就是建模师,我希望能把建模做好,成为建模高手,同时不断吸取新的知识 - -127、为了干这个工作,你都做了哪些准备? - ->【解答思路】: -1、基本原则上“投其所好”。 -2、回答这个问题前应聘者最好能“先发制人”,了解招聘单位期待这个职位所能发挥的作用。 -3、应聘者可以根据自己的了解,结合自己在专业领域的优势来回答这个问题。 - -128、你认为怎样的环境适合你?怎样的工作适合你? - ->【解答思路】:每个人都有自己的职业期望,这个期望值:首先是和自身的爱好和兴趣联系在一起的.作为一名xxx是我从小的梦想.其次要看这个职位能不能发挥自家的优势和专业知识,我觉得在这个专业对口职位上能充分发挥我的长处.人往高处走,水往低处流,往往进步意味着向上发展,取得一个更重要的角色. - -## 交际面试题 - -129、在和一个令你讨厌的人一起工作时,你是怎样处理和他在工作中的冲突的? - ->【解答思路】:既然在和一个令自己讨厌的人一起工作,没有办法让环境适应自己,只有主动的去适应环境,学着去发现他身上的优点,这样你就能够和他很愉快的共事,从不喜欢演变成喜欢。 - -130、你喜欢和什么样的人一起工作?为什么? - ->【解答思路】:我喜欢和工作认真,一丝不苟的人一起工作。因为这种人事业心强,无论做什么事情都想把它做得更好一些,我也是这种性格,因此与这些人一起工作有一种安全可靠感。我最不喜欢与那些混日子的人和那种把成绩归于自己、把错误归于别人的人一起工作,因为与这些人一起工作没有安全感,这些人的心思不是放在工作上,而是放在算计别人上。这些人过于自私,与这种类型的人在一起工作太累了,无法真正地沟通与合作。 - -131、在你以前的工作中,你发现和什么样的人最难处?为了和这种人共事,并使工作效率提高,你是怎样做的? - ->【解答思路】:考察你的交际能力,你要知道在这个社会生存,记住一点,是让你适应社会,不是让社会适应你!!具体来说,就是你要适合你公司的环境,其实有个这样的人在你身边,你应该觉得是好事,不是吗?你想啊,这个社会适者生存,这个社会需要强人,而强人不是天生就有的,是在后天的环境锻炼出来的,借这个机会你正好锻炼你的耐力和忍力,提高了自己的工作效率 - -132、你以前的经理做的哪些事情最令你讨厌了? - ->【解答思路】:对于你来说千万不要去评价与你共事过得老板的缺点,即使你知道也不要评价。如果你说了,就算你说的很好、很对也会给对方留下很不好得印象。举例:对于这个问题我想我没有办法回答你,首先我老板的资历和经验都比我丰富,凭我现在的水平没有资格去评价他,其次即使有些问题我觉得老板做得不好,原因也许是我和老板的立场和出发点都不一样,过了两三年我也会按老板的方法去处理 - -133、想想你共事过的老板,他们工作中各自的缺点是什么 - ->【解答思路】:对于你来说千万不要去评价与你共事过得老板的缺点,即使你知道也不要评价。如果你说了,就算你说的很好、很对也会给对方留下很不好得印象。举例:对于这个问题我想我没有办法回答你,首先我老板的资历和经验都比我丰富,凭我现在的水平没有资格去评价他,其次即使有些问题我觉得老板做得不好,原因也许是我和老板的立场和出发点都不一样,过了两三年我也会按老板的方法去处理 - -134、你认为这些年来同事对你怎么样? - ->【解答思路】:面试官问这个问题的目的,主要想从你的同事对你的态度和评价上推测你这个人是什么样的,对于你来说回答这个问题要谨慎。比如:同事对我都很热心(从侧面反衬你对同事也很热心);同事们有棘手的工作我会主动去帮助他们,所以我有事情的时候他们都来帮助我等等 - -135、讲一些你和你的老板有分歧的事例,你是怎样处理这些分歧的? - ->【解答思路】: -1、一般可以这样回答“我会给老板以必要的解释和提醒,在这种情况下,我会服从老板的意见。” -2、如果面试你的是总经理,而你所应聘的职位另有一位经理,且这位经理当时不在场,可以这样回答:“对于非原则性问题,我会服从上级的意见,对于涉及公司利益的重大问题,我希望能向更高层领导反映。” 例如:在公司的一次会议上,你和老板因为一件事情的处理方法产生了分歧。首先,在会议上暂时同意老板的说法,开玩会后私下找老板谈一下,列举你出你理由、看法,问老板是否这样会更好一点。 - -136、都说同事关系很难相处?你如何看待?如你被录用你如何和同事相处? - ->【解答思路】:首先要认识到大家同在一间办公室里工作,和睦相处形成一个和谐一致心情舒畅的工作环境是非常重要的,否则各个部门的正常工作可能都要受到干扰;其次 人都是有感情的,有可塑性的,是可以被说服的。因此和同事友好相处是完全可以做到的.;第三 和任何人相处都要尊重对方,只有尊重别人,别人才会尊重你.在办公室中应真诚的对待他人,有分歧时要多做沟通,不在背后传播谣言.;第四 要谦虚谨慎,自己刚进公司比较年轻,资历浅工作经验少,要本着请教学习的态度和同事交流,不恃才傲物,善于发现别人的优点,不苛求别人;第五 同事和同事又有不同的地方,有的工作性质和你不沾边,有的工作性质完全相同。对待后一种同事,除了正常相处之外还要注意一些问题,要有主动承担繁重工作的精神,有成绩不要自我夸耀,不计较个人的得失,有忍让精神等等.我相信通过自己的努力应该能够与同事友好相处 - -137、若你的经理让你告诉你的某位同事“表现不好就走人”,你该怎样处理这件事? - ->【解答思路】:面试者问这个问题目的在于,领导交与你一件你根本完不成或根本做不好的事情,你能否提出建议并拒绝。比如你可以说,经理这件事情交给我,我觉得有些不妥。第一我和他是同事关系,没有权威性,第二如果我去说了他也许会以为我在挑拨,不但没有起到一个好效果,反而适得其反······ - -138、我为什么要从这么多应聘者中选择你呢? ->【解答思路】:首先我符合你们的用人标准,同时我又具备一定的优势。第一:xxx优势;第二yyy优势 - -## 向面试官提问题 - -在面试结束前,大多数的主考官都会丢问题给求职者,最常见的就是:你有没有什么问题或疑问,想要提出来的?无论求职者是否有提出问题,其实,这个问题背后的真正含意,通常是主考官用来测试你对这份工作有多大的企图心、决心和热情。因此,如果你害怕发问不妥当,或是不知道该从何问起,甚至回答没有问题时,都很可能会让主考官认为,你想要这份工作的企图心、决心还不够强。相反的,求职者应该更积极、主动的利用面试最后一关的机会,适时的提出问题,这不但有助于主考官对你的印象能够加深,而且你也能趁此机会进一步了解这家公司的背景、企业文化是否适合你。最重要的是,如果能够在面试时,提出漂亮的问题,录取的机率将会大大提高。所以,无论如何,前往面试前,先谨记10个可以反问主考官的问题,以便到时候可以提出。 - -> 贵公司对这项职务的工作内容和期望目标为何?有没有什么部分是我可以努力的地方? - -> 贵公司是否有正式或非正式教育训练? - -> 贵公司的升迁管道如何? - -> 贵公司的多角化经营,而且在海内外都设有分公司,将来是否有外派、轮调的机会? - -> 贵公司能超越同业的最大利基点为何? - -> 在项目的执行分工上,是否有资深的人员能够带领新进者,并让新进者有发挥的机会? - -> 贵公司强调的团队合作中,其它的成员素质和特性如何? - -> 贵公司是否鼓励在职进修?对于在职进修的补助办法如何? - -> 贵公司在人事上的规定和作法如何? - -> 能否为我介绍一下工作环境,或者是否有机会能参观一下贵公司? - -至于薪水待遇、年假天数、年终奖金、福利措施等问题,有些公司的主考官在面试时,会直接向求职者提出。如果对方没有提及,对社会新鲜人来说,在找第一份工作时,比较不适合提出,除非你有对方不得不录取你的条件.另外,也有人在结束前,谦虚的请教主考官:您认为我今天的表现如何?录取的机率有多大?通常,这个问题也会让对方认为,你对这份工作抱有很大的决心和企图心,而你也可以试着从对方的回答中,约略猜测出自己成功的机率有多大,并且作为下一次面试时表现的参考! - -## 自我介绍范文 - -### 实实在在、诚诚恳恳的范文 - -> 我叫赵婉君,您可能会联想起琼瑶小说,字的确就是那两个字,差别就是人没有那么漂亮,呵呵。其实,我的同学更都喜欢称呼我的英文名字,叫June,六月的意思,是君的谐音。(点评:寥寥数语,让面试官轻松地记住了申请人的名字。) - -> 我来自广东的恩平市,可能您没有去过,是一个很小的县级市,这几年刚刚开发了温泉业,我想将来会有更多的人了解这个小城市。(点评:提到温泉,面试官对恩平这个陌生的地方一定会产生一点亲切感。) - -> 在2003年我以恩平市全市第一名的成绩考上了中山大学,学的是计算机科学专业。不过,在中大,我没法再像高中一样总是名列前茅了,到目前为止,我的综合学分排名是40%左右。在专业课程方面,我C++的编程能力比较强,一年以前就开始自学Java,在班级里是最早开始学Java的。(点评:诚实可信地阐述了自己的学习能力。) - -> 我参与过我们老师领导的一个项目,叫做LAN聊天室,我负责开发了其中的及时通信系统的编写。在我们班,老师只挑选了我一个女生参与这个项目,主要是我写程序的效率比较高,态度也非常认真。(点评:如果你的编程能力不如男生强,至少你要告诉面试官, 我比多数女生要强。如果贵公司考虑选择一名女生,那么,选择我吧!) - -> 除了学习和项目实习以外,我在学生会工作了两年,第一年做干事,第二年被提升为秘书长。大家对我的评价是考虑问题很周全,令人放心。(点评:稳重周全,是做技术支持工程师的一个完美形象。) - -> 在我的求职清单上,IBM是我的首选单位,原因和您面试过的很多同学都一样,出于对大品牌的信赖。毕竟,大品牌公司意味着很多我们需要的东西,比如培训和薪资,能和优秀的高素质的人在一起工作等等。(点评:对于IBM这种一流的企业,没有必要过多地解释自己为什么想来应聘,点到为止。) - -> 技术支持工程师也刚好是我的首选职位,因为我有技术背景,也有作为女性和和客户沟通的天然优势。还有,我不担心频繁出差,因为我身体素质很好,我已经坚持晨跑两年多了。在IBM专业技术方面,我信赖公司的培训体系和我自己的快速学习能力!希望能有机会加入IBM团队!(点评:除了强调自己的优点,也告诉面试官,不必把自己当作一个弱女子,而是要当作一个长跑健将!) - -### 互动性强、以情动人的范文 - -> 大家好,我叫张一凡,首先我想衷心感谢各位领导冒着大雨从深圳赶到华工,给我们一次宝贵的面试机会。面试机会对于我们应届毕业生来说,绝对就像今天的天气,春雨贵如油;或者说,贵如石油!(点评:在自我介绍之前,利用当天的天气表示对招聘单位的感激之情,是非常高明的一个互动环节。) - -> 在华工,我所学的专业是工商管理,和刚才做自我介绍的张优和李秀同学相比,我非常遗憾地说,自己的学习成绩只是中等水平,原因主要有两方面,一是华工的确人才济济, 二是我本人把相当一部分精力投入到了兼职工作中,因为我确实需要兼职的收入。我累计担任了八名中小学生的数学与英语家教,参加了二十多次校园促销,比如雅芳化妆品促销、卡西欧电子字典促销等等。销售工作锻炼了我的勇气和耐力,我相信这两种素质将会对我未来的工作很有帮助!(点评:第二部分,和其他同学相比,她的成绩中等甚至是下等是块硬伤,如果不自己把这块硬伤揭开,面试官会觉得如鲠在喉。主动暴露自己的弱点,则表现出一凡的勇敢与诚实,也巧妙地使面试官把注意力转移到她的兼职经验上,化弱势为优势!) - -> 今天我来申请中海石油的"商务代表"一职,说实话,除了张经理刚刚介绍过的工作内容之外,我并不十分清楚具体的工作要求是什么。在此,我谨浅显地谈谈我个人的理解,我想,这个职位需要这样一个人:第一,她要了解并热爱石油行业;第二,她要形象端正, 表达能力强,思维严谨;第三,作为应届毕业生,她必须具备良好的学习心态和踏踏实实的工作态度。对于这三点,我自信自己能够满足,我希望自己能够有机会进入下一轮面试,届时再详细地向您阐述。最后,再次表示我的感谢!(鞠躬)(点评:大学生对于就业岗位往往一知半解,如果不懂装懂反而引人反感。实话实说,为自己又赢一分!不过大家绝对不要盲目抄袭此答案,说自己不了解所申请的职位是相当危险的招数,用不好等于引火烧身。一凡并非真正不了解这个职位,一来她说自己上网做过调查,二来她的理解基本正确,她用的是以退为进的方法。 ) - -### 优缺结合、诚实诚恳的范文 - -申请人概况:一个有三门不及格历史的男生 - -> 您好,我叫郭竞,竞赛的竞。我生在珠海长在珠海,所以希望毕业后依然在珠海工作。 (点评:利用家乡做一个快速的互动。)作为一名应届毕业生,我认为自己在三个方面比较有优势。第一个优势是我的硬件和网络知识实践经验。我大一、 大二的时候组织了一个校园计算机服务工作室,专门给同学们提供装机和网络维护服务。去年,我担任了一间小公司的兼职网络管理员,月薪500元。我对待工作非常负责,不亚于全职员工。有两次网络DOWN机,我连续24小时不睡觉,直到把问题解决。我认为我的第二个优势是编程能力比较强。在学校的模拟MIS项目中,我被教授任命为组长。我在金蝶公司工作过两个月,参与了一个50万元大项目的二次开发,编写数据库。第三个优势是我对IT技术十分狂热!我9岁的时候就有了自己的第一台电脑,初高中时期三次在珠海市中学生编程大赛的获奖,在大学我的计算机专业课成绩向来名列前茅。非常遗憾的一点是,虽然我的计算机专业课成绩非常好,但是我曾经在有三门非专业课考试不及格。究其原因,是我在大一、 大二的时候完全按照自己的兴趣分配学习时间,我把几乎所有时间都给了计算机。回头看来,这是我大学生活最失败的地方,它已经给我带来了非常严重的后果,有一些企业一看到我有不及格的科目立刻把我的求职申请淘汰掉了。工商银行能够给我这次面试的机会,让我非常地感动,也体会到我们人性化的招聘标准。所以,无论今天我是否能够面试成功,我都要先说一句,谢谢您给我这次面试机会!(点评:用人单位对应届毕业生的成绩还是相当看重的,越是有名的企业就越是如此。郭竞所申请的工商银行恰恰就要求应届毕业生提供自己的成绩单,所以他在自我介绍当中把"不及格"这块硬伤解释成"在计算机课程上花了太多时间",确实很高明,把缺点巧妙地转化成了优点。) - -### 数字说话、知己知彼的范文 - -申请人概况: 有工作经验的申请人 - -> 我叫卫洁华,洁净的洁,中华的华,我的同学一般叫我阿华,或者是我的英文名字Berry。我是广州本地人,不过父母是汕头人,所以我既会讲粤语也会讲潮汕话。(点评:介绍家乡的时候顺便带出自己的语言优势。)我毕业于广州大学市场营销专业,在校期间我曾经两次获得奖学金,两次被评为优秀学生干部,还得过一次全勤奖。(点评:已经毕业了两年的申请人一般不会提到自己在大学期间所获得的奖励,但是"全勤奖"当然是值得一提的。)毕业后的第一年,我在中山市的一家电子公司担任前台兼秘书,主要负责接听电话、整理文件等常见的文秘工作。之所以离开这里,是因为工作实在太清闲了,每天的工作只用小半天就能干完。我是一个比较喜欢忙碌的人,只有忙碌一点,才觉得心里很踏实。(点评:既说明了离职原因,也暗示了自己的优点,可谓一箭双雕。)我的第二份工作,也就是我现在在广州乐士医药公司的工作,就相当忙碌。乐士是一间从事中药药材批发的民营公司,每年的销售额大概有五千多万,大大小小的客户有几百家。我所在的客户服务部现在有八名客服代表,每个人手上都有几十家客户。我的工作职责包括打单、配货、制作销售跟踪报表、催收货款、处理客户投诉等等。在去年年底人力资源部做的满意度调查中,我获得了4.5分的销售代表满意度和4.2分的客户满意度,满分是5分,这个分数在当时的九名客户服务代表中名列第二。(点评:用事实说话,用数字说话,打动人的自我介绍一定有很多的数字。)我现在来到汽巴求职,原因有两个。第一个原因是, 我一直向往着能加入世界一流的大公司。第二个原因是,我觉得自己很符合汽巴的招聘要求。虽然我并没有做过精细化工产品的客服,但是药品行业对客服的要求是相当高的,因为药品在包装、运输、进出库和销售等各个环节都有着非常严格的要求。此外,我了解到汽巴所使用的是ORACLE公司的ERP系统,它和我长期使用的和佳ERP是很类似的,而且我每天都用英文版的ERP。(点评:自我介绍的结尾,必须提到所应聘的单位,可以再次陈述自己对该单位的浓厚兴趣,也可以强调自己的优势。)我就介绍这么多吧。 - -### 亮点突出、言简意赅的范文 - -申请人概况: 有工作经验的申请人 - -> 我叫萧峰,山峰的峰。我是山东人,身材很"山东",性格也很"山东",呵呵。(点评: 如果面试官是个矮小的男子,这个身材很"山东"的幽默就必须删除,否则就是把自己的快乐建立在人家的痛苦之上喽!)2000年我从兰州铁道学院毕业,所学的专业叫火车头,但实际上我们的学习内容是用计算机软件来设计火车头。(点评:申请技术类岗位的申请人,如果能略微幽默一些,立刻就会和别的候选人拉开档次。)毕业后我首先从事了两年的火车头设计工作,然后从2002年开始转行从事软件开发工作。从2002到2004年,我在徐州天润软件公司工作近两年。估计您没有听说过这家公司,天润是一个专业性很强的公司,是在几年以前由几个博士后创办起来的,我们主要服务的对象就是铁路系统。我最初担任的是C++程序员,在工作了三个月之后被升职为项目经理,主要负责到客户所在单位进行驻点式软件实施。由于我带的小组实施速度比较快,我带队负责了公司三个合同额最大的项目,总计合同额约占公司全年营业额的30%。(点评:以数字来说明自己的能力永远是最上乘的自夸技巧。)2004年8月,我被一个朋友推荐到广东蓝天软件公司担任项目经理。在蓝天工作的两年时间里,我最大的成就是,我成功解决了两个已经实施了一年半之久的烂尾项目。其中一个已经验收完毕,给公司收回了15万元的软件款。我个人的体会是,软件实施人员,一定要"以柔克刚",对待客户的态度要"柔",但是对一些不合理的客户化要求要"刚",否则项目实施起来就没完没了,公司根本就赚不到钱了。(点评:实施慢、收款难,是令软件公司最头痛的地方,强调了自己在这方面的优势一定会引起用人单位的兴趣。)我有几个很好的朋友在神州数码工作,他们都很鼓励我过来,所以我今天来到这里与您面试。我的自我介绍就做到这儿吧。(点评:提到自己的朋友,会令招聘方产生一种"自己人"的亲切感。) - - -## 优缺点范文 - -### 优点范文 - -> 我比较善于发现问题、解决问题。比方说,我们公司有鼓励员工提建议的制度,在两年的工作中我提出了60多条改善提案,80%被采用,总积分在我科533人中居第三。除了技术上的改善提议,我也提了很多管理方面的小建议,比方说工作时间播放轻音乐,厕所的灯从三盏减少为一盏等等。 - -> 我擅长合理地安排时间,作为助理,我的杂事很多,总是觉得手边有做不完的事情,所以我特别注意时间管理,这样才能高效地工作,而不至于搞得焦头烂额。比方说,我在办公室和家里的墙上各自贴一张"当月时间表",随时添加工作和生活上的安排,比如两周后的销售会议呀,朋友的生日之类,这样我就可以提前很长时间把生活和工作都安排得井井有条,不会遗漏重要的事情,我今天带来了我以前的"当月时间表"。平时我也很注意有效利用零碎时间,比如我习惯随身带一本口袋书,等公车的时候看。上个月我就读了《走出华为》这本书,看得我特别激动。 - -> 我是一个做事全力以赴的人, 或者说是一个比较拼命的人。一旦确定了一个目标, 我会用上自己全部的力量, 直到成功, 或者说即使失败了也要不留遗憾。比方说, 上大学的时候我为了做实验可以连续两天不睡觉。前一段时间我为了做一个紧急项目连续两个月呆在柳州一天也没有休息。为了今天和您的这场面试, 我提前做了两个星期的功课。(点评: 面试官往往会追问你做了些什么功课, 那么你就可以开始阐述自己对产品的了解啦。) - -> 我比较有条理。上大学时别的同学都喜欢借我的笔记, 我的个人物品和工作文件都很有规律, 我不在办公室的时候, 同事们很容易就能找到他们想要的文件, 包括电脑上的文件。我觉得有条理是一种习惯, 只要坚持每个人都可以做到。 - ->我做事很有计划性,我的信条是那句谚语:`If you fail to plan, you plan to fail`.要么做计划,要么就失败。每天我的第一件事就是列计划, 把当天要做的事情分为两类:必须完成的,最好能完成的。我买东西之前一定会列购物清单, 所以我几乎从来不会在超市里面瞎逛。职业发展方面我也有明确的计划,从大二开始我就决定毕业后要从事市场和销售工作,所以选修的课程主要是市场类的,也尽量去做一些相关的兼职。 - -> 我最大的优点是爱思考,爱动脑筋,总想把事情做得更好。我从高中的时候就开始用"联想法"来记数字,比如我把英文单词philosophy联想成"废话 唆废话",把马克思的生日联想成"(一)个(巴)掌(一)个(巴)掌(捂)一(捂)“代表1818年5月5日, 把朋友的电话82822012联想成"爸儿爸儿,儿领一儿”。当然现在有很多书宣传这种 “联想式记忆法”,不过我觉得只有自己的联想才印象最深刻。在大学做兼职也是一样,虽然只是兼职, 但是我一定会想办法取得最好的效果。(点评:面试官应该会要求举例说明,那么就可以给出准备好的例子。)比方说,大二时我曾经给一个MP3销售商做过派单员,就是在校园里发小广告,我和我们宿舍的另外一名同学连续发了两个晚上,但是我发现同学们根本不在乎这种街头小广告,往往随手就扔到垃圾桶甚至扔到地上,既浪费又不环保,而且完全没有带动销售。我和同学商量了一个晚上之后,给老板打电话报告了这种情况,建议他改成张贴到厕所的方式进行宣传,我们都知道大学生厕所是宣传宝地(笑),而且我建议他不要按天付给我们钱,而是按照提成的方式,每卖出一台给我们提10%,这样我们就有动力随时关注我们的厕所广告有没有被覆盖。事实证明效果很不错,那个学期他卖了几十个MP3呢。 - -> 我比较善于带领别人一起工作。我担任过班长,学生会干部,从无到有创建了系青年志愿者组织,还创建了一个很大的IT社团萤火虫俱乐部。同学们都说和我一起工作干劲很高,我想最主要的原因是因为我善于调动每个人的积极性。我觉得一个人无论才能大小, 都希望能肩负一定的责任,所以在团队工作中,我十分重视让每个人都能展示自己的才能。 - -> 我觉得我最大的优点是比较符合儒家思想,呵呵,仁、义、礼、智、信,可能一下子就是五个优点了。通俗点说,善良正直、值得信任。在科龙的博士后工作站呆了两年多,同事和领导对我的评价都是"尽心尽责"。我还从前年开始捐助希望工程,其实一年才1,000块钱就可以帮一个孩子上学了,呵呵。(点评:我对这个博士后学员至今留有深刻的印象, 他在新东方上完面试英语培训班后不久,就被"抢"到一间著名的国际制冷公司, 年薪是三十万人民币。他又一次验证了一个理论:一个成功的人,首先是一个好人。) - -> 我的优点是适应性很强,我从东北来到广州读书,很快就喜欢上这个城市,交了很多广东的朋友。我在银行工作两年以来,调换了三次工作岗位,每一次都能很快适应新的岗位要求。(点评:面试官很可能会问,为什么你适应性这么强?那么则要继续阐述。)环境改变的时候,首先要积极学习,所谓自助者天助。其次, 更为重要的是要学会寻求帮助,不懂就问,不能怕丢脸。 - ->我的优点是表达能力强,而且乐于表达自己的观点。上学的时候在课堂上我总是踊跃发言,还参加过演讲比赛。在工作岗位上,我敢于表达自己的观点。我认为敢于表达自己的观点是对工作负责的一种表现,如果我们爱一份工作,一定会献计献策让它更好。 - -> 我的一个特点是比较活跃。我很少安静地呆在家里看一整天电视,我喜欢出去,与人打交道。所以我选择从事销售工作,出差对我来说不是苦差事,而是乐事。 - -> 我的求知欲望比较强,有好奇心。从小到大我一直喜欢读书,小时候就喜欢把东西拆开看个究竟,现在工作了也特别喜欢钻研新技术。对我来说,钻研技术既是工作也是乐趣。    -   -> 我的优点是对工作很有热情,老板和同事都说我看上去老是干劲十足的样子,呵呵。我确实很喜欢工作中忙忙碌碌的感觉。有时候事情少了,我会主动找事做,去年我申请了公司内部讲师资格,参与培训新员工,其实并不额外拿钱,但是我很喜欢做,觉得挺有成就感。 - -> 我的优点是敏感度比较高,通俗一点说,比较善于观察环境和察言观色吧。我觉得这是做销售的基本素质。去年,我曾经建议我的一个客户进一批NBA卡片,结果他一个月就赚了上万块钱,其实就是我在网上跟外甥聊天获得的灵感。还有一次,我约一个很熟的客户去吃饭,约了两次他都推说有事儿,我感觉到肯定出了什么问题,所以第二天立刻出差去拜访他。果然,有一家新的体育品牌提出和他合作,要占他一半的店面。我马上搜集了很多这个品牌的负面新闻和负面案例,让我的客户最终打消了这个念头。(点评: 面试官很可能会接着问:那么现在的流行趋势是什么?)  -  -> 我的朋友们都说,我解决问题的能力很强,往往可以把比较难办的问题处理得比较妥当。朋友们有难处的时候经常征求我的意见,连我父母都愿意找我商量事儿,呵呵。举个工作当中的例子吧,我上个暑假兼职做审计的时候发现项目经理犯了一个比较原始的错误, 她把一个数字的小数点位置搞错了。其实本来我可以直接告诉她的,但不巧的是她当天上午刚刚因为一个同事的数字错误发了一通脾气,如果我直接指出她的错误, 她可能会比较尴尬。所以我想了个更加委婉的办法,我给她发了个EMAIL,没有只是报告这个小数点的问题,而是同时还问了几个专业问题。这样她既改正了数字错误,还给我讲了一大堆专业知识,维护了她的专业威信。  -  -> 我动手能力很强,说俗一点,就是手很巧,呵呵。家里水电气有些小毛病的话我基本上都能搞定,大学里我的实验课成绩非常好。我还擅长计算机网页设计,这是我自己设计的作品。所以我相信我应聘这份工作有一定的优势, 因为我们每天都需要和设备打交道。 - -> 我具备开创性思维。我给朋友的生日卡片都是手工制作的,我做的菜也很受家人欢迎。我们班里有一半的晚会都是我的创意,比如说当时搞的一个"模拟面试大赛"特别成功, 后来其他班全部都效仿这个比赛。 - -> 我的知识面比较广,这主要是由于我爱好阅读。我每周都会租几本书来看,读书能提高我的个人素质,对我的工作帮助极大。(点评:HR的下一个问题很可能是:那就说说你读过的书吧?)比方说,我前段时间读了新东方徐小平老师写了一本书叫《骑驴找马》,里面讲述了一些关于跳槽换工作的故事,很有趣也很有启发性。他把换工作的人比喻成骑驴找马的人,大部分人由于生出了找马的念头,就不再理会座下的驴了,不给草吃不给水喝甚至虐待驴, 结果呢, 马一看到你曾经把驴虐待成这样,全都吓跑了。这个比喻说的是,如果不好好对待现有的工作,不在现有的工作中做出成绩,其实是很难找到好马的。 - -> 我是一个责任感很强的人,不论是对学习,对工作,还对别的事情。比如说,我看到小偷偷东西一定会喊,绝对不会熟视无睹!如果我看到公共场所的水龙头坏了,我一定会打电话,甚至亲自去找到相应的负责人来修理。工作中也是一样,如果我没有很好地完成工作,就会感到很内疚。(点评:感动啊!我们热爱向贼发出怒吼的女士,和敢于同贼搏斗的男士!) - -> 我是一个持之以恒的人。我从大学开始写日记一直坚持到现在, 我也一直坚持健康饮食和早晨锻炼的习惯, 我在一个社团工作了三年, 而不是像很多同学那样尝试了很多社团。工作上我也希望自己能够在一个好公司里长远发展, 而不是换来换去。 - -> 我的第一个优点是认真。我一直以认真为荣,每当我做一项工作时,总是竭尽全力去把它做好。我的父母就是做事认真的人,他们从小就经常向我灌输认真的生活态度。我认为,只要是值得做的事情,不管大少,都应该全力以赴,认真做好。实习时,只要是值得做的事,不管大小,都应该全力以赴,认真做好。实习时,经理曾让我负责收发工作。那是一项极为简单的工作,但我却做得一丝不苟。每天我都提前来到办公室,把当天的信件整理归类,我把所有的邮件分成“急件”、“非急件”、“报纸期刊”及“其他”几大类,在大家到达办公室时把它们井然有序地分放在有关人士的办公桌上。由于平时留心观察,有时碰到与某位同事的研究课题相关或有参考价值的信息,我也会细心收集并及时送到他们手中,以便对他们的工作有所帮助。大家对我的收发工作赞不绝口。在实习后期,我被任命为经理助理。我的第二个有点就是具有责任感。凡事我答应的事,不管有多难,有一定会努力做到。事实上,朋友们都说,我是他们最信得过的朋友。” - -### 缺点范文 - -> 我的公开演讲能力比较差,在公共场合讲话的时候我会感到紧张,不过谈论我熟悉的领域我会比较放松。所以当我需要做公开发言的时候,我必须要准备得很充分。我确实羡慕那些无论什么话题都能够高谈阔论的人。 - -> 作为经理我有一个缺点,心肠太软。尽管好心肠可以被说成是一个优点,但是作为经理人这是一个不小的缺点,因为管理的确需要一些强硬手段。和别的组长相比,由于我比较"软",所以我的员工纪律性稍差,比如说开会迟到。当然,"软"也有它的优点,我的员工很乐于与我沟通, 所以工作效率会提高。 - -> 我觉得我的一个缺点是说话太多,总急于表达自己的观点,我同学说我有些好为人师, 爱出风头。我的确注意到,由于自己说得太多,就会忽略听别人的意见。所以我在笔记本上写了一句话: “少说多听”!结果有些滑稽,即使我觉得自己说得很少,我的同事也会说:"哦,你挺活跃的!"印象是很难改变的。 - -> 我有的时候做事情宏观有余,细节不足。有时犯一些低级的错误,比方说把打字的时候把2005年打成2004年,丢东西什么的。去年我和同学一起策划迎新晚会的时候,我忘记了最后检查一次麦克风,结果演出半小时之前发现麦克风失灵,引起了很大的恐慌。所以我特别喜欢和注重细节的人在一起,能从他们身上学到很多东西。 - -> 我有时候急于求成,或者说做事爱急躁。一旦接手一个任务,总是想要尽快把它赶完, 总觉得做完了一件事情心里才舒服。但是,欲速则不达,太追求efficiency,就会牺牲accuracy。我现在总是提醒自己accuracy第一位,efficiency第二位,这样会好得多。 - -> 我有时候会设立不切实际的目标,比方说一个月要减肥10公斤,一个小时把表格打出来。我觉得,设立不切实际的目标对我个人来讲的确不是一件坏事, 所谓`Aim at the Sun, land on the Moon`.(向着太阳飞,至少落在月亮上。) 有了高目标就会有很大的动力, 即使完不成也无所谓。但是, 当我在一个团队里工作的时候,这就变成了一个很大的缺点。在团队里,一旦目标定得太高,就会引发很多管理上的问题。我想这是我需要克服的一个缺点。 - -> 我觉得我最大的弱点是还没有学会时刻以最佳状态来工作。我现在一天八个小时在银行做柜员,要面对大量客户,要时刻以最佳状态服务每一个客人真的特别难。但是,如果不这样,就很容易丧失机会。我去年冬天就有过这样一个教训。当时我因为家里人生病心情不太好,所以在有个客户来咨询的时候就显得不是特别热情,只是公事公办地给了他一些资料。结果呢,他第二天来办理业务的时候没有来找我,而是换了一位看上去特别可亲的柜员,当时就买了二十万块钱的利得盈(理财产品)。如果我没有把个人情绪带到工作中,他肯定会成为我的客户!这件事情给我的教训很大,我想,不愉快的情绪是有连锁效应的,一旦把生活当中的不愉快带进工作,立刻就会产生工作中的不愉快! - -> 我觉得我有时候会过分在意别人的感受,比方说,不敢直接表达不同意见,因为觉得会让对方丢面子,其实这样做很不利于快速有效地开展工作。我希望自己能够逐渐学会更加爽快,对人对事更加直接。 - -> 我的缺点是工作需要压力,在有要求、有竞争的时候我效率更高。我的学习成绩很好, 因为有考试的压力。但是在大学里面,我没有做太多的兼职活动,因为没有赚钱的压力。 - -> 我的缺点是容易受到别人的影响,比方说,大家都学习我也就学得很拼命,大家纷纷找兼职,我也开始做兼职。我发现自己有从众的心理,呵呵,所以,如果能加入腾讯就好了,腾讯里有很多优秀人才,我就可以受到好的影响了。 \ No newline at end of file diff --git a/Java/alibaba-java-coding-guidelines-1.md b/Java/alibaba-java-coding-guidelines-1.md deleted file mode 100644 index ff99a95..0000000 --- a/Java/alibaba-java-coding-guidelines-1.md +++ /dev/null @@ -1,562 +0,0 @@ -# 阿里巴巴Java开发手册——编程规约 - - -## Java开发手册版本更新说明 - -|版本号|版本名|更新日期|备注| -|:------:|:------:|:------:|:------:| -|1.3.0|终极版|2017.09.25|单元测试规约,IDE代码规约插件| -|1.3.1|纪念版|2017.11.30|修正部分描述| -|1.4.0|详尽版|2018.05.20|增加设计规约大类,共16条| -|1.5.0|华山版|2019.06.19|详细更新见下面| - -本笔记主要基于`华山版`(1.5.0)的总结。华山版具体更新如下: -- 鉴于本手册是社区开发者集体智慧的结晶,本版本移除阿里巴巴`Java开发手册`的限定词`阿里巴巴` -- 新增21条新规约。比如,`switch`的NPE问题、浮点数的比较、无泛型限制、锁的使用方式、判断表达式、日期格式等 -- 修改描述112处。比如,`IFNULL`的判断、集合的`toArray`、日志处理等 -- 完善若干处示例。比如,命名示例、卫语句示例、`enum`示例、`finally`的`return`示例等。 - -## 专有名词解释 -1. `POJO`(Plain Ordinary Java Object): 在本手册中,POJO专指只有setter、getter、toString的简单类,包括DO、DTO、BO、VO等。 -2. `GAV`(GroupId、ArtifactctId、Version): Maven坐标,是用来唯一标识jar包。 -3. `OOP`(Object Oriented Programming): 本手册泛指类、对象的编程处理方式。 -4. `ORM`(Object Relation Mapping): 对象关系映射,对象领域模型与底层数据之间的转换,本文泛指ibatis, mybatis等框架。 -5. `NPE`(java.lang.NullPointerException): 空指针异常。 -6. `SOA`(Service-Oriented Architecture): 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用,有利于提升组件可重用性,可维护性。 -7. `IDE`(Integrated Development Environment): 用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具,本《手册》泛指 IntelliJ IDEA 和eclipse。 -8. `OOM`(Out Of Memory): 源于java.lang.OutOfMemoryError,当JVM没有足够的内存来为对象分配空间并且垃圾回收器也无法回收空间时,系统出现的严重状况。 -9. `一方库`:本工程内部子项目模块依赖的库(jar包)。 -10. `二方库`:公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar包)。 -11. `三方库`:公司之外的开源库(jar包)。 -## 一、 编程规约 -### (一) 命名风格 -***正例:*** -- 国际通用的名称,可视同英文;`alibaba` / `youku` / `hangzhou` 等 -- 类名使用`UpperCamelCase`风格,但以下情形例外:`DO` / `BO` / `DTO` / `VO` / `AO` / `PO` / `UID` 等。如:`UserDO` / `XmlService` / `TcpUdpDeal` -- 方法名、参数名、成员变量、局部变量都统一使用`lowerCamelCase`风格,必须遵从驼峰形式;`localValue` / `getHttpMessage` / `inputUserId` -- 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。`MAX_STOCK_COUNT` / `CACHE_EXPIRED_TIME` -- 抽象类命名使用`Abstract`或`Base`开头;异常类命名使用`Exception`结尾;测试类命名以它要测试的类的名称开始,以`Test`结尾 -- 类型与中括号紧挨相连来表示数组,定义整形数组 ```int[] arrayDemo;``` -- `包名`统一使用`小写`,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用`单数`形式,但是`类名`如果有复数含义,`类名可以使用复数形式`。包名`com.alibaba.ai.util`,类名为`MessageUtils`(此规则参考`spring`的框架结构) -- 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。在JDK中,表达原子更新的类名为:`AtomicReferenceFieldUpdater` -- 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。如:`startTime` / `workQueue` / `nameList` / `TERMINATED_THREAD_COUNT` -- 如果模块、接口、类、方法使用了`设计模式`,在命名时需体现出具体模式(将设计模式体现在名字中,有利于阅读者快速理解架构设计理念)。如: `class OrderFactory` / `class LoginProxy` / `class ResourceObserver` -- 接口类中的方法和属性不要加任何修饰符号(`public`也不要加),保持代码的简洁性,并加上有效的`Javadoc`注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。接口方法签名`void commit();`,接口基础常量`String COMPANY = "alibaba";` -- 对于`Service`和`DAO`类,基于`SOA`的理念,暴露出来的服务一定是接口,内部的实现类用`Impl`的后缀与接口区别。如`CacheServiceImpl`实现`CacheService`接口 -- 如果是`形容能力`的`接口`名称,取对应的形容词为接口名(通常是–`able`的形容词)如 `AbstractTranslator`实现`Translatable`接口 -- 枚举类名带上`Enum`后缀,枚举成员名称需要`全大写`,单词间用`下划线`隔开。(说明:枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有。)枚举名字为`ProcessStatusEnum`的成员名称:`SUCCESS` / `UNKNOWN_REASON` -- 各层命名规约: -> **A) Service/DAO 层方法命名规约** -> 1. 获取单个对象的方法用`get`做前缀。 -> 2. 获取多个对象的方法用`list`做前缀,复数形式结尾如:`listObjects`。 -> 3. 获取统计值的方法用`count`做前缀。 -> 4. 插入的方法用`save`/`insert`做前缀。 -> 5. 删除的方法用`remove`/`delete`做前缀。 -> 6. 修改的方法用`update`做前缀。 - -> **B) 领域模型命名规约** -> 1. 数据对象:`xxxDO`,xxx即为数据表名。 -> 2. 数据传输对象:`xxxDTO`,xxx为业务领域相关的名称。 -> 3. 展示对象:`xxxVO`,xxx一般为网页名称。 -> 4. POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。 - -***反例:*** -- 不能以下划线或美元符号开始、结束,如:~~_name~~、~~$name~~、~~name_~~ -- 严禁使用拼音与英文混合的方式,如:~~DaZhePromotion~~[打折]、 ~~getPingfenByName()~~ [评分] -- 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可读性降低。 -- 杜绝完全不规范的缩写,避免望文不知义。`AbstractClass`“缩写”命名成~~AbsClass~~,`condition`“缩写”命名成~~condi~~,此类随意缩写严重降低了代码的可阅读性。 -- 接口类中的方法和属性不要加任何修饰符号(public也不要加) ~~public abstract void f();~~ -- POJO类中`布尔类型`变量都不要加`is`前缀,否则部分框架解析会引起`序列化错误`。 -> 定义为基本数据类型`Boolean isDeleted`的属性,它的方法也是`isDeleted()`,RPC框架在反向解析的时候,“误以为”对应的属性名称是`deleted`,导致属性获取不到,进而抛出异常。 - -### (二) 常量定义 -- 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中 ~~String key =``"Id#taobao_"`` + tradeId;~~ -- 在`long`或者`Long`赋值时,数值后使用大写的`L`,不能是小写的`l`,小写容易跟数字1混淆,造成误解。~~Long a = 2l;~~ -- 不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。正例:缓存相关常量放在类`CacheConsts`下;系统配置相关常量放在类`ConfigConsts`下。 -- 如果变量值仅在一个固定范围内变化用`enum`类型来定义。 - -### (三) 代码格式 -- 采用4个空格缩进,禁止使用`tab`字符。 -- 注释的双斜线与注释内容之间有且仅有一个空格。 -- 在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开`int second = (int)first + 2;` -- IDE的text file encoding设置为UTF-8;IDE中文件的换行符使用Unix格式,不要使用Windows格式。 -- 单个方法的总行数不超过80行 -- 不同逻辑、不同语义、不同业务的代码之间插入`一个空行`分隔开来以提升可读性(说明:`任何情形,没有必要插入多个空行进行隔开`。) -### (四) OOP 规约 -- 避免通过一个类的`对象引用`访问此类的`静态变量`或`静态方法`,无谓增加编译器解析成本,直接用类名来访问即可 -- 所有的覆写方法,必须加`@Override`注解 -- 外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加`@Deprecated`注解,并清晰地说明采用的新接口或者新服务是么。 -- 不能使用过时的类或方法。 -- Object的`equals`方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals,``"test".equals(object);``【推荐使用 java.util.Objects#equals(JDK7 引入的工具类】 -- 所有整型包装类对象之间值的比较,全部使用`equals`方法比较。【在-128至127这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断】 -- 定义数据对象`DO`类时,属性类型要与数据库字段类型相匹配。数据库字段的`bigint`必须与类属性的`Long`类型相对应。 -- 为了防止精度损失,禁止使用构造方法`BigDecimal(double)`的方式把`double`值转化为`BigDecimal`对象(在精确计算或值比较的场景中可能会导致业务逻辑异常)。`BigDecimal g = new BigDecimal(0.1f);`实际的存储值为:`0.10000000149`。正例:优先推荐入参为`String`的构造方法,或使用`BigDecimal`的`valueOf`方法,此方法内部其实执行了`Double`的`toString`,而Double的`toString`按`double`的实际能表达的精度对尾数进行了截断。 -```java -BigDecimal recommend1 = new BigDecimal("0.1"); -BigDecimal recommend2 = BigDecimal.valueOf(0.1); -``` -- 关于基本数据类型与包装数据类型的使用标准如下:`1)【强制】所有的POJO类属性必须使用包装数据类型。2)【强制】RPC方法的返回值和参数必须使用包装数据类型。3) 【推荐】所有的局部变量使用基本数据类型。`【说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。`正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。反例:比如显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务,调用不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线。所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。`】 -- 定义 DO/DTO/VO等`POJO`类时,不要设定任何属性`默认值`。【反例:POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。】 -- 序列化类新增属性时,请不要修改`serialVersionUID`字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改`serialVersionUID`值。(说明:注意serialVersionUID不一致会抛出序列化运行时异常。) -- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在`init`方法中。 -- `POJO`类必须写`toString`方法。使用IDE中的工具:source> generate toString时,如果继承了另一个POJO类,注意在前面加一下`super.toString`。【说明:在方法执行抛出异常时,可以直接调用 POJO 的 `toString()`方法打印其属性值,便于排查问题】 -- 禁止在`POJO`类中,同时存在对应属性xxx的`isXxx()`和`getXxx()`方法。【说明:框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到】 -- 使用索引访问用String的`split`方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛`IndexOutOfBoundsException`的风险 -- 当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,此条规则优先于下一条 -- 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法。 -- 在`getter/setter`方法中,不要增加业务逻辑,增加排查问题的难度 -- 循环体内,字符串的连接方式,使用`StringBuilder`的`append`方法进行扩展 -- `final`可以声明类(不允许被继承的类,如`String`类)、成员变量(不允许修改引用的域对象)、方法、以及本地变量(不允许运行过程中重新赋值的局部变量),避免上下文重复使用一个变量,使用final可以强制重新定义一个变量,方便更好地进行重构 -- 慎用`Object`的`clone`方法来拷贝对象,对象`clone`方法默认是浅拷贝,若想实现深拷贝需覆写`clone`方法实现域对象的深度遍历式拷贝。 -- 类成员与方法访问控制从严。`1)如果不允许外部直接通过new来创建对象,那么构造方法必须是private。2)工具类不允许有public或default构造方法。3)类非static 成员变量并且与子类共享,必须是protected。 4)类非static成员变量并且仅在本类使用,必须是private。5)类static成员变量如果仅在本类使用,必须是private。 6)若是static成员变量,考虑是否为final。7)类成员方法只供类内部调用,必须是 private。8)类成员方法只对继承类公开,那么限制为protected。`【说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦】 -- `浮点数`之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals 来判断。【浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式】 -```java -// 反例 -float a = 1.0f - 0.9f; -float b = 0.9f - 0.8f; -if (a == b) { // 预期进入此代码快,执行其它业务逻辑 -// 但事实上 a==b 的结果为 false -} -Float x = Float.valueOf(a); -Float y = Float.valueOf(b); -if (x.equals(y)) { // 预期进入此代码快,执行其它业务逻辑 -// 但事实上 equals 的结果为 false -} - -// 正例 - -// (1)指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的 -float a = 1.0f - 0.9f; -float b = 0.9f - 0.8f; -float diff = 1e-6f; - -if (Math.abs(a - b) < diff) { - System.out.println("true"); -} - -// (2)使用BigDecimal来定义值,再进行浮点数的运算操作 -// BigDecimal构造的时候注意事项 见上文 -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -BigDecimal c = new BigDecimal("0.8"); - -BigDecimal x = a.subtract(b); -BigDecimal y = b.subtract(c); - -if (x.equals(y)) { - System.out.println("true"); -} -``` -### (五) 集合处理 -- 关于`hashCode`和`equals`的处理,遵循如下规则:1)只要覆写`equals`,就必须覆写`hashCode`。2)因为`Set`存储的是不重复的对象,依据`hashCode`和`equals`进行判断,所以`Set`存储的对象必须覆写这两个方法。3)如果自定义对象作为`Map`的键,那么必须覆写`hashCode`和`equals`。【说明:`String`已覆写`hashCode`和`equals`方法,所以我们可以愉快地使用`String`对象作为`key`来使用】 -- `ArrayList`的`subList`结果不可强转成`ArrayList`,否则会抛出`ClassCastException`异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList【说明:`subList`返回的是`ArrayList`的内部类`SubList`,并不是`ArrayList`而是`ArrayList`的一个视图,对于`SubList`子列表的所有操作最终会反映到原列表上】 -- 使用`Map`的方法`keySet()/values()/entrySet()`返回集合对象时,不可以对其进行添加元素操作,否则会抛出`UnsupportedOperationException`异常 -- `Collections`类返回的对象,如:`emptyList()/singletonList()`等都是immutable list,不可对其进行添加或者删除元素的操作【反例:如果查询无结果,返回 `Collections.emptyList()`空集合对象,调用方一旦进行了添加元素的操作,就会触发`UnsupportedOperationException`异常。】 -- 在`subList`场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生`ConcurrentModificationException`异常 -- 使用集合转数组的方法,必须使用集合的`toArray(T[] array)`,传入的是类型完全一致、长度为0的空数组【反例:直接使用toArray无参方法存在问题,此方法返回值只能是Object[]类,若强转其它类型数组将出现ClassCastException错误。】 -```java -// 正例 -List list = new ArrayList<>(2); -list.add("行无际"); -list.add("itwild"); -String[] array = list.toArray(new String[0]); -/* -说明: -使用toArray带参方法,数组空间大小的length: -1)等于0,动态创建与size相同的数组,性能最好 -2)大于0但小于size,重新创建大小等于size的数组,增加GC负担 -3)等于size,在高并发情况下,数组创建完成之后,size正在变大的情况下,负面影响与上相同 -4)大于size,空间浪费,且在size处插入null值,存在NPE隐患 -*/ -``` -- 在使用`Collection`接口任何实现类的`addAll()`方法时,都要对输入的集合参数进行NPE判断 【说明:在`ArrayList#addAll`方法的第一行代码即`Object[] a = c.toArray();`其中c为输入集合参数,如果为null,则直接抛出异常。】 -- 使用工具类`Arrays.asList()`把数组转换成集合时,不能使用其修改集合相关的方法,它的`add/remove/clear`方法会抛出`UnsupportedOperationException`异常【说明:`asList`的返回对象是一个`Arrays`内部类,并没有实现集合的修改方法。`Arrays.asList`体现的是适配器模式,只是转换接口,后台的数据仍是数组。】 -```java -String[] str = new String[] { "it", "wild" }; -List list = Arrays.asList(str); - -// 第一种情况:list.add("itwild"); 运行时异常 -// 第二种情况:str[0] = "changed1"; 也会随之修改 -// 反之亦然 list.set(0, "changed2"); -``` -- 泛型通配符``来接收返回的数据,此写法的泛型集合不能使用add方法,而``不能使用get方法,作为接口调用赋值时易出错。【说明:扩展说一下`PECS`(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用``。第二、经常往里插入的,适合用``】 -> 这个地方我觉得有必要简单解释一下(`行无际`本人的个人理解哈,有不对的地方欢迎指出),上面的说法可能有点官方或者难懂。其实我们一直也是这么干的,不过没注意而已。举个最简单的例子,用`泛型`的时候,如果你`遍历`(`read`)一个List,你是不是希望List里面装的越具体越好啊,你希望里面装的是`Object`吗,如果里面装的是`Object`那么你想想你会有多痛苦,每个对象都用`instanceof`判断一下再`类型强转`,所以这个方法的参数List主要用于`遍历`(`read`)的时候,大多数情况你可能会要求里面的元素最大是`T`类型,即用``限制一下。再看你往List里面`插入`(`write`)数据又会怎么样,为了灵活性和可扩展性,你马上可能就要说我当然希望List里面装的是`Object`了,这样我什么类型的对象都能往List里面写啊,这样设计出来的接口的灵活性和可扩展性才强啊,如果里面装的类型太靠下(假定`继承层次从上往下`,父类在上,子孙类在下),那么位于上级的很多类型的数据你就无法写入了,这个时候用``来限制一下最小是`T`类型。下面我们来看`Collections.copy()`这个例子。 - -```java -// 这里就要求dest的List里面的元素类型 不能在src的List元素类型 之下 -// 如果dest的List元素类型位于src的List元素类型之下,就会出现写不进dest -public static void copy(List dest, List src) { - //....省略具体的copy代码 -} - -// 下面再看我写的测试代码就更容易理解了 -static class Animal {} - -static class Dog extends Animal {} - -static class BlackDog extends Dog {} - -@Test -public void test() throws Exception { - - List dogList = new ArrayList<>(2); - dogList.add(new BlackDog()); - dogList.add(new BlackDog()); - - List animalList = new ArrayList<>(2); - animalList.add(new Animal()); - animalList.add(new Animal()); - - // 错误,无法编译通过 - Collections.copy(dogList, animalList); - - // 正确 - Collections.copy(animalList, dogList); - - // Collections.copy()的泛型参数就起作到了很好的限制作用 - // 编译期就能发现类型不对 -} -``` -- 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行`instanceof`判断,避免抛出`ClassCastException`异常。 -```java -// 反例 -List generics = null; - -List notGenerics = new ArrayList(10); -notGenerics.add(new Object()); -notGenerics.add(new Integer(1)); - -generics = notGenerics; - -// 此处抛出 ClassCastException 异常 -String string = generics.get(0); -``` -- 不要在`foreach`循环里进行元素的`remove/add`操作。`remove`元素请使用`Iterator`方式,如果并发操作,需要对Iterator对象`加锁` -```java -// 正例 -List list = new ArrayList<>(); -list.add("1"); -list.add("2"); - -Iterator iterator = list.iterator(); -while (iterator.hasNext()) { - String item = iterator.next(); - if (删除元素的条件) { - iterator.remove(); - } -} - -// 反例 -for (String item : list) { - if ("1".equals(item)) { - list.remove(item); - } -} -``` -- 在JDK7版本及以上,`Comparator`实现类要满足如下三个条件,不然`Arrays.sort`,`Collections.sort`会抛`IllegalArgumentException`异常【说明:三个条件如下 1)x,y 的比较结果和 y,x 的比较结果相反。2)x>y,y>z,则x>z。 3) x=y,则x,z比较结果和y,z 比较结果相同。】 -```java -// 反例:下例中没有处理相等的情况 -new Comparator() { - @Override - public int compare(Student o1, Student o2) { - return o1.getId() > o2.getId() ? 1 : -1; - } -}; -``` -- 集合泛型定义时,在JDK7及以上,使用diamond语法或全省略。【说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型】 -```java -// 正例 -// diamond 方式,即<> -HashMap userCache = new HashMap<>(16); -// 全省略方式 -ArrayList users = new ArrayList(10); -``` -- 集合初始化时,指定集合初始值大小【说明:`HashMap`使用`HashMap(int initialCapacity)`初始化。】 -> 正例:`initialCapacity` = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即`loader factor`)默认为0.75,如果暂时无法确定初始值大小,请设置为16(即默认值)。 - -> 反例:`HashMap`需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大,`resize`需要重建`hash`表,严重影响性能。 -- 使用`entrySet`遍历`Map`类集合KV,而不是`keySet`方式进行遍历。【说明:`keySet`其实是遍历了2次,一次是转为`Iterator`对象,另一次是从`hashMap`中取出 `key`所对应的`value`。而`entrySet`只是遍历了一次就把`key`和`value`都放到了entry中,效率更高。如果是JDK8,使用`Map.forEach`方法。】 -> 正例:`values()`返回的是V值集合,是一个list集合对象;`keySet()`返回的是K值集合,是一个Set集合对象;`entrySet()`返回的是`K-V`值组合集合。 -- 高度注意`Map`类集合`K/V`能不能存储`null`值的情况,如下表格: - -|集合类|Key|Value|Super|说明| -|-----|----|-----|-----|----| -|Hashtable|不允许为null|不允许为null|Dictionary|线程安全| -|ConcurrentHashMap|不允许为null|不允许为null|AbstractMap|锁分段技术(JDK8:CAS)| -|TreeMap|不允许为null|允许为null|AbstractMap|线程不安全| -|HashMap|允许为null|允许为null|AbstractMap|线程不安全| -> 反例:由于`HashMap`的干扰,很多人认为`ConcurrentHashMap`是可以置入`null`值,而事实上,存储`null`值时会抛出`NPE`异常。 -- 合理利用好集合的有序性(`sort`)和稳定性(`order`),避免集合的无序性(`unsort`)和不稳定性(`unorder`)带来的负面影响。【说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如:`ArrayList`是 `order/unsort`;`HashMap`是`unorder/unsort`;`TreeSet`是`order/sort`。】 -- 利用`Set`元素唯一的特性,可以快速对一个集合进行去重操作,避免使用`List`的`contains`方法进行遍历、对比、去重操作 - -### (六) 并发处理 -- 获取单例对象需要保证线程安全,其中的方法也要保证线程安全【说明:资源驱动类、工具类、单例工厂类都需要注意】 -- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯 -```java -// 正例:自定义线程工厂,并且根据外部特征进行分组,比如机房信息 -public class UserThreadFactory implements ThreadFactory { - private final String namePrefix; - private final AtomicInteger nextId = new AtomicInteger(1); - // 定义线程组名称,在 jstack 问题排查时,非常有帮助 - UserThreadFactory(String whatFeaturOfGroup) { - namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-"; - } - - @Override - public Thread newThread(Runnable task) { - String name = namePrefix + nextId.getAndIncrement(); - Thread thread = new Thread(null, task, name, 0, false); - System.out.println(thread.getName()); - return thread; - } -} -``` -- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程【说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题】 -- 线程池不允许使用`Executors`去创建,而是通过`ThreadPoolExecutor`的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 -> 说明:`Executors`返回的线程池对象的弊端如下: - -> 1)`FixedThreadPool`和`SingleThreadPool`: -允许的请求队列长度为`Integer.MAX_VALUE`,可能会堆积大量的请求,从而导致`OOM`。 - -> 2)`CachedThreadPool`: -允许的创建线程数量为`Integer.MAX_VALUE`,可能会创建大量的线程,从而导致`OOM`。 -- `SimpleDateFormat`是线程不安全的类,一般不要定义为`static`变量,如果定义为`static`,必须加锁。【说明:如果是JDK8的应用,可以使用`Instant`代替`Date`,`LocalDateTime`代替`Calendar`,`DateTimeFormatter`代替`SimpleDateFormat`,官方给出的解释:`simple beautiful strong immutable thread-safe`。】 -```java -// 正例:注意线程安全。亦推荐如下处理 -private static final ThreadLocal df = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd"); - } -}; -``` -- 必须回收自定义的`ThreadLocal`变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的`ThreadLocal`变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量使用`try-finally`块进行回收 -```java -// 正例 -objectThreadLocal.set(userInfo); -try { - // ... -} finally { - objectThreadLocal.remove(); -} -``` -- 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁【说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用`RPC`方法。】 -- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。【说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。】 -- 在使用阻塞等待获取锁的方式中,必须在`try`代码块之外,并且在加锁方法与`try`代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在`finally`中无法解锁 -> 说明一:如果在`lock`方法与`try`代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。 - -> 说明二:如果`lock`方法在`try`代码块之内,可能由于其它方法抛出异常,导致在 `finally`代码块中,`unlock`对未加锁的对象解锁,它会调用`AQS`的`tryRelease`方法(取决于具体实现类),抛出`IllegalMonitorStateException`异常。 - -> 说明三:在`Lock`对象的`lock`方法实现中可能抛出`unchecked`异常,产生的后果与说明二相同 -```java -// 正例 -Lock lock = new XxxLock(); -// ... -lock.lock(); -try { - doSomething(); - doOthers(); -} finally { - lock.unlock(); -} - -// 反例 -Lock lock = new XxxLock(); -// ... -try { - // 如果此处抛出异常,则直接执行 finally 代码块 - doSomething(); - // 无论加锁是否成功,finally 代码块都会执行 - lock.lock(); - doOthers(); -} finally { - lock.unlock(); -} -``` -- 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同 -```java -// 正例 -Lock lock = new XxxLock(); -// ... -boolean isLocked = lock.tryLock(); -if (isLocked) { - try { - doSomething(); - doOthers(); - } finally { - lock.unlock(); - } -} -``` -- 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用`version`作为更新依据【说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。】 -- 多线程并行处理定时任务时,`Timer`运行多个`TimeTask`时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,如果在处理定时任务时使用`ScheduledExecutorService`则没有这个问题 -- 资金相关的金融敏感信息,使用悲观锁策略。【说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。】 -- 使用`CountDownLatch`进行异步转同步操作,每个线程退出前必须调用`countDown`方法,线程执行代码注意catch异常,确保`countDown`方法被执行到,避免主线程无法执行至`await`方法,直到超时才返回结果【说明:注意,子线程抛出异常堆栈,不能在主线程`try-catch`到。】 -- 避免`Random`实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一`seed`导致的性能下降【说明:`Random`实例包括`java.util.Random`的实例或者`Math.random()`的方式。正例:在JDK7之后,可以直接使用API`ThreadLocalRandom`,而在JDK7之前,需要编码保证每个线程持有一个实例】 -- 在并发场景下,通过双重检查锁`(double-checked locking)`实现延迟初始化的优化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),推荐解决方案中较为简单一种(适用于JDK5及以上版本),将目标属性声明为`volatile`型。 -```java -// 注意 这里的代码并非出自官方的《java开发手册》 -// 参考 https://blog.csdn.net/lovelion/article/details/7420886 -public class LazySingleton { - // volatile除了保证内容可见性还有防止指令重排序 - // 对象的创建实际上是三条指令: - // 1、分配内存地址 2、内存地址初始化 3、返回内存地址句柄 - // 其中2、3之间可能发生指令重排序 - // 重排序可能导致线程A创建对象先执行1、3两步, - // 结果线程B进来判断句柄已经不为空,直接返回给上层方法 - // 此时对象还没有正确初始化内存,导致上层方法发生严重错误 - private volatile static LazySingleton instance = null; - - private LazySingleton() { } - - public static LazySingleton getInstance() { - // 第一重判断 - if (instance == null) { - synchronized (LazySingleton.class) { - // 第二重判断 - if (instance == null) { - // 创建单例实例 - instance = new LazySingleton(); - } - } - } - return instance; - } -} - -// 既然这里提到 单例懒加载,还有这样写的 -// 参考 https://blog.csdn.net/lovelion/article/details/7420888 -class Singleton { - private Singleton() { } - - private static class HolderClass { - // 由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次 - final static Singleton instance = new Singleton(); - } - - public static Singleton getInstance() { - return HolderClass.instance; - } -} -``` -- `volatile`解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。【说明:如果是`count++`操作,使用如下类实现:`AtomicInteger count = new AtomicInteger();` `count.addAndGet(1);`如果是JDK8,推荐使用`LongAdder`对象,比`AtomicLong`性能更好(减少乐观锁的重试次数)。】 -- `HashMap`在容量不够进行`resize`时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险 -- `ThreadLocal`对象使用`static`修饰,`ThreadLocal`无法解决共享对象的更新问题【说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量】 - -### (七) 控制语句 -- 当`switch`括号内的变量类型为`String`并且此变量为外部参数时,必须先进行`null`判断。 -```java -public class SwitchString { - - public static void main(String[] args) { - // 这里会抛异常 java.lang.NullPointerException - method(null); - } - - public static void method(String param) { - switch (param) { - // 肯定不是进入这里 - case "sth": - System.out.println("it's sth"); - break; - // 也不是进入这里 - case "null": - System.out.println("it's null"); - break; - // 也不是进入这里 - default: - System.out.println("default"); - } - } -} -``` -- 在`if/else/for/while/do`语句中必须使用大括号。【说明:即使只有一行代码,避免采用单行的编码方式:~~`if (condition) statements;`~~】 -- 在`高并发场景`中,避免使用”等于”判断作为中断或退出的条件。【说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。】 -> 反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。 -- 表达异常的分支时,少用`if-else`方式,这种方式可以改写成下面代码:【说明:如果非使用`if()...else if()...else...`方式表达逻辑,避免后续代码维护困难,请勿超过`3`层】 -```java -if (condition) { - ... - return obj; -} -// 接着写 else 的业务逻辑代码; -``` -> 超过3层的`if-else`的逻辑判断代码可以使用`卫语句`、`策略模式`、`状态模式`等来实现。其中卫语句即代码逻辑先考虑失败、异常、中断、退出等直接返回的情况,以方法多个出口的方式,解决代码中判断分支嵌套的问题,这是逆向思维的体现。 -```java -// 示例代码 -public void findBoyfriend(Man man) { - if (man.isUgly()) { - System.out.println("本姑娘是外貌协会的资深会员"); - return; - } - if (man.isPoor()) { - System.out.println("贫贱夫妻百事哀"); - return; - } - if (man.isBadTemper()) { - System.out.println("银河有多远,你就给我滚多远"); - return; - } - System.out.println("可以先交往一段时间看看"); -} -``` -- 除常用方法(如`getXxx/isXxx`)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。【说明:很多`if`语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。】 -```java -// 正例 -// 伪代码如下 -final boolean existed = (file.open(fileName, "w") != null) && (...) || (...); -if (existed) { - ... -} - -// 反例 -// 哈哈,这好像是ReentrantLock里面有类似风格的代码 -// 连Doug Lea的代码都拿来当做反面教材啊 -// 早前就听别人说过“编程不识Doug Lea,写尽Java也枉然!!!” -public final void acquire(long arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { - selfInterrupt(); - } -} -``` -- 不要在其它表达式(尤其是条件表达式)中,插入赋值语句【说明:赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。】 -```java -// 反例 -public Lock getLock(boolean fair) { - // 算术表达式中出现赋值操作,容易忽略 count 值已经被改变 - threshold = (count = Integer.MAX_VALUE) - 1; - // 条件表达式中出现赋值操作,容易误认为是 sync==fair - return (sync = fair) ? new FairSync() : new NonfairSync(); -} -``` -- 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的`try-catch`操作(这个`try-catch`是否可以移至循环体外) -- 避免采用取反逻辑运算符。【说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。正例:使用`if (x < 628)`来表达x小于628。反例:使用 if (!(x >= 628))来表达x小于628。】 -- 下列情形,需要进行参数校验 -> 1) 调用频次低的方法。2)执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。3)需要极高稳定性和可用性的方法。4)对外提供的开放接口,不管是`RPC/API/HTTP`接口。5)敏感权限入口。 -- 下列情形,不需要进行参数校验: -> 1)极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求。 2)底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般`DAO`层与`Service`层都在同一个应用中,部署在同一台服务器中,所以`DAO`的参数校验,可以省略。3)被声明成`private`只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。 - -### (八) 注释规约 -- 类、类属性、类方法的注释必须使用`Javadoc`规范,使用`/**内容*/`格式,不得使用`// xxx`方式。【说明:在IDE编辑窗口中,`Javadoc`方式会提示相关注释,生成 `Javadoc`可以正确输出相应注释;在IDE中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。】 -- 所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能【说明:对子类的实现要求,或者调用注意事项,请一并说明】 -- 所有的类都必须添加创建者和创建日期。 -- 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用`/* */`注释,注意与代码对齐 -- 所有的`枚举类型`字段必须要有注释,说明每个数据项的用途 -- 与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可【反例:“TCP 连接超时”解释成“传输控制协议连接超时”,理解反而费脑筋。】 -- 代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。【代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义】 -- 谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。【说明:代码被注释掉有两种可能性:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库已然保存了历史代码)】 -- 对于注释的要求:第一、能够准确反映设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。 -- 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担【语义清晰的代码不需要额外的注释。】 -- 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。 -> 1)待办事宜(`TODO`):(标记人,标记时间,[预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个`Javadoc`的标签,目前的`Javadoc`还没 -有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc标签)。2)错误,不能工作(`FIXME`):(标记人,标记时间,[预计处理时间]) -在注释中用`FIXME`标记某代码是错误的,而且不能工作,需要及时纠正的情况。 - -### (九) 其它 -- 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度【说明:不要在方法体内定义:`Pattern pattern = Pattern.compile("规则");`】 -- 注意`Math.random()`这个方法返回是`double`类型,注意取值的范围`0≤x<1`(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后取整,直接使用`Random`对象的`nextInt`或者`nextLong`方法。 -- 获取当前毫秒数`System.currentTimeMillis();`而不是`new Date().getTime();`【说明:如果想获取更加精确的纳秒级时间值,使用`System.nanoTime()`的方式。在JDK8中,针对统计时间等场景,推荐使用`Instant`类。】 -- 日期格式化时,传入`pattern`中表示年份统一使用`小写的y`。【说明:日期格式化时,`yyyy`表示当天所在的年,而大写的`YYYY`代表是 week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的`YYYY`就是下一年。另外需要注意:**表示月份是大写的M,表示分钟则是小写的m,24小时制的是大写的H,12小时制的则是小写的h** 。正例:`new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");`】 -- 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存 -- 及时清理不再使用的代码段或配置信息【说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。正例:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(`///`)来说明注释掉代码的理由】 diff --git a/Java/alibaba-java-coding-guidelines-2.md b/Java/alibaba-java-coding-guidelines-2.md deleted file mode 100644 index b717911..0000000 --- a/Java/alibaba-java-coding-guidelines-2.md +++ /dev/null @@ -1,212 +0,0 @@ -# 阿里巴巴Java开发手册——异常处理、MySQL 数据库 - - -## 二、异常日志 - -### (一) 异常处理 - -- Java类库中定义的可以通过预检查方式规避的`RuntimeException`异常不应该通过`catch`的方式来处理,比如:`NullPointerException`,`IndexOutOfBoundsException`等等【说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过`catch NumberFormatException`来实现】 -> 正例:`if (obj != null) {...}` -反例:try { obj.method(); } catch (NullPointerException e) {…} -- 异常不要用来做流程控制,条件控制【说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多】 -- `catch`时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的`catch`尽可能进行区分异常类型,再做对应的异常处理【说明:对大段代码进行`try-catch`,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。】 -- 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容 -- 有`try`块放到了事务代码中,`catch`异常后,如果需要回滚事务,一定要注意手动回滚事务 -- `finally`块必须对资源对象、流对象进行关闭,有异常也要做`try-catch`。【如果JDK7及以上,可以使用`try-with-resources`方式】 -- 不要在`finally`块中使用`return`【说明:`try`块中的`return`语句执行成功后,并不马上返回,而是继续执行`finally`块中的语句,如果此处存在`return`语句,则在此直接返回,无情丢弃掉`try`块中的返回点。】 -```java -// 反例 -private int x = 0; -public int checkReturn() { - try { - // x 等于 1,此处不返回 - return ++x; - } finally { - // 返回的结果是 2 - return ++x; - } -} -``` -- 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。【说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。】 -- 在调用`RPC、二方包、或动态生成类`的相关方法时,捕捉异常必须使用`Throwable`类来进行拦截。【说明:通过反射机制来调用方法,如果找不到方法,抛出`NoSuchMethodException`。什么情况会抛出`NoSuchMethodError`呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出`NoSuchMethodError`。】 -- 方法的返回值可以为`null`,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回`null`值【说明:本手册明确防止`NPE`是`调用者的责任`。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回`null`的情况。】 -- 防止`NPE`,是程序员的基本修养,注意`NPE`产生的场景 -> 1)返回类型为基本数据类型,`return`包装数据类型的对象时,自动拆箱有可能产生 NPE。【反例:`public int f() { return Integer 对象}`,如果为`null`,自动解箱抛`NPE`】 -2)数据库的查询结果可能为`null` -3)集合里的元素即使`isNotEmpty`,取出的数据元素也可能为`null`。 -4)远程调用返回对象时,一律要求进行空指针判断,防止`NPE` -5)对于`Session`中获取的数据,建议进行`NPE`检查,避免空指针。 -6)级联调用`obj.getA().getB().getC();`一连串调用,易产生`NPE`。正例:使用JDK8的`Optional`类来防止NPE问题。 -- 定义时区分`unchecked / checked`异常,避免直接抛出`new RuntimeException()`,更不允许抛出`Exception`或者`Throwable`,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:`DAOException / ServiceException`等。 -- 对于公司外的`http/api`开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间`RPC`调用优先考虑使用`Result`方式,封装`isSuccess()`方法、“错误码”、“错误简短信息”【说明:关于`RPC`方法返回方式使用`Result`方式的理由:1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。2)如果不加栈信息,只是new自定义异常,加入自己的理解的`error message`,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。】 -- 避免出现重复的代码(`Don't Repeat Yourself`),即DRY原则【说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化】 - -### (二) 日志规约 -- 应用中不可直接使用日志系统(`Log4j、Logback`)中的API,而应依赖使用日志框架`SLF4J`中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一 -```java -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -private static final Logger logger = LoggerFactory.getLogger(Test.class); -``` -- 所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点。网络运行状态、安全相关信息、系统监测、管理后台操作、用户敏感操作需要留存相关的网络日志不少于6个月 -- 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:`appName_logType_logName.log`。logType:日志类型,如`stats/monitor/access`等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找【说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。正例:`force-web`应用中单独监控时区转换异常,如:`force_web_timeZoneConvert.log`】 -- 在日志输出时,字符串变量之间的拼接使用占位符的方式【说明:因为`String`字符串的拼接会使用`StringBuilder`的`append()`方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。正例:`logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);`】 -- 对于`trace/debug/info`级别的日志输出,必须进行日志级别的开关判断【说明:虽然在debug(参数)的方法体内第一行代码`isDisabled(Level.DEBUG_INT)`为真时(Slf4j的常见实现`Log4j`和`Logback`),就直接`return`,但是参数可能会进行字符串拼接运算。此外,如果`debug(getName())`这种参数内有`getName()`方法调用,无谓浪费方法调用的开销。】 -```java -// 正例 -// 如果判断为真,那么可以输出trace和debug级别的日志 -if (logger.isDebugEnabled()) { - logger.debug("Current ID is: {} and name is: {}", id, getName()); -} -``` -- 避免重复打印日志,浪费磁盘空间,务必在`log4j.xml`中设置`additivity=false`【正例:``】 -- 异常信息应该包括两类信息:`案发现场信息`和`异常堆栈信息`。如果不处理,那么通过关键字`throws`往上抛出【正例:`logger.error(各类参数或者对象 toString() + "_" + e.getMessage(), e);`】 -- 谨慎地记录日志。生产环境禁止输出`debug`日志;有选择地输出`info`日志;如果使用`warn`来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志【说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:`这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?`】 -- 可以使用`warn`日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出`error`级别,避免频繁报警【说明:注意日志输出的级别,`error`级别只记录`系统逻辑出错、异常或者重要的错误`信息】 -- 尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义【国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。】 - -## 三、单元测试 - -- 好的单元测试必须遵守`AIR`原则【说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。A:`Automatic`(自动化)I:`Independent`(独立性)R:`Repeatable`(可重复)】 -- 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用`System.out`来进行人肉验证,必须使用`assert`来验证 -- 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序【反例:`method2`需要依赖`method1`的执行,将执行结果作为`method2`的输入】 -- 单元测试是可以重复执行的,不能受到外界环境的影响。【说明:单元测试通常会被放到持续集成中,每次有代码`check in`时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用`spring`这样的`DI`框架注入一个本地(内存)实现或者`Mock`实现】 -- 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别【说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域】 -- 核心业务、核心应用、核心模块的增量代码确保单元测试通过【说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正】 -- 单元测试代码必须写在如下工程目录:`src/test/java`,不允许写在业务代码目录下【说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录】 -- 单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%【说明:在工程规约的应用分层中提到的`DAO`层,`Manager`层,可重用度高的`Service`,都应该进行单元测试。】 -- 编写单元测试代码遵守`BCDE`原则,以保证被测试模块的交付质量【B:`Border`,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等; C:`Correct`,正确的输入,并得到预期的结果; D:`Design`,与设计文档相结合,来编写单元测试; E:`Error`,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果】 -- 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据【反例:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数据并不符合业务插入规则,导致测试结果异常】 -- 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识 -- 对于不可测的代码在适当的时机做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码 -- 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例 -- 单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后补充单元测试用例 -- 为了更方便地进行单元测试,业务代码应避免以下情况【构造方法中做的事情过多;存在过多的全局变量和静态方法;存在过多的外部依赖;存在过多的条件语句;说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构】 -- 不要对单元测试存在如下误解【那是测试同学干的事情。1.本文是开发手册,凡是本文内容都是与开发同学强相关的;2.单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的;3.单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态;4.单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障】 - -## 四、安全规约 - -- 隶属于用户个人的页面或者功能必须进行`权限控制校验`【说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容、修改他人的订单】 -- 用户敏感数据禁止直接展示,必须对展示数据进行`脱敏`【说明:中国大陆个人手机号码显示为:137****0969,隐藏中间4位,防止隐私泄露】 -- 用户输入的SQL、参数严格使用参数绑定或者 METADATA 字段值限定,防止`SQL 注`入,禁止字符串拼接SQL访问数据库 -- 用户请求传入的任何参数必须做有效性验证【说明:忽略参数校验可能导致:1.`page size`过大导致内存溢出;2.恶意`order by`导致数据库慢查询;3.任意重定向;4.SQL注入;5.反序列化注入;6.正则输入源串拒绝服务ReDoS;说明:Java代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。】 -- 禁止向HTML页面输出未经安全过滤或未正确转义的用户数据 -- 表单、AJAX提交必须执行CSRF安全验证【说明:`CSRF(Cross-site request forgery)`跨站请求伪造是一类常见编程漏洞。对于存在CSRF漏洞的应用/网站,攻击者可以事先构造好URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。】 -- 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的 机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损 -- 发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略 - -## 五、MySQL数据库 - -### (一) 建表规约 -- 表达是与否概念的字段,必须使用`is_xxx`的方式命名,数据类型是`unsigned tinyint`(1表示是,0表示否)【说明:任何字段如果为非负数,必须是unsigned。注意:POJO类中的任何布尔类型的变量,都不要加`is`前缀,所以,需要在``设置从`is_xxx`到`Xxx`的映射关系。数据库表示是与否的值,使用`tinyint`类型,坚持`is_xxx`的命名方式是为了明确其取值含义与取值范围】 -> 正例:表达`逻辑删除`的字段名`is_deleted`,1表示删除,0表示未删除 -- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑【说明:MySQL在 Windows下不区分大小写,但在Linux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝】 -> 正例:`aliyun_admin`,`rdc_config`,`level3_name` -反例:AliyunAdmin,rdcConfig,level_3_name -- 表名不使用复数名词【表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于`DO`类名也是单数形式,符合表达习惯】 -- 禁用保留字,如`desc、range、match、delayed`等,请参考`MySQL`官方保留字 -- 主键索引名为`pk_字段名`;唯一索引名为`uk_字段名`;普通索引名则为`idx_字段名`【说明:`pk_`即`primary key`;`uk_`即`unique key`;`idx_`即`index`的简称】 -- 小数类型为`decimal`,禁止使用`float`和`double`【说明:在存储的时候,`float`和`double`都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过`decimal`的范围,建议将数据拆成整数和小数并分开存储】 -- 如果存储的字符串长度几乎相等,使用`char`定长字符串类型 -- `varchar`是可变长字符串,不预先分配存储空间,长度不要超过`5000`,如果存储长度大于此值,定义字段类型为`text`,独立出来一张表,用主键来对应,避免影响其它字段索引效率 -- 表必备三字段:`id, create_time, update_time`【说明:其中`id`必为主键,类型为`bigint unsigned`、单表时自增、步长为1。`create_time, update_time`的类型均为`datetime`类型。】 -- 表的命名最好是遵循“业务名称_表的作用”【正例:`alipay_task / force_project / trade_config`】 -- 库名与应用名称尽量一致 -- 如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释 -- 字段允许适当冗余,以提高查询性能,但必须考虑数据一致【冗余字段应遵循:1)不是频繁修改的字段。2)不是`varchar`超长字段,更不能是`text`字段。3)不是唯一索引的字段】(`正例`:商品类目名称使用频率高,字段长度短,名称基本一不变,可在相关联的表中冗余存储类目名称,避免关联查询) -- 单表行数超过`500万`行或者单表容量超过`2GB`,才推荐进行`分库分表`【说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表】 -- 合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。如下表,其中无符号值可以避免误存负数,且扩大了表示范围。 - -|对象| 年龄区间 |类型 |字节| 表示范围| -|---| --- |--- |---|---| -|人|150岁之内|tinyint unsigned| 1|无符号值:0到255| -|龟|数百岁|smallint unsigned |2|无符号值:0到65535| -|恐龙化石|数千万年|int unsigned|4|无符号值:0到约42.9亿| -|太阳|约50亿年|bigint unsigned|8|无符号值:0到约10的19次方| - -### (二) 索引规约 -- 业务上具有`唯一特性`的字段,即使是多个字段的组合,也必须建成唯一索引【说明:不要以为唯一索引影响了`insert`速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生】 -- 超过三个表禁止`join`。需要`join`的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有`索引`【说明:即使双表join也要注意表索引、SQL性能】 -- 在`varchar`字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可【说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用`count(distinct left(列名, 索引长度))/count(*)`的区分度来确定】 -- 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决【说明:索引文件具有 `B-Tree`的`最左前缀匹配`特性,如果左边的值未确定,那么无法使用此索引】 -- 如果有`order by`的场景,请注意利用索引的有序性。`order by`最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现`file_sort`的情况,影响查询性能 -> 正例:where a=? and b=? order by c; 索引:a_b_c -反例:索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。 -- 利用覆盖索引来进行查询操作,避免回表【说明:如果一本书需要知道第11章是什么标题,会翻开第11章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。】 -> 正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用`explain`的结果,`extra`列会出现:`using index` -- 利用延迟关联或者子查询优化超多分页场景【说明:MySQL并不是跳过`offset`行,而是取`offset+N`行,然后返回放弃前`offset`行,返回`N`行,那当`offset`特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写】 -> 正例:先快速定位需要获取的id段,然后再关联:`SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id` -- SQL性能优化的目标:至少要达到`range`级别,要求是`ref`级别,如果可以是`consts`最好【说明:1)`consts`单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。2)`ref`指的是使用普通的索引(normal index)。3)`range`对索引进行范围检索。】 -> 反例:`explain`表的结果,type=index,索引物理文件全扫描,速度非常慢,这个`index`级别比较`range`还低,与全表扫描是小巫见大巫。 -- 建组合索引的时候,区分度最高的在最左边【说明:存在非等号和等号混合时,在建索引时,请把等号条件的列前置。如:`where c>? and d=?`那么即使c的区分度更高,也必须把d放在索引的最前列,即索引`idx_d_c`】 -> 正例:如果`where a=? and b=?`,如果a列的几乎接近于唯一值,那么只需要单建`idx_a`索引即可 -- 防止因字段类型不同造成的隐式转换,导致索引失效 -- 创建索引时避免有如下极端误解 -> 1)宁滥勿缺。认为一个查询就需要建一个索引 -2)宁缺勿滥。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度 -3)抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决 - -### (三) SQL语句 -- 不要使用`count(列名)`或`count(常量)`来替代`count(*)`,`count(*)`是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关【说明:`count(*)`会统计值为`NULL`的行,而`count(列名)`不会统计此列为NULL值的行】 -- `count(distinct col)`计算该列除NULL之外的不重复行数,注意`count(distinct col1, col2)`如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为0 -- 当某一列的值全是NULL时,`count(col)`的返回结果为 0,但`sum(col)`的返回结果为NULL,因此使用`sum()`时需注意`NPE`问题 -> 正例:使用如下方式来避免sum的NPE问题:`SELECT IFNULL(SUM(column), 0) FROM table;` -- 使用`ISNULL()`来判断是否为`NULL`值【说明:NULL与任何值的直接比较都为NULL。 1)`NULL<>NULL`的返回结果是`NULL`,而不是`false`。 2)`NULL=NULL`的返回结果是`NULL`,而不是`true`。 3)`NULL<>1`的返回结果是`NULL`,而不是`true`。】 -- 代码中写分页查询逻辑时,若`count`为0应直接返回,避免执行后面的分页语句 -- 不得使用外键与级联,一切外键概念必须在应用层解决【说明:以学生和成绩的关系为例,学生表中的`student_id`是主键,那么成绩表中的`student_id`则为外键。如果更新学生表中的`student_id`,同时触发成绩表中的`student_id`更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度】 -- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性 -- 数据订正(特别是删除、修改记录操作)时,要先select,避免出现误删除,确认无误才能执行更新语句 -- `in`操作能避免则避免,若实在避免不了,需要仔细评估in后边的集合元素数量,控制在`1000`个之内 -- 如果有国际化需要,所有的字符存储与表示,均以`utf-8`编码,注意字符统计函数的区别。【说明:`SELECT LENGTH("轻松工作");`返回为12;`SELECT CHARACTER_LENGTH("轻松工作");` 返回为4;如果需要存储表情,那么选择`utf8mb4`来进行存储,注意它与`utf-8`编码的区别。】 -- `TRUNCATE TABLE`比`DELETE`速度快,且使用的系统和事务日志资源少,但`TRUNCATE`无事务且不触发`trigger`,有可能造成事故,故不建议在开发代码中使用此语句【说明:`TRUNCATE TABLE`在功能上与不带`WHERE`子句的`DELETE`语句相同】 - -### (四) ORM映射 -- 在表查询中,一律不要使用`*`作为查询的字段列表,需要哪些字段必须明确写明【说明:1)增加查询分析器解析成本。2)增减字段容易与`resultMap`配置不一致。3)无用字段增加网络消耗,尤其是`text`类型的字段】 -- POJO类的布尔属性不能加`is`,而数据库字段必须加`is_`,要求在`resultMap`中进行字段与属性之间的映射。【说明:参见定义POJO类以及数据库字段定义规定,在``中增加映射,是必须的。在`MyBatis Generator`生成的代码中,需要进行对应的修改】 -- 不要用`resultClass`当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个`POJO`类与之对应【说明:配置映射关系,使字段与`DO`类解耦,方便维护。】 -- `sql.xml`配置参数使用:`#{},#param#`不要使用`${}`此种方式容易出现SQL 注入 -- 不允许直接拿`HashMap`与`Hashtable`作为查询结果集的输出【说明:`resultClass=”Hashtable”`,会置入字段名和属性值,但是值的类型不可控】 -- 更新数据表记录时,必须同时更新记录对应的`gmt_modified`字段值为当前时间 -- 不要写一个大而全的数据更新接口。传入为POJO类,不管是不是自己的目标更新字段,都进行`update table set c1=value1,c2=value2,c3=value3; `这是不对的。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加`binlog`存储 -- `@Transactional`事务不要滥用。事务会影响数据库的`QPS`,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等 -- ``中的compareValue是与属性值对比的常量,一般是数字,表示相等时带上此条件;``表示不为空且不为null时执行;``表示不为null值时执行。 - -## 六、工程结构 -- 分层异常处理规约 -> 在`DAO`层,产生的异常类型有很多,无法用细粒度的异常进行catch,使用`catch(Exception e)`方式,并`throw new DAOException(e)`,不需要打印日志,因 -为日志在`Manager/Service`层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在`Service`层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。如果`Manager`层与`Service`同机部署,日志方式与DAO层处理一致,如果是单独部署,则采用与`Service`一致的处理方式。Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,加上用户容易理解的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。 -- 分层领域模型规约 -> `DO`(Data Object):此对象与数据库表结构一一对应,通过DAO层向上传输数据源对象 -`DTO`(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象 -`BO`(Business Object):业务对象,由Service层输出的封装业务逻辑的对象 -`AO`(Application Object):应用对象,在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高 -`VO`(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象 -`Query`:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用Map类来传输 -- 二方库依赖 -> 1)`GroupID`格式:`com.{公司/BU }.业务线 [.子业务线]`,最多4级。正例:`com.taobao.jstorm`或`com.alibaba.dubbo.register` -2)`ArtifactID`格式:`产品线名-模块名`。语义不重复不遗漏,先到中央仓库去查证一下。正例:`dubbo-client / fastjson-api / jstorm-tool` -3)`Version`:主版本号.次版本号.修订号。【1.主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级;2.次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改;3.修订号:保持完全兼容性,修复 BUG、新增次要功能特性等】 -**说明**:注意起始版本号必须为:1.0.0,而不是0.0.1,正式发布的类库必须先去中央仓库进行查证,使版本号有延续性,正式版本号不允许覆盖升级。如当前版本:1.3.3,那么下一个合理的版本号:1.3.4或1.4.0或2.0.0 -- 底层基础技术框架、核心数据管理平台、或近硬件端系统谨慎引入第三方实现 -- 所有pom文件中的依赖声明放在``语句块中,所有版本仲裁放在``语句块中【说明:``里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖,`version`和`scope`都读取自父pom。而``所有声明在主pom的``里的依赖都会自动引入,并默认被所有的子项目继承】 -- 二方库不要有配置项,最低限度不要再增加配置项 -- 为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则 -> 1)精简可控原则。移除一切不必要的API和依赖,只包含Service API、必要的领域模型对象、Utils类、常量、枚举等 -2)稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到 -- 高并发服务器建议调小`TCP`协议的`time_wait`超时时间【操作系统默认`240`秒后,才会关闭处于`time_wait`状态的连接,在高并发访问下,服务器端会因为处于`time_wait`的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值】 -> 正例:在linux服务器上请通过变更`/etc/sysctl.conf`文件去修改该缺省值(秒):`net.ipv4.tcp_fin_timeout = 30` -- 调大服务器所支持的最大文件句柄数(`File Descriptor`,简写为 fd)【说明:主流操作系统的设计是将`TCP/UDP`连接采用与文件一样的方式去管理,即一个连接对应于一个fd。主流的linux服务器默认所支持最大fd数量为`1024`,当并发连接数很大时很容易因为fd不足而出现`“open too many files”`错误,导致新的连接无法建立。建议将linux服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)】 -- 给JVM环境参数设置`-XX:+HeapDumpOnOutOfMemoryError`参数,让`JVM`碰到 - `OOM`场景时输出`dump`信息【说明:OOM的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助】 -- 在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力 - -## 七、设计规约 -- 存储方案和数据结构需要认真地进行设计和评审 -- 需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程与业务边界 -- 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现 -- 系统设计主要目的是明确需求、理顺逻辑、后期维护,次要目的用于指导编码。 -- 设计的本质就是识别和表达系统难点,找到系统的变化点,并隔离变化点 -- 系统架构设计的目的【1.确定系统边界。确定系统在技术层面上的做与不做;2.确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出; 3.确定指导后续设计与演化的原则。使后续的子系统或模块设计在规定的框架内继续演化;4.确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等】 - -## 个人小结 -以上内容便是我一边读,一边思考整理出来的。《Java开发手册》是实践开发中提炼出来的`精华`,当中有很多小点都是值得拿出来仔细推敲,往往小的规约背后隐藏着大学问。或许你和我一样,有些地方并不熟悉或者暂时并不理解为什么这样规约,个人觉得这些困惑的点读一遍、思考一遍远远不够,应该收藏下来,有空的时候多读、多思考。如果开发中遇到了手册中类似的场景,那么收获会更大,理解会更深。最后,正如手册中提到的愿景,“码出高效,码出质量”,希望你我都能码出一个新的高度。 \ No newline at end of file diff --git a/Java/dynamic-proxy-in-java.md b/Java/dynamic-proxy-in-java.md deleted file mode 100644 index ad600f4..0000000 --- a/Java/dynamic-proxy-in-java.md +++ /dev/null @@ -1,595 +0,0 @@ -# 一文读懂Java中的动态代理 - -## 从代理模式说起 - -回顾前文: [设计模式系列之代理模式(Proxy Pattern)](https://www.cnblogs.com/itwild/p/13196590.html) - -要读懂`动态代理`,应从`代理模式`说起。而实现代理模式,常见有下面两种实现: - -(1) 代理类`关联`目标对象,实现目标对象实现的接口 -```java -public class Proxy implements Subject { - // 维持一个对真实主题对象的引用 - private RealSubject realSubject; - - public Proxy(RealSubject realSubject) { - this.realSubject = realSubject; - } - - public void preRequest() { - // ... - } - - public void postRequest() { - // ... - } - - @Override - public void request() { - preRequest(); - // 调用真实主题对象的方法 - realSubject.request(); - postRequest(); - } -} -``` - -(2) 代理类`继承`目标类,重写需要代理的方法 -```java -public class Proxy extends RealSubject { - - public void preRequest() { - // ... - } - - public void postRequest() { - // ... - } - - @Override - public void request() { - preRequest(); - super.request(); - postRequest(); - } -} -``` - -> 如果程序`运行前`就在Java代码中定义好代理类(`Proxy`),那么这种代理方式就叫做`静态代理`;若代理类在程序`运行时`创建就叫做`动态代理` - -- 如果为特定类的特定方法生成固定的代理,当然使用`静态代理`就能很好满足需求。 -- 如果要为大量不同类的不同方法生成代理,使用静态代理的话就需要编写大量的代理类,且大量代码冗余,此时`动态代理`就应该闪亮登场了。 - -Java中实现动态代理常用的技术包括`JDK的动态代理`、`CGLib`等。 - -## JDK的动态代理 - -### 快速入门 - -假设我们的业务系统中有对用户(`UserService`)和商品(`ProductService`)的查询(`query`)和删除(`delete`)业务逻辑,代码如下: -```java -public interface CommonService { - Object query(Long id); - - void delete(Long id); -} - -public class UserService implements CommonService { - @Override - public Object query(Long id) { - String s = "查询到用户:" + id; - System.out.println(s); - return s; - } - - @Override - public void delete(Long id) { - System.out.println("已删除用户:" + id); - } -} - -public class ProductService implements CommonService { - @Override - public Object query(Long id) { - String s = "查询到商品:" + id; - System.out.println(s); - return s; - } - - @Override - public void delete(Long id) { - System.out.println("已删除商品:" + id); - } -} -``` - -现在想用`代理模式`给这些`Service`统一加上业务处理时间的日志(`log`),如果使用`静态代理`,那么拿上面的例子来说就要再手动写代理类,但实际的业务系统肯定远不止这2个类,那么就需要写大量的相似冗余的代码。 - -那么使用JDK提供的`动态代理`,应该如何实现呢? - -(1) 编写日志处理器`LogHandler`,该处理器需要实现java中的`InvocationHandler`接口中的`invoke`方法 -```java -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; - -public class LogHandler implements InvocationHandler { - // 目标对象 - private Object target; - - public LogHandler(Object target) { - this.target = target; - } - - private void preHandle() { - System.out.println("开始处理请求时间: " + System.currentTimeMillis()); - } - - private void postHandle() { - System.out.println("结束处理请求时间: " + System.currentTimeMillis()); - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // 请求处理前 记录日志 - preHandle(); - // 目标对象的业务处理逻辑 - Object result = method.invoke(target, args); - // 请求处理完成 记录日志 - postHandle(); - return result; - } -} -``` -(2) 生成代理对象,并测试代理是否生效 -```java -public static void main(String[] args) { - /** - * @see sun.misc.ProxyGenerator#saveGeneratedFiles - * jdk1.8加上这样的配置(其他版本应当取找sun.misc.ProxyGenerator#saveGeneratedFiles用的是什么) - * 会将运行时生成的代理Class落磁盘,方便我们查看动态代理生成的class文件。jdk1.8应该是在当前项目根目录的com/sun/proxy目录 - * 注意:在main方法中加该配置 - */ - System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); - - // 原对象 - CommonService userService = new UserService(); - CommonService productService = new ProductService(); - - // 代理对象 - CommonService proxyUserService = (CommonService) Proxy.newProxyInstance( - CommonService.class.getClassLoader(), - new Class[]{CommonService.class}, - new LogHandler(userService)); - - CommonService proxyProductService = (CommonService) Proxy.newProxyInstance( - CommonService.class.getClassLoader(), - new Class[]{CommonService.class}, - new LogHandler(productService)); - - // 测试代理是否生效 - proxyUserService.query(1L); - System.out.println("----------"); - proxyUserService.delete(1L); - - System.out.println("\n"); - - proxyProductService.query(1L); - System.out.println("----------"); - proxyProductService.delete(1L); -} -``` -(3) 运行结果 -```sh -开始处理请求时间: 1594528163163 -查询到用户:1 -结束处理请求时间: 1594528163163 ----------- -开始处理请求时间: 1594528163163 -已删除用户:1 -结束处理请求时间: 1594528163163 - - -开始处理请求时间: 1594528163163 -查询到商品:1 -结束处理请求时间: 1594528163163 ----------- -开始处理请求时间: 1594528163163 -已删除商品:1 -结束处理请求时间: 1594528163163 -``` -可见,通过代理模式增加统一日志处理生效了,而且即便是给多个不同类的对象添加统一日志处理,写一个`LogHandler`就够了,不用为每个类额外写一个对应的代理类。 - -### 实现原理 -```java -System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); -``` -上面代码中有这样一行,加上这个之后就能把运行时生成的代理class文件写到文件中(在项目根目录的com/sun/proxy下),关键奥秘就在于生成的这个class文件。 - -运行之后,在当前项目的根目录的com/sun/proxy下,会多出一个`$Proxy0.class`文件,反编译查看源代码(这里去除了`equals()`、`toString()`、`hashCode()`方法),如下: -```java -package com.sun.proxy; -import com.github.itwild.proxy.CommonService; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.lang.reflect.UndeclaredThrowableException; - -public final class $Proxy0 extends Proxy implements CommonService { - private static Method m4; - private static Method m3; - - public $Proxy0(InvocationHandler var1) { - super(var1); - } - - public final Object query(Long var1) { - try { - return (Object)super.h.invoke(this, m4, new Object[]{var1}); - } catch (RuntimeException | Error var3) { - throw var3; - } catch (Throwable var4) { - throw new UndeclaredThrowableException(var4); - } - } - - public final void delete(Long var1) { - try { - super.h.invoke(this, m3, new Object[]{var1}); - } catch (RuntimeException | Error var3) { - throw var3; - } catch (Throwable var4) { - throw new UndeclaredThrowableException(var4); - } - } - - static { - try { - m4 = Class.forName("com.github.itwild.proxy.CommonService").getMethod("query", Class.forName("java.lang.Long")); - m3 = Class.forName("com.github.itwild.proxy.CommonService").getMethod("delete", Class.forName("java.lang.Long")); - } catch (NoSuchMethodException var2) { - throw new NoSuchMethodError(var2.getMessage()); - } catch (ClassNotFoundException var3) { - throw new NoClassDefFoundError(var3.getMessage()); - } - } -} -``` -看到上面的代码,你有没有似曾相识的感觉,这不正是博客一开篇介绍的实现代理模式的第一种方式吗(`代理类关联目标对象,实现目标对象实现的接口`)。 - -我们再理一下生成的代理类的代码逻辑,`$Proxy0`继承了`java.lang.reflect.Proxy`,并实现了`CommonService`接口,对代理类的方法调用(比如说`query()`)实际上都会转发到`super.h`对象的`invoke()`方法调用,再看下`super.h`到底是啥,追踪一下父类`java.lang.reflect.Proxy`可知 -```java -/** - * the invocation handler for this proxy instance. - */ -protected InvocationHandler h; -``` -这正是`快速入门`中我们编写的`LogHandler`所实现的`InvocationHandler`接口。这样整个过程就理清了,这里通过`super.h`调用了我们前面编写的`LogHandler`中的处理逻辑。 - -那么,新的问题又来了,代理类是怎么生成的,我们没有写任何相关的代码,它是怎么知道我需要代理的方法以及方法参数等等。我们在创建代理对象的时候调用`Proxy.newProxyInstance`传入了代理类需要实现的接口 -```java -/** - * Returns an instance of a proxy class for the specified interfaces - * that dispatches method invocations to the specified invocation - * handler. - * - * @param loader the class loader to define the proxy class - * @param interfaces the list of interfaces for the proxy class - * to implement - * @param h the invocation handler to dispatch method invocations to - */ -@CallerSensitive -public static Object newProxyInstance(ClassLoader loader, - Class[] interfaces, - InvocationHandler h) -``` -至于一步步如何生成class的byte[]可先追踪`java.lang.reflect.Proxy`中的`ProxyClassFactory`相关代码 -```java -/** - * A factory function that generates, defines and returns the proxy class given - * the ClassLoader and array of interfaces. - */ -private static final class ProxyClassFactory - implements BiFunction[], Class> -{ - // prefix for all proxy class names - private static final String proxyClassNamePrefix = "$Proxy"; - - // next number to use for generation of unique proxy class names - private static final AtomicLong nextUniqueNumber = new AtomicLong(); - - @Override - public Class apply(ClassLoader loader, Class[] interfaces) { - - Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); - for (Class intf : interfaces) { - /* - * Verify that the class loader resolves the name of this - * interface to the same Class object. - */ - Class interfaceClass = null; - try { - interfaceClass = Class.forName(intf.getName(), false, loader); - } catch (ClassNotFoundException e) { - } - if (interfaceClass != intf) { - throw new IllegalArgumentException( - intf + " is not visible from class loader"); - } - /* - * Verify that the Class object actually represents an - * interface. - */ - if (!interfaceClass.isInterface()) { - throw new IllegalArgumentException( - interfaceClass.getName() + " is not an interface"); - } - /* - * Verify that this interface is not a duplicate. - */ - if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { - throw new IllegalArgumentException( - "repeated interface: " + interfaceClass.getName()); - } - } - - String proxyPkg = null; // package to define proxy class in - int accessFlags = Modifier.PUBLIC | Modifier.FINAL; - - /* - * Record the package of a non-public proxy interface so that the - * proxy class will be defined in the same package. Verify that - * all non-public proxy interfaces are in the same package. - */ - for (Class intf : interfaces) { - int flags = intf.getModifiers(); - if (!Modifier.isPublic(flags)) { - accessFlags = Modifier.FINAL; - String name = intf.getName(); - int n = name.lastIndexOf('.'); - String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); - if (proxyPkg == null) { - proxyPkg = pkg; - } else if (!pkg.equals(proxyPkg)) { - throw new IllegalArgumentException( - "non-public interfaces from different packages"); - } - } - } - - if (proxyPkg == null) { - // if no non-public proxy interfaces, use com.sun.proxy package - proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; - } - - /* - * Choose a name for the proxy class to generate. - */ - long num = nextUniqueNumber.getAndIncrement(); - String proxyName = proxyPkg + proxyClassNamePrefix + num; - - /* - * Generate the specified proxy class. - */ - byte[] proxyClassFile = ProxyGenerator.generateProxyClass( - proxyName, interfaces, accessFlags); - try { - return defineClass0(loader, proxyName, - proxyClassFile, 0, proxyClassFile.length); - } catch (ClassFormatError e) { - throw new IllegalArgumentException(e.toString()); - } - } -} -``` -通过上面一段代码不知道你有没有明白生成的第一个代理类的`ClassName`为什么是`$Proxy0`。通过观察生成的`class $Proxy0 extends Proxy implements CommonService`,我们知道JDK的动态代理必须要针对接口,而上面一段代码也做了合法性检查 -```java -if (!interfaceClass.isInterface()) { - throw new IllegalArgumentException( - interfaceClass.getName() + " is not an interface"); -} -``` -然后就要往`sun.misc.ProxyGenerator#generateProxyClass()`方法里看了 -```java -private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles")); - -public static byte[] generateProxyClass(final String var0, Class[] var1, int var2) { - ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2); - final byte[] var4 = var3.generateClassFile(); - if (saveGeneratedFiles) { - AccessController.doPrivileged(new PrivilegedAction() { - public Void run() { - try { - // 省略... - Files.write(var2, var4, new OpenOption[0]); - return null; - } catch (IOException var4x) { - throw new InternalError("I/O exception saving generated file: " + var4x); - } - } - }); - } - - return var4; -} -``` -这里看到了为什么我们要在main方法一开始加上`sun.misc.ProxyGenerator.saveGeneratedFiles`配置就是为了让生成的代理class字节码落盘生成文件。 - -继续就是`ProxyGenerator#generateClassFile()`如何根据`className`、`interfaces`生成classfile的byte[]以及如何得到`class`对象`java.lang.reflect.Proxy#defineClass0`,有兴趣可以深入探究。 -```java -private static native Class defineClass0(ClassLoader loader, String name, - byte[] b, int off, int len); -``` - - -### 注意事项 - -JDK的动态代理是不需要第三方库支持的,被代理的对象必须要实现接口。 - -## CGLib - -CGLib(`Code Generation Library`)是一个功能较为强大、性能也较好的代码生成包,在许多AOP框架中得到广泛应用。 - -### 快速入门 - -除了`UserService`、`ProductService`,还有订单业务(`OrderService`)也需要用代理模式添加统一日志处理,但是注意,`OrderService`并没有实现任何接口,且`delete()`方法用`final`修饰。 -```java -public class OrderService { - public Object query(Long id) { - String s = "查询到订单:" + id; - System.out.println(s); - return s; - } - - public final void delete(Long id) { - System.out.println("已删除订单:" + id); - } -} -``` -我们知道,JDK的动态代理必须要求实现了接口,而`cglib`没有这个限制。具体操作如下: - -(1) 引入cglib的maven依赖 -```sh - - cglib - cglib - 3.3.0 - -``` -(2) 编写方法拦截器 -```java -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; -import java.lang.reflect.Method; - -public class LogInterceptor implements MethodInterceptor { - - private void preHandle() { - System.out.println("开始处理请求时间: " + System.currentTimeMillis()); - } - - private void postHandle() { - System.out.println("结束处理请求时间: " + System.currentTimeMillis()); - } - - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - // pre handle - preHandle(); - // Invoke the original (super) method on the specified object - Object object = proxy.invokeSuper(obj, args); - // post handle - postHandle(); - return object; - } -} -``` - -(3) 生成代理对象,并测试代理是否生效 -```java -public static void main(String[] args) { - // 指定目录生成动态代理类class文件 - System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/tmp/cglib"); - - Enhancer enhancer = new Enhancer(); - // set the class which the generated class will extend - enhancer.setSuperclass(OrderService.class); - // set the single Callback to use - enhancer.setCallback(new LogInterceptor()); - - // generate a new class - OrderService proxy = (OrderService) enhancer.create(); - - proxy.query(1L); - System.out.println(); - proxy.delete(1L); -} -``` -(4) 运行结果 -```sh -开始处理请求时间: 1594653500162 -查询到订单:1 -结束处理请求时间: 1594653500183 - -已删除订单:1 -``` -可见,对`OrderService`的`query()`方法实现了代理,而被`final`修饰的`delete()`方法没有被代理。 - -### 实现原理 - -非常类似学习JDK的动态代理,这里我们同样反编译生成的代理class文件,去除其他暂时这里不关注的信息,代码如下: -```java -import java.lang.reflect.Method; -import net.sf.cglib.core.ReflectUtils; -import net.sf.cglib.core.Signature; -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.Factory; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - -public class OrderService$$EnhancerByCGLIB$$ba8463fa extends OrderService implements Factory { - private static final Method CGLIB$query$0$Method; - private static final MethodProxy CGLIB$query$0$Proxy; - - static void CGLIB$STATICHOOK1() { - CGLIB$query$0$Method = ReflectUtils.findMethods(new String[]{"query", "(Ljava/lang/Long;)Ljava/lang/Object;"}, (var1 = Class.forName("com.github.itwild.proxy.OrderService")).getDeclaredMethods())[0]; - CGLIB$query$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Long;)Ljava/lang/Object;", "query", "CGLIB$query$0"); - } - - final Object CGLIB$query$0(Long var1) { - return super.query(var1); - } - - public final Object query(Long var1) { - MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; - if (var10000 == null) { - CGLIB$BIND_CALLBACKS(this); - var10000 = this.CGLIB$CALLBACK_0; - } - - return var10000 != null ? var10000.intercept(this, CGLIB$query$0$Method, new Object[]{var1}, CGLIB$query$0$Proxy) : super.query(var1); - } - - public static MethodProxy CGLIB$findMethodProxy(Signature var0) { - String var10000 = var0.toString(); - switch(var10000.hashCode()) { - case -508378822: - if (var10000.equals("clone()Ljava/lang/Object;")) { - return CGLIB$clone$4$Proxy; - } - break; - case 842547398: - if (var10000.equals("query(Ljava/lang/Long;)Ljava/lang/Object;")) { - return CGLIB$query$0$Proxy; - } - break; - case 1826985398: - if (var10000.equals("equals(Ljava/lang/Object;)Z")) { - return CGLIB$equals$1$Proxy; - } - break; - case 1913648695: - if (var10000.equals("toString()Ljava/lang/String;")) { - return CGLIB$toString$2$Proxy; - } - break; - case 1984935277: - if (var10000.equals("hashCode()I")) { - return CGLIB$hashCode$3$Proxy; - } - } - - return null; - } - - static { - CGLIB$STATICHOOK1(); - } -} -``` -观察``OrderService$$EnhancerByCGLIB$$ba8463fa``得知该类继承了`OrderService`,并且override了`query(Long id)`方法,而`delete`方法被`final`修饰不能被重写。 - -到了这里,不知道你有没有想起开篇讲到的实现代理模式的第二种方式(`代理类继承目标类,重写需要代理的方法`)。这里应用的正是这种。 - -关于cglib更详细的介绍并不是这里的重点,后面我会抽时间细致学习学习做个笔记出来。不过这里还是要多提几句。 - -当调用代理类的`query()`方法时,会寻找该`query()`方法上有没有被绑定拦截器(比如说编写代码时实现的`MethodInterceptor`接口),没有的话则不需要代理。JDK动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率较低,cglib采用了`FastClass`的机制来实现对被拦截方法的调用。FastClass机制会对一个类的方法建立索引,通过索引来直接调用相应的方法,提高了效率。 \ No newline at end of file diff --git a/Java/java-in-the-future.md b/Java/java-in-the-future.md deleted file mode 100644 index ac8f441..0000000 --- a/Java/java-in-the-future.md +++ /dev/null @@ -1,180 +0,0 @@ -# 云原生时代的Java - -原文链接(作者:周志明):`https://time.geekbang.org/column/article/321185` -公开课链接:`https://time.geekbang.org/opencourse/detail/100067401` - -今天,25 岁的 Java 仍然是最具有统治力的编程语言,长期占据编程语言排行榜的首位,拥有 1200 万的庞大开发者群体,全世界有 450 亿部物理设备使用着 Java 技术。同时,在云端数据中心的虚拟化环境里,还运行着超过 250 亿个 Java 虚拟机的进程实例(数据来自 Oracle 的 WebCast)。 - -以上这些数据是 Java 过去 25 年巨大成就的功勋佐证,更是 Java 技术体系维持自己“天下第一”编程语言的坚实壁垒。Java 与其他语言竞争,底气从来不在于语法、类库有多么先进好用,而是来自它庞大的用户群和极其成熟的软件生态,这在朝夕之间难以撼动。 - -然而,这个现在看起来仍然坚不可摧的 Java 帝国,其统治地位的稳固程度不仅没有高枕无忧,反而说是危机四伏也不为过。目前已经有了可预见的、足以威胁动摇其根基的潜在可能性正在酝酿,并随云原生时代而降临。 - -## Java 的危机 - -Java 与云原生的矛盾,来源于 Java 诞生之初,植入到它基因之中的一些基本的前提假设已经逐渐开始被动摇,甚至已经不再成立。 - -我举个例子,每一位 Java 的使用者都听说过“一次编写,到处运行”(Write Once,Run Anywhere)这句口号。20 多年前,Java 成熟之前,开发者如果希望程序在 Linux、Solaris、Windows 等不同平台,在 x86、AMD64、SPARC、MIPS、ARM 等不同指令集架构上都能正常运行,就必须针对每种组合,编译出对应的二进制发行包,或者索性直接分发源代码,由使用者在自己的平台上编译。 - -面对这个问题,Java 通过语言层虚拟化的方式,令每一个 Java 应用都自动取得平台无关(Platform Independent)、架构中立(Architecture Neutral)的先天优势,让同一套程序格式得以在不同指令集架构、不同操作系统环境下都能运行且得到一致的结果,不仅方便了程序的分发,还避免了各种平台下内存模型、线程模型、字节序等底层细节差异对程序编写的干扰。 - -在当年,Java 的这种设计带有令人趋之若鹜的强大吸引力,直接开启了托管语言(Managed Language,如 Java、.NET)的一段兴盛期。 - -面对相同的问题,今天的云原生选择以操作系统层虚拟化的方式,通过容器实现的不可变基础设施去解决。不可变基础设施这个概念出现得比云原生要早,原本是指该如何避免由于运维人员对服务器运行环境所做的持续的变更,而导致的意想不到的副作用。 - -但在云原生时代,它的内涵已不再局限于方便运维、程序升级和部署的手段,而是升华为一种向应用代码隐藏环境复杂性的手段,是分布式服务得以成为一种可普遍推广的普适架构风格的必要前提。 - -将程序连同它的运行环境一起封装到稳定的镜像里,现在已经是一种主流的应用程序分发方式。Docker 同样提出过“一次构建,到处运行”(Build Once,Run Anywhere)的口号,尽管它只能提供环境兼容性和有局限的平台无关性(指系统内核功能以上的 ABI 兼容),且完全不可能支撑架构中立性,所以将“一次构建,到处运行”与“一次编写,到处运行”对立起来并不严谨恰当。 - -但是无可否认,今天 Java 技术“一次编译,到处运行”的优势,已经被容器大幅度地削弱,不再是大多数服务端开发者技术选型的主要考虑因素了。 - -如果仅仅是优势的削弱,并不足以成为 Java 的直接威胁,充其量只是一个潜在的不利因素,但更加迫在眉睫的风险来自于那些与技术潮流直接冲突的假设。 - -譬如,Java 总体上是面向大规模、长时间的服务端应用而设计的,严(luō)谨(suō)的语法利于约束所有人写出较一致的代码;静态类型动态链接的语言结构,利于多人协作开发,让软件触及更大规模;即时编译器、性能制导优化、垃圾收集子系统等 Java 最具代表性的技术特征,都是为了便于长时间运行的程序能享受到硬件规模发展的红利。 - -另一方面,在微服务的背景下,提倡服务围绕业务能力而非技术来构建应用,不再追求实现上的一致,一个系统由不同语言、不同技术框架所实现的服务来组成是完全合理的。服务化拆分后,很可能单个微服务不再需要再面对数十、数百 GB 乃至 TB 的内存。有了高可用的服务集群,也无须追求单个服务要 7×24 小时不可间断地运行,它们随时可以中断和更新。 - -同时,微服务又对应用的容器化亲和性,譬如镜像体积、内存消耗、启动速度,以及达到最高性能的时间等方面提出了新的要求。这两年的网红概念 Serverless 也进一步增加这些因素的考虑权重,而这些却正好都是 Java 的弱项:哪怕再小的 Java 程序也要带着完整的虚拟机和标准类库,使得镜像拉取和容器创建效率降低,进而使整个容器生命周期拉长。 - -基于 Java 虚拟机的执行机制,使得任何 Java 的程序都会有固定的基础内存开销,以及固定的启动时间,而且 Java 生态中广泛采用的依赖注入进一步将启动时间拉长,使得容器的冷启动时间很难缩短。 - -软件工业中已经出现过不止一起因 Java 这些弱点而导致失败的案例。如 JRuby 编写的 Logstash,原本是同时承担部署在节点上的收集端(Shipper)和专门转换处理的服务端(Master)的职责,后来因为资源占用的原因,被 Elstaic.co 用 Golang 的 Filebeat 代替了 Shipper 部分的职能。又如 Scala 语言编写的边车代理 Linkerd,作为服务网格概念的提出者,却最终被 Envoy 所取代,其主要弱点之一也是由于 Java 虚拟机的资源消耗所带来的劣势。 - -虽然在云原生时代依然有很多适合 Java 发挥的领域,但是具备弹性与韧性、随时可以中断重启的微型服务的确已经形成了一股潮流,在逐步蚕食大型系统的领地。正是由于潮流趋势的改变,新一代的语言与技术尤其重视轻量化和快速响应能力,大多又重新回归到了原生语言(Native Language,如 Golang、Rust)之上。 - -## Java 的变革 - -面对挑战,Java 的开发者和社区都没有退缩,它们在各自的领域给出了很多优秀的解决方案,涌现了如 Quarkus、Micronaut、Helidon 等一大批以提升 Java 在云原生环境下的适应性为卖点的框架。 - -不过,今天我们的主题将聚焦在由 Java 官方本身所推进的项目上。在围绕 Java 25 周年的研讨和布道活动中,官方的设定是以“面向未来的变革”(Innovating for the Future)为基调,你有可能在此之前已经听说过其中某个(某些)项目的名字和改进点,但这里我们不仅关心这些项目改进的是什么,还更关心它们背后的动机与困难,带来的收益,以及要付出的代价。 - -![Innovating for the Future](http://img2020.cnblogs.com/blog/1546632/202105/1546632-20210516162303783-1665307539.png) - -### Project Leyden - -对于原生语言的挑战,最有力、最彻底的反击手段无疑是将字节码直接编译成可以脱离 Java 虚拟机的原生代码。 - -如果真的能够生成脱离 Java 虚拟机运行的原生程序,将意味着启动时间长的问题能够彻底解决,因为此时已经不存在初始化虚拟机和类加载的过程。也意味着程序马上就能达到最佳的性能,因为此时已经不存在即时编译器运行时编译,所有代码都是在编译期编译和优化好的(如下图所示)。 - -没有了 Java 虚拟机、即时编译器这些额外的部件,也就意味着能够省去它们原本消耗的那部分内存资源与镜像体积。 - -![Java Performance Matrices](http://img2020.cnblogs.com/blog/1546632/202105/1546632-20210516162420683-1866896226.png) - -但同时,这也是风险系数最高、实现难度最大的方案。 - -Java 并非没有尝试走过这条路。从 Java 2 之前的 GCJ(GNU Compiler for Java),到后来的 Excelsior JET,再到 2018 年 Oracle Labs 启动的 GraalVM 中的 SubstrateVM 模块,最后到 2020 年中期刚建立的 Leyden 项目,都在朝着提前编译(Ahead-of-Time Compilation,AOT)生成原生程序这个目标迈进。 - -**Java 支持提前编译最大的困难在于它是一门动态链接的语言**,它假设程序的代码空间是开放的(Open World),允许在程序的任何时候通过类加载器去加载新的类,作为程序的一部分运行。要进行提前编译,就必须放弃这部分动态性,假设程序的代码空间是封闭的(Closed World),所有要运行的代码都必须在编译期全部可知。 - -这一点不仅仅影响到了类加载器的正常运作,除了无法再动态加载外,反射(通过反射可以调用在编译期不可知的方法)、动态代理、字节码生成库(如 CGLib)等一切会运行时产生新代码的功能都不再可用。 - -如果将这些基础能力直接抽离掉,Hello world 还是能跑起来,但 Spring 肯定跑不起来,Hibernate 也跑不起来,大部分的生产力工具都跑不起来,整个 Java 生态中绝大多数上层建筑都会轰然崩塌。 - -要获得有实用价值的提前编译能力,只有依靠提前编译器、组件类库和开发者三方一起协同才可能办到。由于 Leyden 刚刚开始,几乎没有公开的资料,所以下面我是以 SubstrateVM 为目标对象进行的介绍: - -- 有一些功能,像反射这样的基础特性是不可能妥协的,折中的解决办法是由用户在编译期,以配置文件或者编译器参数的形式,明确告知编译器程序代码中有哪些方法是只通过反射来访问的,编译器将方法添加到静态编译的范畴之中。同理,所有使用到动态代理的地方,也必须在事先列明,在编译期就将动态代理的字节码全部生成出来。其他所有无法通过程序指针分析(Points-To Analysis)得到的信息,譬如程序中用到的资源、配置文件等等,也必须照此处理。 - -- 另一些功能,如动态生成字节码也十分常用,但用户自己往往无法得知那些动态字节码的具体信息,就只能由用到 CGLib、javassist 等库的程序去妥协放弃。在 Java 世界中也许最典型的场景就是 Spring 用 CGLib 来进行类增强,默认情况下,每一个 Spring 管理的 Bean 都要用到 CGLib。从 Spring Framework 5.2 开始增加了 @proxyBeanMethods 注解来排除对 CGLib 的依赖,仅使用标准的动态代理去增强类。 - -2019 年起,Pivotal 的 Spring 团队与 Oracle Labs 的 GraalVM 团队共同孵化了 Spring GraalVM Native 项目,这个目前仍处于 Experimental / Alpha 状态的项目,能够让程序先以传统方式运行(启动)一次,自动化地找出程序中的反射、动态代理的代码,代替用户向编译器提供绝大部分所需的信息,并能将允许启动时初始化的 Bean 在编译期就完成初始化,直接绕过 Spring 程序启动最慢的阶段。这样从启动到程序可以提供服务,耗时竟能够低于 0.1 秒。 - -![Spring Boot Startup Time](http://img2020.cnblogs.com/blog/1546632/202105/1546632-20210516162738205-1675648960.png) - -以原生方式运行后,缩短启动时间的效果立竿见影,一般会有数十倍甚至更高的改善,程序容量和内存消耗也有一定程度的下降。不过至少目前而言,程序的运行效率还是要弱于传统基于 Java 虚拟机的方式。 - -虽然即时编译器有编译时间的压力,但由于可以进行基于假设的激进优化和运行时性能度量的制导优化,使得即时编译器的效果仍要优于提前编译器。这方面需要 GraalVM 编译器团队的进一步努力,也需要从语言改进上入手,让 Java 变得更适合被编译器优化。 - -### Project Valhalla - -Java 语言上可感知的语法变化,多数来自于 Amber 项目,它的项目目标是持续优化语言生产力,近期(JDK 15、16)会有很多来自这个项目的特性,如 Records、Sealed Class、Pattern Matching、Raw String Literals 等实装到生产环境。 - -然而**语法不仅与编码效率相关,与运行效率也有很大关系**。“程序 = 代码 + 数据”这个提法至少在衡量运行效率上是合适的,无论是托管语言还是原生语言,最终产物都是处理器执行的指令流和内存存储的数据结构。Java、.NET、C、C++、Golang、Rust 等各种语言谁更快,取决于特定场景下,编译器生成指令流的优化效果,以及数据在内存中的结构布局。 - -Java 即时编译器的优化效果拔群,但是由于 Java“一切皆为对象”的前提假设,导致它在处理一系列不同类型的小对象时,内存访问性能很差。这点是 Java 在游戏、图形处理等领域一直难有建树的重要制约因素,也是 Java 建立 Valhalla 项目的目标初衷。 - -这里举个例子来说明此问题,如果我想描述空间里面若干条线段的集合,在 Java 中定义的代码会是这样的: -```java -public record Point(float x, float y, float z) {} -public record Line(Point start, Point end) {} -Line[] lines; -``` - -面向对象的内存布局中,对象标识符(Object Identity)存在的目的是为了允许在不暴露对象结构的前提下,依然可以引用其属性与行为,这是面向对象编程中多态性的基础。在 Java 中堆内存分配和回收、空值判断、引用比较、同步锁等一系列功能都会涉及到对象标识符,内存访问也是依靠对象标识符来进行链式处理的,譬如上面代码中的“若干条线段的集合”,在堆内存中将构成如下图的引用关系: - -![Object Identity / Memory Layout](http://img2020.cnblogs.com/blog/1546632/202105/1546632-20210516163041328-1799574835.png) - -计算机硬件经过 25 年的发展,内存与处理器虽然都在进步,但是内存延迟与处理器执行性能之间的冯诺依曼瓶颈(Von Neumann Bottleneck)不仅没有缩减,反而还在持续加大,“RAM Is the New Disk”已经从嘲讽梗逐渐成为了现实。 - -一次内存访问(将主内存数据调入处理器 Cache)大约需要耗费数百个时钟周期,而大部分简单指令的执行只需要一个时钟周期而已。因此,在程序执行性能这个问题上,如果编译器能减少一次内存访问,可能比优化掉几十、几百条其他指令都来得更有效果。 - -> 额外知识:冯诺依曼瓶颈 -> -> 不同处理器(现代处理器都集成了内存管理器,以前是在北桥芯片中)的内存延迟大概是 40-80 纳秒(ns,十亿分之一秒),而根据不同的时钟频率,一个时钟周期大概在 0.2-0.4 纳秒之间,如此短暂的时间内,即使真空中传播的光,也仅仅能够行进 10 厘米左右。 -> -> 数据存储与处理器执行的速度矛盾是冯诺依曼架构的主要局限性之一,1977 年的图灵奖得主 John Backus 提出了“冯诺依曼瓶颈”这个概念,专门用来描述这种局限性。 - -编译器的确在努力减少内存访问,从 JDK 6 起,HotSpot 的即时编译器就尝试通过逃逸分析来做标量替换(Scalar Replacement)和栈上分配(Stack Allocations)优化,基本原理是如果能通过分析,得知一个对象不会传递到方法之外,那就不需要真实地在对象中创建完整的对象布局,完全可以绕过对象标识符,将它拆散为基本的原生数据类型来创建,甚至是直接在栈内存中分配空间(HotSpot 并没有这样做),方法执行完毕后随着栈帧一起销毁掉。 - -不过,**逃逸分析是一种过程间优化(Interprocedural Optimization),非常耗时,也很难处理那些理论上有可能但实际不存在的情况**。相同的问题在 C、C++ 中却并不存在,上面场景中,程序员只要将 Point 和 Line 都定义为 struct 即可,C# 中也有 struct,是依靠 .NET 的值类型(Value Type)来实现的。 - -Valhalla 项目的核心改进就是提供类似的值类型支持,提供一个新的关键字(inline),让用户可以在不需要向方法外部暴露对象、不需要多态性支持、不需要将对象用作同步锁的场合中,将类标识为值类型。此时编译器就能够绕过对象标识符,以平坦的、紧凑的方式去为对象分配内存。 - -有了值类型的支持后,现在 Java 泛型中令人诟病的不支持原数据类型(Primitive Type)、频繁装箱问题也就随之迎刃而解。现在 Java 的包装类,理所当然地会以代表原生类型的值类型来重新定义,这样 Java 泛型的性能会得到明显的提升。因为此时 Integer 与 int 的访问,在机器层面看完全可以达到一致的效率。 - -### Project Loom - -Java 语言抽象出来隐藏了各种操作系统线程差异性的统一线程接口,这曾经是它区别于其他编程语言(C/C++ 表示有被冒犯到)的一大优势,不过,统一的线程模型不见得永远都是正确的。 - -Java 目前主流的线程模型是直接映射到操作系统内核上的 1:1 模型,这对于计算密集型任务这很合适,既不用自己去做调度,也利于一条线程跑满整个处理器核心。但对于 I/O 密集型任务,譬如访问磁盘、访问数据库占主要时间的任务,这种模型就显得成本高昂,主要在于内存消耗和上下文切换上: - -64 位 Linux 上 HotSpot 的线程栈容量默认是 1MB,线程的内核元数据(Kernel Metadata)还要额外消耗 2-16KB 内存,所以单个虚拟机的最大线程数量一般只会设置到 200 至 400 条,当程序员把数以百万计的请求往线程池里面灌时,系统即便能处理得过来,其中的切换损耗也相当可观。 - -Loom 项目的目标是让 Java 支持额外的 N:M 线程模型,请注意是“额外支持”,而不是像当年从绿色线程过渡到内核线程那样的直接替换,也不是像 Solaris 平台的 HotSpot 虚拟机那样通过参数让用户二选其一。 - -Loom 项目新增加一种“虚拟线程”(Virtual Thread,以前以 Fiber 为名进行宣传过,但因为要频繁解释啥是 Fiber,所以现在放弃了),本质上它是一种有栈协程(Stackful Coroutine),多条虚拟线程可以映射到同一条物理线程之中,在用户空间中自行调度,每条虚拟线程的栈容量也可由用户自行决定。 - -![Virtual Thread](http://img2020.cnblogs.com/blog/1546632/202105/1546632-20210516163513853-1945490240.png) - -同时,Loom 项目的另一个目标是要尽最大可能保持原有统一线程模型的交互方式,通俗地说就是原有的 Thread、J.U.C、NIO、Executor、Future、ForkJoinPool 等这些多线程工具,都应该能以同样的方式支持新的虚拟线程,原来多线程中你理解的概念、编码习惯大多数都能够继续沿用。 - -为此,虚拟线程将会与物理线程一样使用 java.lang.Thread 来进行抽象,只是在创建线程时用到的参数或者方法稍有不同(譬如给 Thread 增加一个 Thread.VIRTUAL_THREAD 参数,或者增加一个 startVirtualThread() 方法)。 - -这样现有的多线程代码迁移到虚拟线程中的成本就会变得很低,而代价就是 Loom 的团队必须做更多的工作以保证虚拟线程在大部分涉及到多线程的标准 API 中都能够兼容,甚至在调试器上虚拟线程与物理线程看起来都会有一致的外观。但很难全部都支持,譬如调用 JNI 的本地栈帧就很难放到虚拟线程上,所以一旦遇到本地方法,虚拟线程就会被绑定(Pinned)到一条物理线程上。 - -Loom 的另一个重点改进是支持结构化并发(Structured Concurrency),这是 2016 年才提出的新的并发编程概念,但很快就被诸多编程语言所吸纳。它是指程序的并发行为会与代码的结构对齐,譬如以下代码所示,按照传统的编程观念,如果没有额外的处理(譬如无中生有地弄一个 await 关键字),那在 task1 和 task2 提交之后,程序应该继续向下执行: -```java -ThreadFactory factory = Thread.builder().virtual().factory(); -try (var executor = Executors.newThreadExecutor(factory)) { - executor.submit(task1); - executor.submit(task2); -} // blocks and waits -``` - -但是在结构化并发的支持下,只有两个并行启动的任务线程都结束之后,程序才会继续向下执行,很好地以同步的编码风格,来解决异步的执行问题。事实上,“Code like sync,Work like async”正是 Loom 简化并发编程的核心理念。 - -### Project Portola - -Portola 项目的目标是将 OpenJDK 向 Alpine Linux 移植。Alpine Linux 是许多 Docker 容器首选的基础镜像,因为它只有 5 MB 大小,比起其他 Cent OS、Debain 等动辄一百多 MB 的发行版来说,更适合用于容器环境。 - -不过 Alpine Linux 为了尽量瘦身,默认是用 musl 作为 C 标准库的,而非传统的 glibc(GNU C library)。因此要以 Alpine Linux 为基础制作 OpenJDK 镜像,必须先安装 glibc,此时基础镜像大约有 12 MB。Portola 计划将 OpenJDK 的上游代码移植到 musl,并通过兼容性测试。使用 Portola 制作的标准 Java SE 13 镜像仅有 41 MB,不仅远低于 Cent OS 的 OpenJDK(大约 396 MB),也要比官方的 slim 版(约 200 MB)小得多。 - - -## Java 的未来 - -云原生时代,Java 技术体系的许多前提假设都受到了挑战,“一次编译,到处运行”“面向长时间大规模程序而设计”“从开放的代码空间中动态加载”“一切皆为对象”“统一线程模型”等等。技术发展迭代不会停歇,没有必要坚持什么“永恒的真理”,旧的原则被打破,只要合理,便是创新。 - -Java 语言意识到了挑战,也意识到了要面向未来而变革。文中提到的这些项目,Amber 和 Portola 已经明确会在 2021 年 3 月的 Java 16 中发布,至少也会达到 Feature Preview 的程度: - -- JEP 394:Pattern Matching for instanceof -- JEP 395:Records -- JEP 397:Sealed Classes -- JEP 386:Alpine Linux Port - -至于更受关注,同时也是难度更高的 Valhalla 和 Loom 项目,目前仍然没有明确的版本计划信息,尽管它们已经开发了数年时间,非常希望能够赶在 Java 17 这个 LTS 版本中面世,但前路还是困难重重。 - -至于难度最高、创建时间最晚的 Leyden 项目,目前还完全处于特性讨论阶段,连个胚胎都算不上。对于 Java 的原生编译,我们中短期内只可能寄希望于 Oracle 的 GraalVM。 - -未来一段时间,是 Java 重要的转型窗口期,如果作为下一个 LTS 版的 Java 17,能够成功集 Amber、Portola、Valhalla、Loom 和 Panama(用于外部函数接口访问,本文没有提到)的新能力、新特性于一身,GraalVM 也能给予足够强力支持的话,那么 Java 17 LTS 大概率会是一个里程碑式的版本,带领着整个 Java 生态从大规模服务端应用,向新的云原生时代软件系统转型。甚至可能成为从面向嵌入式设备与浏览器 Web Applets 的 Java 1,到确立现代 Java 语言方向(Java SE/EE/ME 和 JavaCard)雏形的 Java 2 转型那样的里程碑。 - -但是,如果 Java 不能加速自己的发展步伐,那么它由强大生态所构建的护城河终究会消耗殆尽,被 Golang、Rust 这样的新生语言,以及 C、C++、C#、Python 等老对手蚕食掉很大一部分市场份额,甚至会被迫从“天下第一”编程语言的宝座中退位。 - -Java 的未来是继续向前,再攀高峰,还是由盛转衰,锋芒挫缩,你我拭目以待。 diff --git a/Java/resultmap-in-mybatis-plus.md b/Java/resultmap-in-mybatis-plus.md deleted file mode 100644 index 876d180..0000000 --- a/Java/resultmap-in-mybatis-plus.md +++ /dev/null @@ -1,370 +0,0 @@ -# MyBatis-Plus中如何使用ResultMap - -> [MyBatis-Plus](https://baomidou.com/) (简称`MP`)是一个`MyBatis`的增强工具,在`MyBatis`的基础上只做增强不做改变,为简化开发、提高效率而生。 - -`MyBatis-Plus`对`MyBatis`基本零侵入,完全可以与`MyBatis`混合使用,这点很赞。 - -在涉及到关系型数据库增删查改的业务时,我比较喜欢用`MyBatis-Plus`,开发效率极高。具体的使用可以参考官网,或者自己上手摸索感受一下。 - -下面简单总结一下在`MyBatis-Plus`中如何使用`ResultMap`。 - -## 问题说明 - -先看个例子: - -有如下两张表: -```sql -create table tb_book -( - id bigint primary key, - name varchar(32), - author varchar(20) -); - -create table tb_hero -( - id bigint primary key, - name varchar(32), - age int, - skill varchar(32), - bid bigint -); -``` -其中,`tb_hero`中的`bid`关联`tb_book`表的`id`。 - -下面先看`Hero`实体类的代码,如下: - -```java -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@TableName("tb_hero") -@JsonInclude(JsonInclude.Include.NON_NULL) -public class Hero { - - @TableId("id") - private Long id; - - @TableField(value = "name", keepGlobalFormat = true) - private String name; - - @TableField(value = "age", keepGlobalFormat = true) - private Integer age; - - @TableField(value = "skill", keepGlobalFormat = true) - private String skill; - - @TableField(value = "bid", keepGlobalFormat = true) - private Long bookId; - - // ********************************* - // 数据库表中不存在以下字段(表join时会用到) - // ********************************* - - @TableField(value = "book_name", exist = false) - private String bookName; - - @TableField(value = "author", exist = false) - private String author; -} -``` - -注意了,我特地把`tb_hero`表中的`bid`字段映射成实体类`Hero`中的`bookId`属性。 - -- 测试`BaseMapper`中内置的`insert()`方法或者`IService`中的`save()`方法 - -`MyBatis-Plus`打印出的`SQL`为: - -```bash -==> Preparing: INSERT INTO tb_hero ( id, "name", "age", "skill", "bid" ) VALUES ( ?, ?, ?, ?, ? ) -==> Parameters: 1589788935356416(Long), 阿飞(String), 18(Integer), 天下第一快剑(String), 1(Long) -``` - -没毛病, `MyBatis-Plus`会根据`@TableField`指定的映射关系,生成对应的`SQL`。 - -- 测试`BaseMapper`中内置的`selectById()`方法或者`IService`中的`getById()`方法 - -`MyBatis-Plus`打印出的`SQL`为: - -```bash -==> Preparing: SELECT id,"name","age","skill","bid" AS bookId FROM tb_hero WHERE id=? -==> Parameters: 1(Long) -``` - -也没毛病,可以看到生成的`SELECT`中把`bid`做了别名`bookId`。 - - -- 测试自己写的SQL - -比如现在我想连接`tb_hero`与`tb_book`这两张表,如下: -```java -@Mapper -@Repository -public interface HeroMapper extends BaseMapper { - @Select({"SELECT tb_hero.*, tb_book.name as book_name, tb_book.author" + - " FROM tb_hero" + - " LEFT JOIN tb_book" + - " ON tb_hero.bid = tb_book.id" + - " ${ew.customSqlSegment}"}) - IPage pageQueryHero(@Param(Constants.WRAPPER) Wrapper queryWrapper, - Page page); -} -``` - -查询`MyBatis-Plus`打印出的`SQL`为: - -```bash -==> Preparing: SELECT tb_hero.*, tb_book.name AS book_name, tb_book.author FROM tb_hero LEFT JOIN tb_book ON tb_hero.bid = tb_book.id WHERE ("bid" = ?) ORDER BY id ASC LIMIT ? OFFSET ? -==> Parameters: 2(Long), 1(Long), 1(Long) -``` - -SQL没啥问题,过滤与分页也都正常,但是此时你会发现`bookId`属性为`null`,如下: - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211122141925905-1394428265.png) - -为什么呢? - -调用`BaseMapper`中内置的`selectById()`方法并没有出现这种情况啊??? - -回过头来再对比一下在`HeroMapper`中自己定义的查询与`MyBatis-Plus`自带的`selectById()`有啥不同,还记得上面的刚刚的测试吗,生成的SQL有啥不同? - -原来,`MyBatis-Plus`为`BaseMapper`中内置的方法生成SQL时,会把`SELECT`子句中`bid`做别名`bookId`,而自己写的查询`MyBatis-Plus`并不会帮你修改`SELECT`子句,也就导致`bookId`属性为`null`。 - -## 解决方法 - -- 方案一:表中的字段与实体类的属性严格保持一致(字段有下划线则属性用驼峰表示) - -在这里就是`tb_hero`表中的`bid`字段映射成实体类`Hero`中的`bid`属性。这样当然可以解决问题,但不是本篇讲的重点。 - -- 方案二:把自己写的`SQL`中`bid`做别名`bookId` - -- 方案三:使用`@ResultMap`,这是此篇的重点 - -在`@TableName`设置`autoResultMap = true` - -```java -@TableName(value = "tb_hero", autoResultMap = true) -public class Hero { - -} -``` - -然后在自定义查询中添加`@ResultMap`注解,如下: - -```java -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.ResultMap; -import org.apache.ibatis.annotations.Select; -import org.springframework.stereotype.Repository; - -@Mapper -@Repository -public interface HeroMapper extends BaseMapper { - @ResultMap("mybatis-plus_Hero") - @Select({"SELECT tb_hero.*, tb_book.name as book_name, tb_book.author" + - " FROM tb_hero" + - " LEFT JOIN tb_book" + - " ON tb_hero.bid = tb_book.id" + - " ${ew.customSqlSegment}"}) - IPage pageQueryHero(@Param(Constants.WRAPPER) Wrapper queryWrapper, - Page page); -} -``` -这样,也能解决问题。 - -下面简单看下源码,`@ResultMap("mybatis-plus_实体类名")`怎么来的。 - -详情见: `com.baomidou.mybatisplus.core.metadata.TableInfo#initResultMapIfNeed()` - -```java -/** - * 自动构建 resultMap 并注入(如果条件符合的话) - */ -void initResultMapIfNeed() { - if (autoInitResultMap && null == resultMap) { - String id = currentNamespace + DOT + MYBATIS_PLUS + UNDERSCORE + entityType.getSimpleName(); - List resultMappings = new ArrayList<>(); - if (havePK()) { - ResultMapping idMapping = new ResultMapping.Builder(configuration, keyProperty, StringUtils.getTargetColumn(keyColumn), keyType) - .flags(Collections.singletonList(ResultFlag.ID)).build(); - resultMappings.add(idMapping); - } - if (CollectionUtils.isNotEmpty(fieldList)) { - fieldList.forEach(i -> resultMappings.add(i.getResultMapping(configuration))); - } - ResultMap resultMap = new ResultMap.Builder(configuration, id, entityType, resultMappings).build(); - configuration.addResultMap(resultMap); - this.resultMap = id; - } -} -``` -注意看上面的字符串`id`的构成,你应该可以明白。 - -思考: 这种方式的`ResultMap`默认是强绑在一个`@TableName`上的,如果是某个聚合查询或者查询的结果并非对应一个真实的表怎么办呢?有没有更优雅的方式? - -## 自定义@AutoResultMap注解 - -基于上面的思考,我做了下面简单的实现: - -- 自定义@AutoResultMap注解 - -```java -import java.lang.annotation.*; - -/** - * 使用@AutoResultMap注解的实体类 - * 自动生成{auto.mybatis-plus_类名}为id的resultMap - * {@link MybatisPlusConfig#initAutoResultMap()} - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface AutoResultMap { - -} -``` - -- 启动时扫描@AutoResultMap注解的实体类 - -```java -package com.bytesfly.mybatis.config; - -import cn.hutool.core.util.ClassUtil; -import cn.hutool.core.util.ReflectUtil; -import com.baomidou.mybatisplus.annotation.DbType; -import com.baomidou.mybatisplus.core.metadata.TableInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; -import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; -import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils; -import com.bytesfly.mybatis.annotation.AutoResultMap; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.builder.MapperBuilderAssistant; -import org.mybatis.spring.SqlSessionTemplate; -import org.mybatis.spring.annotation.MapperScan; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -import javax.annotation.PostConstruct; -import java.util.Set; - -/** - * 可添加一些插件 - */ -@Configuration -@EnableTransactionManagement(proxyTargetClass = true) -@MapperScan(basePackages = "com.bytesfly.mybatis.mapper") -@Slf4j -public class MybatisPlusConfig { - - @Autowired - private SqlSessionTemplate sqlSessionTemplate; - - /** - * 分页插件(根据jdbcUrl识别出数据库类型, 自动选择适合该方言的分页插件) - * 相关使用说明: https://baomidou.com/guide/page.html - */ - @Bean - public MybatisPlusInterceptor mybatisPlusInterceptor(DataSourceProperties dataSourceProperties) { - - String jdbcUrl = dataSourceProperties.getUrl(); - DbType dbType = JdbcUtils.getDbType(jdbcUrl); - - MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); - interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType)); - return interceptor; - } - - /** - * @AutoResultMap注解的实体类自动构建resultMap并注入 - */ - @PostConstruct - public void initAutoResultMap() { - try { - log.info("--- start register @AutoResultMap ---"); - - String namespace = "auto"; - - String packageName = "com.bytesfly.mybatis.model.db.resultmap"; - Set> classes = ClassUtil.scanPackageByAnnotation(packageName, AutoResultMap.class); - - org.apache.ibatis.session.Configuration configuration = sqlSessionTemplate.getConfiguration(); - - for (Class clazz : classes) { - MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); - assistant.setCurrentNamespace(namespace); - TableInfo tableInfo = TableInfoHelper.initTableInfo(assistant, clazz); - - if (!tableInfo.isAutoInitResultMap()) { - // 设置 tableInfo的autoInitResultMap属性 为 true - ReflectUtil.setFieldValue(tableInfo, "autoInitResultMap", true); - // 调用 tableInfo#initResultMapIfNeed() 方法,自动构建 resultMap 并注入 - ReflectUtil.invoke(tableInfo, "initResultMapIfNeed"); - } - } - - log.info("--- finish register @AutoResultMap ---"); - } catch (Throwable e) { - log.error("initAutoResultMap error", e); - System.exit(1); - } - } -} -``` - -关键代码其实没有几行,耐心看下应该不难懂。 - -- 使用@AutoResultMap注解 - -还是用例子来说明更直观。 - -下面是一个聚合查询: - -```java -@Mapper -@Repository -public interface BookMapper extends BaseMapper { - - @ResultMap("auto.mybatis-plus_BookAgg") - @Select({"SELECT tb_book.id, max(tb_book.name) as name, array_agg(distinct tb_hero.id order by tb_hero.id asc) as hero_ids" + - " FROM tb_hero" + - " INNER JOIN tb_book" + - " ON tb_hero.bid = tb_book.id" + - " GROUP BY tb_book.id"}) - List agg(); -} -``` - -其中`BookAgg`的定义如下,在实体类上使用了`@AutoResultMap`注解: - -```java -@Getter -@Setter -@NoArgsConstructor -@AutoResultMap -public class BookAgg { - - @TableId("id") - private Long bookId; - - @TableField("name") - private String bookName; - - @TableField("hero_ids") - private Object heroIds; -} -``` - -完整代码见: [https://github.com/bytesfly/springboot-demo/tree/master/springboot-mybatis-plus](https://github.com/bytesfly/springboot-demo/tree/master/springboot-mybatis-plus) diff --git a/Java/testable-mock.md b/Java/testable-mock.md deleted file mode 100644 index 1616dba..0000000 --- a/Java/testable-mock.md +++ /dev/null @@ -1,358 +0,0 @@ -# 换种思路写Mock,让单元测试更简单 - - -## 开篇引入 - - -> 单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。在定义Mock方法时,开发者真正关心的只有一件事:"这个调用,在测试的时候要换成那个假的Mock方法"。 - - - -然而当下主流的Mock框架在实现Mock功能时,需要开发者操心的事情实在太多:Mock框架如何初始化、与所用的单元测试框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。 - - -周末,在翻github上alibaba的开源项目时,无意间看到了下面这个特立独行的轻量Mock工具。当前知道这个工具的人应该很少,star人数28(包括本人在内),另外我留意了一下该项目在github上第一次提交代码时间是2020年5月9日。 - - -项目地址:[https://github.com/alibaba/testable-mock](https://github.com/alibaba/testable-mock) -文档:[https://alibaba.github.io/testable-mock/](https://alibaba.github.io/testable-mock/) - - -> 换种思路写Mock,让单元测试更简单。无需初始化,不挑测试框架,甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法,也甭管要换的对象是怎么创建的。写好Mock方法,加个@TestableMock注解,一切统统搞定。 - - - -这是 `README` 上的描述。扫了一眼项目描述与目录结构后,就抵制不住诱惑,快速上手玩了一下。于是,就有了这篇划水博客,让看到的朋友也心痒一下(●´ω`●)。当然,最重要的是如果确实好用的话,可以在实际项目中用起来,这样就不再反感需要Mock的单元测试了。 - - -## 快速上手 - - -完整代码见本人github:[https://github.com/bytesfly/less/tree/master/less-alibaba/less-testable](https://github.com/bytesfly/less/tree/master/less-alibaba/less-testable) - - -这里有一个 `WeatherApi` 的接口,通过调用第三方接口查询天气情况,如下: -```java -import com.github.itwild.less.base.http.feign.WeatherExample; -import feign.Param; -import feign.RequestLine; - -public interface WeatherApi { - - @RequestLine("GET /api/weather/city/{city_code}") - WeatherExample.Response query(@Param("city_code") String cityCode); -} -``` - - -`CityWeather` 查询具体城市的天气,如下: -```java -import cn.hutool.core.map.MapUtil; -import com.github.itwild.less.base.http.feign.WeatherExample; -import feign.Feign; -import feign.jackson.JacksonDecoder; -import feign.jackson.JacksonEncoder; - -import java.util.HashMap; -import java.util.Map; - -public class CityWeather { - - private static final String API_URL = "http://t.weather.itboy.net"; - - private static final String BEI_JING = "101010100"; - private static final String SHANG_HAI = "101020100"; - private static final String HE_FEI = "101220101"; - - public static final Map CITY_CODE = MapUtil.builder(new HashMap()) - .put(BEI_JING, "北京市") - .put(SHANG_HAI, "上海市") - .put(HE_FEI, "合肥市") - .build(); - - private static WeatherApi weatherApi = Feign.builder() - .encoder(new JacksonEncoder()) - .decoder(new JacksonDecoder()) - .target(WeatherApi.class, API_URL); - - public String queryShangHaiWeather() { - WeatherExample.Response response = weatherApi.query(SHANG_HAI); - return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); - } - - private String queryHeFeiWeather() { - WeatherExample.Response response = weatherApi.query(HE_FEI); - return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); - } - - public static String queryBeiJingWeather() { - WeatherExample.Response response = weatherApi.query(BEI_JING); - return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); - } - - public static void main(String[] args) { - CityWeather cityWeather = new CityWeather(); - - String shanghai = cityWeather.queryShangHaiWeather(); - String hefei = cityWeather.queryHeFeiWeather(); - String beijing = CityWeather.queryBeiJingWeather(); - - System.out.println(shanghai); - System.out.println(hefei); - System.out.println(beijing); - } -``` -运行 `main` 方法,输出如下: -```bash -上海市: 不要被阴云遮挡住好心情 -合肥市: 不要被阴云遮挡住好心情 -北京市: 阴晴之间,谨防紫外线侵扰 -``` -相信大多数人编写单元测试时,遇到这种依赖第三方资源时,可能就有点反感写单元测试了。 -下面看看有了 `testable-mock` 工具,如何编写单元测试? -`CityWeatherTest` 文件如下: -```java -import com.alibaba.testable.core.accessor.PrivateAccessor; -import com.alibaba.testable.core.annotation.TestableMock; -import com.alibaba.testable.processor.annotation.EnablePrivateAccess; -import com.github.itwild.less.base.http.feign.WeatherExample; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -@EnablePrivateAccess -public class CityWeatherTest { - - @TestableMock(targetMethod = "query") - public WeatherExample.Response query(WeatherApi self, String cityCode) { - WeatherExample.Response response = new WeatherExample.Response(); - // mock天气接口调用返回的结果 - response.setCityInfo(new WeatherExample.CityInfo().setCity( - CityWeather.CITY_CODE.getOrDefault(cityCode, cityCode))); - response.setData(new WeatherExample.Data().setYesterday( - new WeatherExample.Forecast().setNotice("this is from mock"))); - return response; - } - - CityWeather cityWeather = new CityWeather(); - - /** - * 测试 public方法调用 - */ - @Test - public void test_public() { - String shanghai = cityWeather.queryShangHaiWeather(); - - System.out.println(shanghai); - assertEquals("上海市: this is from mock", shanghai); - } - - /** - * 测试 private方法调用 - */ - @Test - public void test_private() { - String hefei = (String) PrivateAccessor.invoke(cityWeather, "queryHeFeiWeather"); - - System.out.println(hefei); - assertEquals("合肥市: this is from mock", hefei); - } - - /** - * 测试 静态方法调用 - */ - @Test - public void test_static() { - String beijing = CityWeather.queryBeiJingWeather(); - - System.out.println(beijing); - assertEquals("北京市: this is from mock", beijing); - } -} -``` -运行单元测试,输出如下: -```bash -合肥市: this is from mock -上海市: this is from mock -北京市: this is from mock -``` -从运行结果不难发现,依赖第三方接口的 `query` 方法已经被仅仅加了个 `TestableMock` 注解的方法Mock了。也就是说达到了预期的Mock效果,而且代码优雅易读。 -## 实现原理 - - -那么,这优雅易读的背后到底隐藏着什么秘密呢? - - -相信对这方面有些了解的朋友或多或少也猜到了,没错,正是字节码增强技术!!! -```java -package com.alibaba.testable.agent; - -import com.alibaba.testable.agent.transformer.TestableClassTransformer; -import java.lang.instrument.Instrumentation; - -/** - * Agent entry, dynamically modify the byte code of classes under testing - * @author flin - */ -public class PreMain { - - public static void premain(String agentArgs, Instrumentation inst) { - parseArgs(agentArgs); - inst.addTransformer(new TestableClassTransformer()); - } -} -``` -```java -package com.alibaba.testable.agent.handler; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; - -import java.io.IOException; - -/** - * @author flin - */ -abstract public class BaseClassHandler implements Opcodes { - - public byte[] getBytes(byte[] classFileBuffer) throws IOException { - ClassReader cr = new ClassReader(classFileBuffer); - ClassNode cn = new ClassNode(); - cr.accept(cn, 0); - transform(cn); - ClassWriter cw = new ClassWriter( 0); - cn.accept(cw); - return cw.toByteArray(); - } - - /** - * Transform class byte code - * @param cn original class node - */ - abstract protected void transform(ClassNode cn); - -} -``` -追一下源码,可见,该Mock工具借助了ASM Core API来修改字节码。上面也提到了,该项目在github上开源出来的时间并不长,核心代码并不多,认真看应该能看懂,主要是有些朋友可能从来没有了解过字节码增强技术。这里推荐美团技术团队的一篇字节码增强技术相关的文章,[https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html](https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html),相信有了这样的基础,回过头来再看看 `TestableMock` 的源码会轻松许多。 - - -本篇博客并不会过多探究字节码增强技术的细节,顶多算是抛砖引玉,目的是让读者知道有这么一个优雅的Mock工具,另外字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态,这样就能在开发中减少冗余代码,提高开发效率。顺便提一句,我们平时使用的AOP(Cglib就是基于ASM的)也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。 - - -虽然这篇不讲修改字节码的操作细节,但我还是想让读者直观地看到增强后的字节码(class文件)是什么样子的,说白了就是到底把我写的代码在运行时修改成了啥???于是,我把运行时增强过的字节码重新写入了文件,然后使用反编译工具(拖到IDEA中即可)观察被修改后的源码。 - - -运行时(即增强后的)CityWeatherTest.class反编译后如下: -```java -import com.alibaba.testable.core.accessor.PrivateAccessor; -import com.alibaba.testable.core.annotation.TestableMock; -import com.alibaba.testable.core.util.InvokeRecordUtil; -import com.alibaba.testable.processor.annotation.EnablePrivateAccess; -import com.github.itwild.less.base.http.feign.WeatherExample.CityInfo; -import com.github.itwild.less.base.http.feign.WeatherExample.Data; -import com.github.itwild.less.base.http.feign.WeatherExample.Forecast; -import com.github.itwild.less.base.http.feign.WeatherExample.Response; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -@EnablePrivateAccess -public class CityWeatherTest { - CityWeather cityWeather = new CityWeather(); - public static CityWeatherTest _testableInternalRef; - public static CityWeatherTest _testableInternalRef; - - public CityWeatherTest() { - } - - @TestableMock( - targetMethod = "query" - ) - public Response query(WeatherApi var1, String cityCode) { - InvokeRecordUtil.recordMockInvoke(new Object[]{var1, cityCode}, false); - InvokeRecordUtil.recordMockInvoke(new Object[]{var1, cityCode}, false); - Response response = new Response(); - response.setCityInfo((new CityInfo()).setCity((String)CityWeather.CITY_CODE.getOrDefault(cityCode, cityCode))); - response.setData((new Data()).setYesterday((new Forecast()).setNotice("this is from mock"))); - return response; - } - - @Test - public void test_public() { - _testableInternalRef = this; - _testableInternalRef = this; - String shanghai = this.cityWeather.queryShangHaiWeather(); - System.out.println(shanghai); - Assertions.assertEquals("上海市: this is from mock", shanghai); - } - - @Test - public void test_private() { - _testableInternalRef = this; - _testableInternalRef = this; - String hefei = (String)PrivateAccessor.invoke(this.cityWeather, "queryHeFeiWeather", new Object[0]); - System.out.println(hefei); - Assertions.assertEquals("合肥市: this is from mock", hefei); - } - - @Test - public void test_static() { - _testableInternalRef = this; - _testableInternalRef = this; - String beijing = CityWeather.queryBeiJingWeather(); - System.out.println(beijing); - Assertions.assertEquals("北京市: this is from mock", beijing); - } -} -``` - - -运行时(即增强后的)CityWeather.class反编译后如下: -```java -import cn.hutool.core.map.MapUtil; -import com.github.itwild.less.base.http.feign.WeatherExample.Response; -import feign.Feign; -import feign.jackson.JacksonDecoder; -import feign.jackson.JacksonEncoder; -import java.util.HashMap; -import java.util.Map; - -public class CityWeather { - private static final String API_URL = "http://t.weather.itboy.net"; - private static final String BEI_JING = "101010100"; - private static final String SHANG_HAI = "101020100"; - private static final String HE_FEI = "101220101"; - public static final Map CITY_CODE = MapUtil.builder(new HashMap()).put("101010100", "北京市").put("101020100", "上海市").put("101220101", "合肥市").build(); - private static WeatherApi weatherApi = (WeatherApi)Feign.builder().encoder(new JacksonEncoder()).decoder(new JacksonDecoder()).target(WeatherApi.class, "http://t.weather.itboy.net"); - - public CityWeather() { - } - - public String queryShangHaiWeather() { - Response response = CityWeatherTest._testableInternalRef.query(weatherApi, "101020100"); - return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); - } - - private String queryHeFeiWeather() { - Response response = CityWeatherTest._testableInternalRef.query(weatherApi, "101220101"); - return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); - } - - public static String queryBeiJingWeather() { - Response response = CityWeatherTest._testableInternalRef.query(weatherApi, "101010100"); - return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); - } - - public static void main(String[] args) { - CityWeather cityWeather = new CityWeather(); - String shanghai = cityWeather.queryShangHaiWeather(); - String hefei = cityWeather.queryHeFeiWeather(); - String beijing = queryBeiJingWeather(); - System.out.println(shanghai); - System.out.println(hefei); - System.out.println(beijing); - } -} -``` -原来,运行时把调用到 `query` 方法的实现都换成了自己Mock的代码。 \ No newline at end of file diff --git a/LeetCode/0020-valid-parentheses.md b/LeetCode/0020-valid-parentheses.md deleted file mode 100644 index db70e0b..0000000 --- a/LeetCode/0020-valid-parentheses.md +++ /dev/null @@ -1,121 +0,0 @@ -# [20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses) - -## 题目描述 - - - -给定一个只包括 `'(',')','{','}','[',']'` 的字符串`s`,判断字符串是否有效。 - -

有效字符串需满足:

- -1. 左括号必须用相同类型的右括号闭合。 -2. 左括号必须以正确的顺序闭合。 - -

- -

示例 1:

- -```bash -输入:s = "()" -输出:true -``` - -

示例 2:

- -```bash -输入:s = "()[]{}" -输出:true -``` - -

示例 3:

- -```bash -输入:s = "(]" -输出:false -``` - -

示例 4:

- -```bash -输入:s = "([)]" -输出:false -``` - -

示例 5:

- -```bash -输入:s = "{[]}" -输出:true -``` - -## 分析 - - - -`last-in-first-out`,正好可用`栈`(`LIFO`)这种常见的数据结构来实现。 - -遍历字符串中的字符,遇到左括号则进栈,遇到右括号则弹出栈顶字符,如不匹配当前字符则直接返回,如匹配继续往下遍历。 - -注意,遍历完所有字符后,空栈才能表示字符串是符合规则的。 - - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public boolean isValid(String s) { - if (s == null || s.isEmpty()) { - return false; - } - Map charMap = new HashMap<>(); - charMap.put(')', '('); - charMap.put('}', '{'); - charMap.put(']', '['); - - Set leftChars = new HashSet<>(charMap.values()); - LinkedList stack = new LinkedList<>(); - - char[] chars = s.toCharArray(); - for (char ch : chars) { - if (leftChars.contains(ch)) { - stack.push(ch); - } else if (stack.isEmpty() || !stack.pop().equals(charMap.get(ch))) { - return false; - } - } - - return stack.isEmpty(); - } -} -``` - -### **Python3** - - - -```python -class Solution: - def isValid(self, s: str) -> bool: - if not s: - return False - - d = {')': '(', '}': '{', ']': '['} - left_chars = set(d.values()) - - stack = [] - for char in s: - if char in left_chars: - stack.append(char) - elif not stack or stack.pop() != d[char]: - return False - - return not stack -``` - - \ No newline at end of file diff --git a/LeetCode/0027-remove-element.md b/LeetCode/0027-remove-element.md deleted file mode 100644 index 115b1cc..0000000 --- a/LeetCode/0027-remove-element.md +++ /dev/null @@ -1,114 +0,0 @@ -# [27. 移除元素](https://leetcode-cn.com/problems/remove-element) - -## 题目描述 - - - -给你一个数组`nums`和一个值`val`,你需要`原地`移除所有数值等于`val`的元素,并返回移除后数组的新长度。 - -不要使用额外的数组空间,你必须仅使用`O(1)`额外空间并`原地修改输入数组`。 - -

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

- -

- -

说明:

- -

为什么返回数值是整数,但输出的答案是数组呢?

- -

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

- -

你可以想象内部操作如下:

- -```java -// nums是以“引用”方式传递的。也就是说,不对实参作任何拷贝 -int len = removeElement(nums, val); - -// 在函数里修改输入数组对于调用者是可见的。 -// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 -for (int i = 0; i < len; i++) { - print(nums[i]); -} -``` - -

- -

示例 1:

- -```bash -输入:nums = [3,2,2,3], val = 3 -输出:2, nums = [2,2] -解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。 -``` - -

示例 2:

- -```bash -输入:nums = [0,1,2,2,3,0,4,2], val = 2 -输出:5, nums = [0,1,4,0,3] -解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。 -``` - -

- -

提示:

- -- 0 <= nums.length <= 100 -- 0 <= nums[i] <= 50 -- 0 <= val <= 100 - -## 分析 - - - -顺序遍历数组,遇到可以删除的元素直接扔到数组的末尾,这样数组前面的元素就是保留下来的。 - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public int removeElement(int[] nums, int val) { - int i = 0; - int j = nums.length - 1; - while (i <= j) { - if (nums[i] != val) { - i++; - } else { - int temp = nums[j]; - nums[j] = nums[i]; - nums[i] = temp; - - j--; - } - } - return i; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def removeElement(self, nums: List[int], val: int) -> int: - i = 0 - j = len(nums) - 1 - while i <= j: - if nums[i] != val: - i += 1 - else: - nums[i], nums[j] = nums[j], nums[i] - j -= 1 - - return i -``` - - \ No newline at end of file diff --git a/LeetCode/0045-jump-game-ii.md b/LeetCode/0045-jump-game-ii.md deleted file mode 100644 index 5fb3efa..0000000 --- a/LeetCode/0045-jump-game-ii.md +++ /dev/null @@ -1,156 +0,0 @@ -# [45. 跳跃游戏 II](https://leetcode-cn.com/problems/jump-game-ii) - -## 题目描述 - - - -

给定一个非负整数数组,你最初位于数组的第一个位置。

- -

数组中的每个元素代表你在该位置可以跳跃的最大长度。

- -

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

- -

示例:

- -```bash -输入: [2,3,1,1,4] -输出: 2 -解释: 跳到最后一个位置的最小跳跃数是 2。 - 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 -``` - -

说明:

- -

假设你总是可以到达数组的最后一个位置。

- -## 分析 - - - -贪心算法,详细可参考: [代码随想录](https://programmercarl.com/0045.%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8FII.html) - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public int jump(int[] nums) { - if (nums == null || nums.length <= 1) { - return 0; - } - // 跳跃次数 - int count = 0; - // 当前的覆盖最大区域 - int curDistance = 0; - // 最大的覆盖区域 - int maxDistance = 0; - - for (int i = 0; i < nums.length; i++) { - // 在可覆盖区域内更新最大的覆盖区域 - maxDistance = Math.max(maxDistance, i + nums[i]); - // 说明当前一步,再跳一步就到达了末尾 - if (maxDistance >= nums.length - 1) { - count++; - break; - } - // 走到当前覆盖的最大区域时,更新下一步可达的最大区域 - if (i == curDistance) { - curDistance = maxDistance; - count++; - } - } - - return count; - } -} -``` - -这里顺便也保留自己用动态规划思路的实现(虽然不够巧妙,但也通过了): -```java -class Solution { - public int jump(int[] nums) { - if (nums.length == 1) { - return 0; - } - // 记录对应位置到达最后一个下标需要的最小跳跃数 - // 初始值都为0,表示不知道能否到达最后一个下标 - int[] history = new int[nums.length]; - return jump(nums, 0, history); - } - - public int jump(int[] nums, int startIdx, int[] history) { - int count = history[startIdx]; - if (count == -1) { - return -1; - } - int maxStep = nums[startIdx]; - if ((startIdx + maxStep) >= (nums.length - 1)) { - // 从当前位置可以直接到达最后一个下标 - count++; - return count; - } - Integer minCount = null; - for (int i = maxStep; i >= 1; i--) { - // 先看是否已经记录过 - int jumpCount = history[startIdx + i]; - if (jumpCount == 0) { - // 尝试往前跳跃长度i - jumpCount = jump(nums, startIdx + i, history); - history[startIdx + i] = jumpCount; - } - - if (jumpCount > 0 && (minCount == null || jumpCount < minCount)) { - minCount = jumpCount; - } - } - if (minCount == null) { - return -1; - } - return count + 1 + minCount; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def jump(self, nums: List[int]) -> int: - if not nums: - return 0 - - length = len(nums) - - if length <= 1: - return 0 - - # 跳跃次数 - count = 0 - # 当前的覆盖最大区域 - cur_distance = 0 - # 最大的覆盖区域 - max_distance = 0 - - for i in range(length): - # 在可覆盖区域内更新最大的覆盖区域 - max_distance = max(max_distance, i + nums[i]) - # 说明当前一步,再跳一步就到达了末尾 - if max_distance >= length - 1: - count += 1 - break - # 走到当前覆盖的最大区域时,更新下一步可达的最大区域 - if i == cur_distance: - cur_distance = max_distance - count += 1 - - return count -``` - - \ No newline at end of file diff --git a/LeetCode/0055-jump-game.md b/LeetCode/0055-jump-game.md deleted file mode 100644 index d2f5408..0000000 --- a/LeetCode/0055-jump-game.md +++ /dev/null @@ -1,112 +0,0 @@ -# [55. 跳跃游戏](https://leetcode-cn.com/problems/jump-game) - -## 题目描述 - - - -给定一个非负整数数组`nums`,你最初位于数组的`第一个下标`。 - -

数组中的每个元素代表你在该位置可以跳跃的最大长度。

- -

判断你是否能够到达最后一个下标。

- -

- -

示例 1:

- -```bash -输入:nums = [2,3,1,1,4] -输出:true -解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 -``` - -

示例 2:

- -```bash -输入:nums = [3,2,1,0,4] -输出:false -解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 -``` - -## 分析 - - - -从头到尾遍历数组,如果到不了数组的某个位置,那么也就到不了最后一个下标,是不是这样? - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public boolean canJump(int[] nums) { - // 当前能到达的最大位置 - int curMax = 0; - for (int i = 0, len = nums.length; i < len; i++) { - if (curMax < i) { - return false; - } - curMax = Math.max(curMax, i + nums[i]); - } - return true; - } -} -``` - -这里顺便也保留自己第一次做的实现(虽然不够简洁,但也通过了): -```java -class Solution { - public boolean canJump(int[] nums) { - // 记录对应位置能否到达最后一个下标,true表示不能到达最后一个下标 - // 初始值都为false,表示不知道能否到达最后一个下标 - boolean[] history = new boolean[nums.length]; - return tryJump(nums, 0, history); - } - - public boolean tryJump(int[] nums, int startIdx, boolean[] history) { - if (history[startIdx]) { - // 如果前面已经确定从startIdx位置不能到达最后一个下标,直接返回 - return false; - } - int maxStep = nums[startIdx]; - if ((startIdx + maxStep) >= (nums.length - 1)) { - // 从当前位置可以直接到达最后一个下标 - return true; - } - for (int i = maxStep; i >= 1; i--) { - // 尝试往前跳跃长度i - boolean canJump = tryJump(nums, startIdx + i, history); - if (canJump) { - return true; - } else { - // 记录从(startIdx + i)位置不能到达最后一个下标 - history[startIdx + i] = true; - } - } - return false; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def canJump(self, nums: List[int]) -> bool: - # 当前能到达的最大位置 - cur_max = 0 - for i in range(len(nums)): - if cur_max < i: - return False - cur_max = max(cur_max, i + nums[i]) - return True -``` - - \ No newline at end of file diff --git a/LeetCode/0070-climbing-stairs.md b/LeetCode/0070-climbing-stairs.md deleted file mode 100644 index 960749d..0000000 --- a/LeetCode/0070-climbing-stairs.md +++ /dev/null @@ -1,75 +0,0 @@ -# [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs) - -## 题目描述 - - - -

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

- -

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

- -

注意:给定 n 是一个正整数。

- -

示例 1:

- -```bash -输入: 2 -输出: 2 -解释: 有两种方法可以爬到楼顶。 -1. 1 阶 + 1 阶 -2. 2 阶 -``` - -

示例 2:

- -```bash -输入: 3 -输出: 3 -解释: 有三种方法可以爬到楼顶。 -1. 1 阶 + 1 阶 + 1 阶 -2. 1 阶 + 2 阶 -3. 2 阶 + 1 阶 -``` - -## 分析 - - - -想上第 `n` 级台阶,可从第 `n-1` 级台阶爬一级上去,也可从第 `n-2` 级台阶爬两级上去,即:`f(n) = f(n-1) + f(n-2)`。 - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public int climbStairs(int n) { - int a = 0, b = 1; - for (int i = 0; i < n; i++) { - int c = a + b; - a = b; - b = c; - } - return b; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def climbStairs(self, n: int) -> int: - a, b = 0, 1 - for _ in range(n): - a, b = b, a + b - return b -``` - - \ No newline at end of file diff --git a/LeetCode/0094-binary-tree-inorder-traversal.md b/LeetCode/0094-binary-tree-inorder-traversal.md deleted file mode 100644 index 3943114..0000000 --- a/LeetCode/0094-binary-tree-inorder-traversal.md +++ /dev/null @@ -1,271 +0,0 @@ -# [94. 二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal) - -## 题目描述 - - - -给定一个二叉树的根节点`root`,返回它的`中序`遍历。 - -

- -

示例 1:

- - -```bash -输入:root = [1,null,2,3] -输出:[1,3,2] -``` - -

示例 2:

- -```bash -输入:root = [] -输出:[] -``` - -

示例 3:

- -```bash -输入:root = [1] -输出:[1] -``` - -

示例 4:

- - -```bash -输入:root = [1,2] -输出:[2,1] -``` - -

示例 5:

- - -```bash -输入:root = [1,null,2] -输出:[1,2] -``` - -

- -

提示:

- -- 树中节点数目在范围`[0, 100]`内 -- -100 <= `Node.val` <= 100 - -

- -

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

- -## 分析 - - - -**递归思路:** - -先递归访问左孩子,然后记录该节点的值,再递归访问右孩子。 - -
- -**迭代思路:** - -中序遍列,始终牢记【左,根,右】的顺序,优先左孩子,然后根节点,最后右孩子,遍历左右孩子时同样遵循【左,根,右】的顺序。 - -1. 只要有左孩子,就需要一直往左遍历,所以需要用`栈`来记录遇到的节点,直到左孩子为空为止; -2. 弹出栈顶元素,并输出该节点的值; -3. 当第2步中的栈顶节点的右孩子为空,则重复步骤2和3,否则重复步骤1,2,3。 - -## 实现 - - - -### **Java** - - - -**递归版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List inorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - visit(res, root); - return res; - } - - public void visit(List res, TreeNode root) { - if (root != null) { - visit(res, root.left); - res.add(root.val); - visit(res, root.right); - } - } -} -``` - -**迭代版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List inorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - if (root == null) { - return res; - } - Queue queue = new LinkedList<>(); - queue.offer(root); - - LinkedList stack = new LinkedList<>(); - - while (!queue.isEmpty() || !stack.isEmpty()) { - - if (!queue.isEmpty()) { - TreeNode cur = queue.poll(); - stack.push(cur); - - while (cur.left != null) { - cur = cur.left; - stack.push(cur); - } - } - - if (!stack.isEmpty()) { - TreeNode cur = stack.pop(); - res.add(cur.val); - - if (cur.right != null) { - queue.offer(cur.right); - } - } - } - - return res; - } -} -``` - -迭代过程中,你可以发现上面代码的那个`queue`中其实始终最多只会有一个元素,所以可以再简单优化一下: -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List inorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - if (root == null) { - return res; - } - LinkedList stack = new LinkedList<>(); - TreeNode cur = root; - - while (cur != null || !stack.isEmpty()) { - if (cur != null) { - stack.push(cur); - cur = cur.left; - } else { - cur = stack.pop(); - res.add(cur.val); - cur = cur.right; - } - } - return res; - } -} -``` - - -### **Python3** - - - - -**递归版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def inorderTraversal(self, root: TreeNode) -> List[int]: - res = [] - - def visit(root: TreeNode): - if root: - visit(root.left) - res.append(root.val) - visit(root.right) - - visit(root) - return res -``` - -**迭代版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def inorderTraversal(self, root: TreeNode) -> List[int]: - res = [] - - cur = root - stack = [] - - while cur or stack: - if cur: - stack.append(cur) - cur = cur.left - else: - cur = stack.pop() - res.append(cur.val) - cur = cur.right - - return res -``` - - \ No newline at end of file diff --git a/LeetCode/0101-symmetric-tree.md b/LeetCode/0101-symmetric-tree.md deleted file mode 100644 index 3878ba4..0000000 --- a/LeetCode/0101-symmetric-tree.md +++ /dev/null @@ -1,233 +0,0 @@ -# [101. 对称二叉树](https://leetcode-cn.com/problems/symmetric-tree) - -## 题目描述 - - - -

给定一个二叉树,检查它是否是镜像对称的。

- -

 

- -例如,二叉树 `[1,2,2,3,4,4,3]`是对称的。 - -```bash - 1 - / \ - 2 2 - / \ / \ -3 4 4 3 -``` - -

 

- -但是下面这个`[1,2,2,null,3,null,3]`则不是镜像对称的: - -```bash - 1 - / \ - 2 2 - \ \ - 3 3 -``` - -

 

- -

进阶:

- -

你可以运用递归和迭代两种方法解决这个问题吗?

- -## 分析 - - - -**递归思路:** - -当左子树与右子树对称时,这棵树镜像对称。 -那么怎么知道左子树与右子树是否镜像对称呢? -左树的左孩子与右树的右孩子对称,左树的右孩子与右树的左孩子对称,那么这个左树和右树就对称。 - -
- -**迭代思路:** - -层序遍历,然后检查每一层是不是回文数组。 - - - -## 实现 - - - -### **Java** - - - -**递归版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public boolean isSymmetric(TreeNode root) { - if (root == null) { - return true; - } - return compare(root.left, root.right); - } - - public boolean compare(TreeNode left, TreeNode right) { - if (left == null && right == null) { - return true; - } - - if (left != null && right != null - && left.val == right.val - && compare(left.left, right.right) - && compare(left.right, right.left)) { - return true; - } - - return false; - } -} -``` - -**迭代版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public boolean isSymmetric(TreeNode root) { - List nodes = Arrays.asList(root); - while (!nodes.isEmpty()) { - int size = nodes.size(); - List values = new ArrayList<>(size); - List nextNodes = new ArrayList<>(size * 2); - - for (TreeNode node : nodes) { - if (node == null) { - values.add(null); - } else { - values.add(node.val); - - nextNodes.add(node.left); - nextNodes.add(node.right); - } - } - - if (!isSymmetric(values)) { - return false; - } - - nodes = nextNodes; - } - return true; - } - - public boolean isSymmetric(List values) { - for (int i = 0, j = values.size() - 1; i < j; i++, j--) { - Integer left = values.get(i); - Integer right = values.get(j); - - if (left == null && right != null) { - return false; - } else if (left != null && right == null) { - return false; - } else if (left != null && right != null && !left.equals(right)) { - return false; - } - } - return true; - } -} -``` - - -### **Python3** - - - - -**递归版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def isSymmetric(self, root: TreeNode) -> bool: - - def compare(left: TreeNode, right: TreeNode) -> bool: - if not left and not right: - return True - if not left or not right or left.val != right.val: - return False - return compare(left.left, right.right) and compare(left.right, right.left) - - if not root: - return True - return compare(root.left, root.right) -``` - -**迭代版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def isSymmetric(self, root: TreeNode) -> bool: - nodes = [root] - - while nodes: - values = [] - next_nodes = [] - - for node in nodes: - if node: - values.append(node.val) - - next_nodes.append(node.left) - next_nodes.append(node.right) - else: - values.append(None) - - if values != values[::-1]: - return False - - nodes = next_nodes - - return True -``` - - - \ No newline at end of file diff --git a/LeetCode/0102-binary-tree-level-order-traversal.md b/LeetCode/0102-binary-tree-level-order-traversal.md deleted file mode 100644 index aab7475..0000000 --- a/LeetCode/0102-binary-tree-level-order-traversal.md +++ /dev/null @@ -1,125 +0,0 @@ -# [102. 二叉树的层序遍历](https://leetcode-cn.com/problems/binary-tree-level-order-traversal) - -## 题目描述 - - - -给你一个二叉树,请你返回其按`层序遍历`得到的节点值。 (即逐层地,从左到右访问所有节点)。 - -示例:
-二叉树:`[3,9,20,null,null,15,7]`, - -```bash - 3 - / \ - 9 20 - / \ - 15 7 -``` - -

返回其层序遍历结果:

- -```bash -[ - [3], - [9,20], - [15,7] -] -``` - -## 分析 - - - -这题其实不难,把根节点放到一个`queue`中,遍历该`queue`时把当前节点的左右孩子放到另外一个`queue`中,再遍历新生成的`queue`。 - -在上面的思路下迭代与递归差别不大。 - -## 实现 - - - -### **Java** - - - -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List> levelOrder(TreeNode root) { - List> res = new ArrayList<>(); - if (root == null) { - return res; - } - List nodes = new ArrayList<>(); - nodes.add(root); - while (!nodes.isEmpty()) { - List values = new ArrayList<>(); - List nextNodes = new ArrayList<>(); - for (TreeNode node : nodes) { - values.add(node.val); - if (node.left != null) { - nextNodes.add(node.left); - } - if (node.right != null) { - nextNodes.add(node.right); - } - } - res.add(values); - nodes = nextNodes; - } - return res; - } -} -``` - -### **Python3** - - - -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def levelOrder(self, root: TreeNode) -> List[List[int]]: - res = [] - if not root: - return res - - nodes = [root] - while len(nodes) > 0: - values = [] - next_nodes = [] - - for node in nodes: - values.append(node.val) - if node.left: - next_nodes.append(node.left) - if node.right: - next_nodes.append(node.right) - - res.append(values) - nodes = next_nodes - - return res -``` - - diff --git a/LeetCode/0121-best-time-to-buy-and-sell-stock.md b/LeetCode/0121-best-time-to-buy-and-sell-stock.md deleted file mode 100644 index 97bfe93..0000000 --- a/LeetCode/0121-best-time-to-buy-and-sell-stock.md +++ /dev/null @@ -1,91 +0,0 @@ -# [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock) - -## 题目描述 - - - -给定一个数组`prices`,它的第`i`个元素`prices[i]`表示一支给定股票第`i`天的价格。 - -你只能选择`某一天`买入这只股票,并选择在`未来的某一个不同的日子`卖出该股票。设计一个算法来计算你所能获取的最大利润。 - -返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回`0`。 - -

- -

示例 1:

- -```bash -输入:[7,1,5,3,6,4] -输出:5 -解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 - 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 -``` - -

示例 2:

- -```bash -输入:prices = [7,6,4,3,1] -输出:0 -解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 -``` - -

- -

注意:

- -- 1 <= `prices.length` <= 105 -- 0 <= `prices[i]` <= 104 - -## 分析 - - - -遍历数组,遍历过程中不断更新最大的利润以及最小的价格。 - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public int maxProfit(int[] prices) { - if (prices == null || prices.length <= 1) { - return 0; - } - int minPrice = prices[0]; - int maxProfit = 0; - - for (int i = 1, len = prices.length; i < len; i++) { - maxProfit = Math.max(maxProfit, prices[i] - minPrice); - minPrice = Math.min(minPrice, prices[i]); - } - - return maxProfit; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def maxProfit(self, prices: List[int]) -> int: - if not prices or len(prices) <= 1: - return 0 - - min_price, max_profit = prices[0], 0 - - for i in range(1, len(prices)): - max_profit = max(max_profit, prices[i] - min_price) - min_price = min(min_price, prices[i]) - - return max_profit -``` - - \ No newline at end of file diff --git a/LeetCode/0122-best-time-to-buy-and-sell-stock-ii.md b/LeetCode/0122-best-time-to-buy-and-sell-stock-ii.md deleted file mode 100644 index 990c985..0000000 --- a/LeetCode/0122-best-time-to-buy-and-sell-stock-ii.md +++ /dev/null @@ -1,143 +0,0 @@ -# [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii) - -## 题目描述 - - - -给定一个数组`prices`,其中`prices[i]`是一支给定股票第`i`天的价格。 - -

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

- -`注意`:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 - -

- -

示例 1:

- -```bash -输入: prices = [7,1,5,3,6,4] -输出: 7 -解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 - 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 -``` - -

示例 2:

- -```bash -输入: prices = [1,2,3,4,5] -输出: 4 -解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 - 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 -``` - -

示例 3:

- -```bash -输入: prices = [7,6,4,3,1] -输出: 0 -解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 -``` - -## 分析 - - - -始终保证低价时买入,高价时卖出即可。 - -## 实现 - - - -### **Java** - - - -始终保证低价时买入,高价时卖出,我的实现代码如下: -```java -class Solution { - public int maxProfit(int[] prices) { - int res = 0; - int buyPrice = -1; - int sellPrice = -1; - - for (int i = 0, len = prices.length; i < len; i++) { - int curPrice = prices[i]; - if (buyPrice == -1) { - buyPrice = curPrice; - } else if (sellPrice == -1 && curPrice <= buyPrice) { - buyPrice = curPrice; - } else if (curPrice > sellPrice) { - sellPrice = curPrice; - } else if (curPrice < sellPrice) { - res += (sellPrice - buyPrice); - - buyPrice = curPrice; - sellPrice = -1; - } - } - if (sellPrice > buyPrice) { - res += (sellPrice - buyPrice); - } - return res; - } -} -``` - -其实本质上就是**只要今天比昨天大,就卖出**,更简洁的实现如下: -```java -class Solution { - public int maxProfit(int[] prices) { - int res = 0; - for (int i = 1, len = prices.length; i < len; i++) { - if (prices[i] > prices[i - 1]) { - res += (prices[i] - prices[i - 1]); - } - } - return res; - } -} -``` - -### **Python3** - - - -始终保证低价时买入,高价时卖出,我的实现代码如下: -```python -class Solution: - def maxProfit(self, prices: List[int]) -> int: - res, buy_price, sell_price = 0, -1, -1 - - for price in prices: - if buy_price == -1: - buy_price = price - elif sell_price == -1 and price <= buy_price: - buy_price = price - elif price > sell_price: - sell_price = price - elif price < sell_price: - res += (sell_price - buy_price) - - buy_price = price - sell_price = -1 - - if sell_price > buy_price: - res += (sell_price - buy_price) - - return res -``` - -其实本质上就是**只要今天比昨天大,就卖出**,更简洁的实现如下: -```python -class Solution: - def maxProfit(self, prices: List[int]) -> int: - res = 0 - - for i in range(1, len(prices)): - if prices[i] > prices[i - 1]: - res += (prices[i] - prices[i - 1]) - - return res -``` - - \ No newline at end of file diff --git a/LeetCode/0136-single-number.md b/LeetCode/0136-single-number.md deleted file mode 100644 index 3eb95fa..0000000 --- a/LeetCode/0136-single-number.md +++ /dev/null @@ -1,80 +0,0 @@ -# [136. 只出现一次的数字](https://leetcode-cn.com/problems/single-number) - -## 题目描述 - - - -

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

- -

说明:

- -

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

- -

示例 1:

- -```bash -输入: [2,2,1] -输出: 1 -``` - -

示例 2:

- -```bash -输入: [4,1,2,1,2] -输出: 4 -``` - -## 分析 - - - -用`异或运算`求解。 - -```bash -num1 ^ num2 ^ num3 = num1 ^ num3 ^ num2 - -num ^ num = 0 - -num ^ 0 = num -``` -即: -- `异或运算`满足交换律 -- 两个相同的数`异或`之后等于`0` -- 一个数与0进行`异或`等于其本身 - -所以,对该数组所有元素进行异或运算,结果就是那个只出现一次的数字。 - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public int singleNumber(int[] nums) { - int res = 0; - for (int num : nums) { - res ^= num; - } - return res; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def singleNumber(self, nums: List[int]) -> int: - res = 0 - for num in nums: - res ^= num - return res -``` - - \ No newline at end of file diff --git a/LeetCode/0144-binary-tree-preorder-traversal.md b/LeetCode/0144-binary-tree-preorder-traversal.md deleted file mode 100644 index 424147c..0000000 --- a/LeetCode/0144-binary-tree-preorder-traversal.md +++ /dev/null @@ -1,226 +0,0 @@ -# [144. 二叉树的前序遍历](https://leetcode-cn.com/problems/binary-tree-preorder-traversal) - -## 题目描述 - - - -给你二叉树的根节点`root`,返回它节点值的`前序`遍历。 - -

- -

示例 1:

- - -```bash -输入:root = [1,null,2,3] -输出:[1,2,3] -``` - -

示例 2:

- -```bash -输入:root = [] -输出:[] -``` - -

示例 3:

- -```bash -输入:root = [1] -输出:[1] -``` - -

示例 4:

- - -```bash -输入:root = [1,2] -输出:[1,2] -``` - -

示例 5:

- - -```bash -输入:root = [1,null,2] -输出:[1,2] -``` - -

- -

提示:

- -- 树中节点数目在范围`[0, 100]`内 -- -100 <= `Node.val` <= 100 - - -

进阶:递归算法很简单,你可以通过迭代算法完成吗?

- -## 分析 - - - -**递归思路:** - -先立刻记录根节点的值,然后递归访问左孩子,递归访问右孩子。 - -
- -**迭代思路:** - -可以借助栈来实现,初始化一个只有根节点的栈。只要栈不为空,反复执行以下操作: -【弹出栈顶元素,立刻记录该栈顶元素的值;如果该栈顶元素的右孩子不为空,则入栈;如果该栈顶元素的左孩子不为空,则入栈】 - - -## 实现 - - - -### **Java** - - - -**递归版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List preorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - visit(res, root); - return res; - } - - public void visit(List res, TreeNode root) { - if (root == null) { - return; - } - - res.add(root.val); - - if (root.left != null) { - visit(res, root.left); - } - - if (root.right != null) { - visit(res, root.right); - } - } -} -``` - -**迭代版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List preorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - if (root == null) { - return res; - } - - LinkedList stack = new LinkedList<>(); - stack.push(root); - - while (!stack.isEmpty()) { - TreeNode cur = stack.pop(); - res.add(cur.val); - - if (cur.right != null) { - stack.push(cur.right); - } - if (cur.left != null) { - stack.push(cur.left); - } - } - - return res; - } -} -``` - - -### **Python3** - - - - -**递归版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def preorderTraversal(self, root: TreeNode) -> List[int]: - res = [] - if not root: - return res - - def visit(root: TreeNode): - res.append(root.val) - if root.left: - visit(root.left) - if root.right: - visit(root.right) - - visit(root) - return res -``` - -**迭代版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def preorderTraversal(self, root: TreeNode) -> List[int]: - res = [] - if not root: - return res - - stack = [root] - while stack: - cur = stack.pop() - res.append(cur.val) - if cur.right: - stack.append(cur.right) - if cur.left: - stack.append(cur.left) - - return res -``` - - \ No newline at end of file diff --git a/LeetCode/0145-binary-tree-postorder-traversal.md b/LeetCode/0145-binary-tree-postorder-traversal.md deleted file mode 100644 index b410ab7..0000000 --- a/LeetCode/0145-binary-tree-postorder-traversal.md +++ /dev/null @@ -1,198 +0,0 @@ -# [145. 二叉树的后序遍历](https://leetcode-cn.com/problems/binary-tree-postorder-traversal) - -## 题目描述 - - - -给定一个二叉树,返回它的`后序`遍历。 - -

示例:

- -```bash -输入: [1,null,2,3] - 1 - \ - 2 - / - 3 - -输出: [3,2,1] -``` - -

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

- -## 分析 - - - -**递归思路:** - -先递归访问左孩子,然后递归访问右孩子,再记录根节点的值。 - -
- -**迭代思路:** - -仔细观察后序遍历的元素进出栈规律编写代码,细节见下面代码中的注释。 - -## 实现 - - - -### **Java** - - - -**递归版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List postorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - visit(res, root); - return res; - } - - public void visit(List res, TreeNode root) { - if (root != null) { - visit(res, root.left); - visit(res, root.right); - res.add(root.val); - } - } -} -``` - -**迭代版本:** -```java -/** - * Definition for a binary tree node. - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode() {} - * TreeNode(int val) { this.val = val; } - * TreeNode(int val, TreeNode left, TreeNode right) { - * this.val = val; - * this.left = left; - * this.right = right; - * } - * } - */ -class Solution { - public List postorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - - TreeNode cur = root; - // 记录上次输出的节点 - TreeNode visited = null; - - LinkedList stack = new LinkedList<>(); - - while (cur != null || !stack.isEmpty()) { - if (cur != null) { - // 左边有孩子,则一直往左遍历 - stack.push(cur); - cur = cur.left; - } else { - // 查看栈顶节点 - TreeNode node = stack.peek(); - if (node.right != null && node.right != visited) { - // 如果栈顶节点的右孩子不为null且没有输出过,则继续往右走一个节点 - cur = node.right; - } else { - // 如果栈顶节点的右孩子为null或者已经输出过,则直接弹出栈顶元素并且输出,同时对该节点做标记 - node = stack.pop(); - res.add(node.val); - visited = node; - } - } - } - - return res; - } -} -``` - - -### **Python3** - - - - -**递归版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def postorderTraversal(self, root: TreeNode) -> List[int]: - res = [] - - def visit(root: TreeNode): - if root: - visit(root.left) - visit(root.right) - res.append(root.val) - - visit(root) - return res -``` - -**迭代版本:** -```python -# Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right -class Solution: - def postorderTraversal(self, root: TreeNode) -> List[int]: - res = [] - - cur = root - # 记录上次输出的节点 - visited = None - stack = [] - - while cur or stack: - if cur: - # 左边有孩子,则一直往左遍历 - stack.append(cur) - cur = cur.left - else: - # 查看栈顶节点 - node = stack[-1] - if node.right and node.right != visited: - # 如果栈顶节点的右孩子不为None且没有输出过,则继续往右走一个节点 - cur = node.right - else: - # 如果栈顶节点的右孩子为None或者已经输出过,则直接弹出栈顶元素并且输出,同时对该节点做标记 - node = stack.pop() - res.append(node.val) - visited = node - - return res -``` - - - diff --git a/LeetCode/0206-reverse-linked-list.md b/LeetCode/0206-reverse-linked-list.md deleted file mode 100644 index 4ef6ae8..0000000 --- a/LeetCode/0206-reverse-linked-list.md +++ /dev/null @@ -1,182 +0,0 @@ -# [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list) - -## 题目描述 - - - -

反转一个单链表。

- -

示例:

- -```bash -输入: 1->2->3->4->5->NULL -输出: 5->4->3->2->1->NULL -``` - -

进阶:
-你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

- -## 分析 - - - -**迭代思路:** - -把第一个`Node`置为`null`,然后在迭代过程中总是把后一个`Node`放到链表头部。 - -
- -**递归思路:** - -第一种方式:类似于上面的迭代思路,把第一个`Node`置为`null`,相邻两个`Node`反转后,继续取后一个`Node`参与反转。 - -第二种方式:先反转`head.next`,然后再把`head`放到链表尾部。反转`head.next`正好是一个递归的过程。 - - -## 实现 - - - -### **Java** - - - -**迭代版本:** -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode() {} - * ListNode(int val) { this.val = val; } - * ListNode(int val, ListNode next) { this.val = val; this.next = next; } - * } - */ -class Solution { - public ListNode reverseList(ListNode head) { - ListNode pre = null; - ListNode cur = head; - while (cur != null) { - ListNode next = cur.next; - cur.next = pre; - pre = cur; - cur = next; - } - return pre; - } -} -``` - -**递归版本(一):** -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode() {} - * ListNode(int val) { this.val = val; } - * ListNode(int val, ListNode next) { this.val = val; this.next = next; } - * } - */ -class Solution { - public ListNode reverseList(ListNode head) { - return reverse(null, head); - } - - public ListNode reverse(ListNode pre, ListNode cur) { - if (cur == null) { - return pre; - } - ListNode next = cur.next; - cur.next = pre; - return reverse(cur, next); - } -} -``` - - -**递归版本(二):** -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode() {} - * ListNode(int val) { this.val = val; } - * ListNode(int val, ListNode next) { this.val = val; this.next = next; } - * } - */ -class Solution { - public ListNode reverseList(ListNode head) { - if (head == null || head.next == null) { - return head; - } - - ListNode res = reverseList(head.next); - head.next.next = head; - head.next = null; - - return res; - } -} -``` - -### **Python3** - - - -**迭代版本:** -```python -# Definition for singly-linked list. -# class ListNode: -# def __init__(self, val=0, next=None): -# self.val = val -# self.next = next -class Solution: - def reverseList(self, head: ListNode) -> ListNode: - pre, cur = None, head - while cur: - cur.next, pre, cur = pre, cur, cur.next - return pre -``` - -**递归版本(一):** -```python -# Definition for singly-linked list. -# class ListNode: -# def __init__(self, val=0, next=None): -# self.val = val -# self.next = next -class Solution: - def reverseList(self, head: ListNode) -> ListNode: - - def reverse(pre: ListNode, cur: ListNode) -> ListNode: - if not cur: - return pre - next_node = cur.next - cur.next = pre - return reverse(cur, next_node) - - return reverse(None, head) -``` - -**递归版本(二):** -```python -# Definition for singly-linked list. -# class ListNode: -# def __init__(self, val=0, next=None): -# self.val = val -# self.next = next -class Solution: - def reverseList(self, head: ListNode) -> ListNode: - if not head or not head.next: - return head - res = self.reverseList(head.next) - head.next.next, head.next = head, None - return res -``` - - \ No newline at end of file diff --git a/LeetCode/0746-min-cost-climbing-stairs.md b/LeetCode/0746-min-cost-climbing-stairs.md deleted file mode 100644 index 91803d8..0000000 --- a/LeetCode/0746-min-cost-climbing-stairs.md +++ /dev/null @@ -1,98 +0,0 @@ -# [746. 使用最小花费爬楼梯](https://leetcode-cn.com/problems/min-cost-climbing-stairs) - -## 题目描述 - - - -数组的每个下标作为一个阶梯,第`i`个阶梯对应着一个非负数的体力花费值`cost[i]`(下标从`0`开始)。

- -

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

- -请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为`0`或`1`的元素作为初始阶梯。 - -

- -

示例 1:

- -```bash -输入:cost = [10, 15, 20] -输出:15 -解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。 -``` - -

示例 2:

- -```bash -输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] -输出:6 -解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。 -``` - -

- -

提示:

- -- `cost`的长度范围是`[2, 1000]`。 -- `cost[i]`将会是一个整型数据,范围为`[0, 999]`。 - -## 分析 - - - -想上第 n 级台阶,可从第 n-1 级台阶爬一级上去,也可从第 n-2 级台阶爬两级上去。 - -所以到达第n级阶梯所需最小体力f(n)的递推关系为: - -f(n) = min ( f(n-1) + cost[n-1] , f(n-2) + cost[n-2] ) - -## 实现 - - - -### **Java** - - - -```java -class Solution { - public int minCostClimbingStairs(int[] cost) { - // 到达第n-2个楼梯所需的最小体力花费 - int a = 0; - // 到达第n-1个楼梯所需的最小体力花费 - int b = 0; - - // 题目要求到达楼梯顶,所以相当于是到达 (数组下标的最后位置+1) - int last = cost.length; - - for (int n = 2; n <= last; n++) { - // 到达第n个楼梯所需的最小体力花费 - int cur = Math.min(a + cost[n - 2], b + cost[n - 1]); - a = b; - b = cur; - } - - return b; - } -} -``` - -### **Python3** - - - -```python -class Solution: - def minCostClimbingStairs(self, cost: List[int]) -> int: - # 到达第n-2, n-1级别所需的最小体力 - a, b = 0, 0 - - # 题目要求到达楼梯顶,所以相当于是到达 (数组下标的最后位置+1) - last = len(cost) + 1 - - for n in range(2, last): - a, b = b, min(a + cost[n - 2], b + cost[n - 1]) - - return b -``` - - \ No newline at end of file diff --git a/LeetCode/README.md b/LeetCode/README.md deleted file mode 100644 index ff152f4..0000000 --- a/LeetCode/README.md +++ /dev/null @@ -1,4 +0,0 @@ - -每日打卡,这里主要使用`Java`、`Python`两种语言实现。 - -参考:[https://github.com/doocs/leetcode](https://github.com/doocs/leetcode) \ No newline at end of file diff --git a/Life/drunk-post-of-a-programmer.md b/Life/drunk-post-of-a-programmer.md deleted file mode 100644 index 0567f00..0000000 --- a/Life/drunk-post-of-a-programmer.md +++ /dev/null @@ -1,78 +0,0 @@ -# 程序员的酒后真言 - -> 作者: 阮一峰 -> 日期: 2021年6月28日 -> 原文链接: [http://www.ruanyifeng.com/blog/2021/06/drunk-post-of-a-programmer.html](http://www.ruanyifeng.com/blog/2021/06/drunk-post-of-a-programmer.html) - -美国最大的论坛`Reddit`,最近有一个 [热帖](https://old.reddit.com/r/ExperiencedDevs/comments/nmodyl/drunk_post_things_ive_learned_as_a_sr_engineer/)。 - -一个程序员说自己喝醉了,软件工程师已经当了10年,心里有好多话想说,"我可能会后悔今天说了这些话。" - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211112094801227-1410926988.png) - -他洋洋洒洒写了一大堆,获得9700多个赞。内容很有意思,值得一读,下面是节选。 - -![](https://img2020.cnblogs.com/blog/1546632/202111/1546632-20211112094835459-55159727.png) - -(1)职业发展的最好方法是换公司。 - -(2)技术栈不重要。技术领域有大约 10-20 条核心原则,重要的是这些原则,技术栈只是落实它们的方法。你如果不熟悉某个技术栈,不需要过度担心。 - -(3)工作和人际关系是两回事。有一些公司,我交到了好朋友,但是工作得并不开心;另一些公司,我没有与任何同事建立友谊,但是工作得很开心。 - -(4)我总是对经理实话实说。怕什么?他开除我?我会在两周内找到一份新工作。 - -(5)如果一家公司的工程师超过 100 人,它的期权可能在未来十年内变得很有价值。对于工程师人数很少的公司,期权一般都是毫无价值。 - -(6)好的代码是初级工程师可以理解的代码。伟大的代码可以被第一年的 CS 专业的新生理解。 - -(7)作为一名工程师,最被低估的技能是记录。说真的,如果有人可以教我怎么写文档,我会付钱,也许是 1000 美元。 - -(8)网上的口水战,几乎都无关紧要,别去参与。 - -(9)如果我发现自己是公司里面最厉害的工程师,那就该离开了。 - -(10)我们应该雇佣更多的实习生,他们很棒。那些精力充沛的小家伙用他们的想法乱搞。如果他们公开质疑或批评某事,那就更好了。我喜欢实习生。 - -(11)技术栈很重要。如果你使用 Python 或 C++ 语言,就会忍不住想做一些非常不同的事情。因为某些工具确实擅长某些工作。 - -(12)如果你不确定自己想做什么东西,请使用 Java。这是一种糟糕的编程语言,但几乎无所不能。 - -(13)对于初学者来说,最赚钱的编程语言是 SQL,干翻所有其他语言。你只了解 SQL 而不会做其他事情,照样赚钱。人力资源专家的年薪?也许5万美元。懂 SQL 的人力资源专家?9万美元。 - -(14)测试很重要,但 TDD (测试驱动的开发)几乎变成了一个邪教。 - -(15) 政府单位很轻松,但并不像人们说的那样好。对于职业生涯早期到中期的工程师,12 万美元的年薪 + 各种福利 + 养老金听起来不错,但是你将被禁锢在深奥的专用工具里面,离开政府单位以后,这些知识就没用了。我非常尊重政府工作人员,但说真的,这些地方的工程师,年龄中位数在 50 岁以上是有原因的。 - -(16)再倒一杯酒。 - -(17)大多数头衔都无关紧要,随便什么公司都可以有首席工程师。 - -(18)手腕和背部的健康问题可不是开玩笑的,好的设备值得花钱。 - -(19)当一个软件工程师,最好的事情是什么?你可以结识很多想法相同的人,大家互相交流,不一定有相同的兴趣,但是对方会用跟你相同的方式思考问题,这很酷。 - -(20)有些技术太流行,我不得不用它。我心里就会很讨厌这种技术,但会把它推荐给客户,比如我恨 Jenkins,但把它推荐给新客户,我不觉得做错了。 - -(21)成为一名优秀的工程师意味着了解最佳实践,成为高级工程师意味着知道何时打破最佳实践。 - -(22)发生事故时,如果周围的人试图将责任归咎于外部错误或底层服务中断,那么是时候离开这家公司,继续前进了。 - -(23)我遇到的最好的领导,同意我的一部分观点,同时耐心跟我解释,为什么不同意我的另一部分观点。我正在努力成为像他们一样的人。 - -(24)算法和数据结构确实重要,但不应该无限夸大,尤其是面试的时候。我没见过药剂师面试时,还要测试有机化学的细节。这个行业的面试过程有时候很糟糕。 - -(25)做自己喜欢的事情并不重要,不要让我做讨厌的事情更重要。 - -(26)越接近产品,就越接近推动收入增长。无论工作的技术性如何,只要它接近产品,我都感到越有价值。 - -(27)即使我平时用 Windows 工作,Linux 也很重要。为什么?因为服务器是 Linux 系统,你最终在 Linux 系统上工作。 - -(28)人死了以后,你想让代码成为你的遗产吗?如果是那样,就花很多时间在代码上面吧,因为那是你的遗产。但是,如果你像我一样,更看重与家人、朋友和生活中其他人相处的时光,而不是写的代码,那就别对它太在意。 - -(29)我挣的钱还不错,对此心存感激,但还是需要省钱。 - -(30)糟糕,我没酒了。 - -(完) - diff --git a/Life/how-to-prove-that-you-are-a-programmer.md b/Life/how-to-prove-that-you-are-a-programmer.md deleted file mode 100644 index 23636a9..0000000 --- a/Life/how-to-prove-that-you-are-a-programmer.md +++ /dev/null @@ -1,109 +0,0 @@ -# 请用一句话证明你是程序员 - -今天是2020-10-24,时光匆匆,转眼间已正式工作2年零4个月,虽未达到当年毕业前的预想,但勉强也算入行了,而且一直会在路上,未来可期。 - -两年前的酷夏,我与同届校友杨同学,背着包,怀着毕业时的激情、对未来的憧憬,拖着行李,来到了魔都。第二天是个周末,忽晴忽阴,找房时,雨倾盆而下。初出校园,很好说话,三言两句就签了租房合同。接着就是打扫收拾,很顺利地安顿了下来。两天很快过去,周一,去公司入职。一开始,合代码都战战兢兢,因为git用得不熟,生怕把别人的代码弄乱了,不过周老师很耐心地演示了一次,便熟记于心。之后,在帝哥等同事的指导与帮助下,渐渐成长。工作中,我也慢慢体会到了归纳总结的意义。归纳总结的过程虽然有些痛苦,但对个人的提升还是蛮大的。当然,前提是你真心想总结一些东西,而不是被强逼的。 - -至今,褪去一丝稚嫩、几分浮躁,添了少许稳重、踏实,但依旧青涩。未来很长,不问前程,但求无悔。始终如一的是,无忌无惧,莫失莫忘。 - -昨天周五,公司提前庆祝1024程序员节,HR小姐姐们做了很用心的准备。其中有环节提问到`请用一句话证明你是程序员`,下面结合网上其他人的回答做了简单的汇总。欢迎补充。 - -> Hello,World - -> 我不是修电脑的 - -> php是世界上最好的语言,不服来辩!!! - -> 我毕业3年,5年工作经验。 - -> 有个悬崖边立了一个`WARNING`的牌子,只有程序员掉了下去。后来不知道哪个人在牌子上加了`FBI`三个字,所有看到牌子的人都掉了下去。 - -> 目前服务器性能跟不上;现有的技术实现不了;去找老板排期吧,这个很费时间的;这个需求太low了... - -> 一切皆对象,没有对象,自己new一个就好。 - -> 昨天写了封邮件骂产品经理,幸好发送前都及时注释掉了...... - -> 1 + 2 == 3 - -> 问:如何生成一个随机的字符串? 答:让新手退出vim。 - -> 问:你怎么从全世界找到喜欢你的人? 答:select * from world where someone like '%you%'; - -> 用IE6的吃方便面都没有调料包,你知道不知道...... - -> 1kg的猪肉怎么少了24g? - -> 书籍推荐:《java基础篇》《java高级篇》《java编程思想》《大型网站架构》《教你怎么不生气》《沉默的愤怒》《颈椎病康复指南》《活着》 - -> 0, 1, 2, 3 ... - -> 用户不会像你这么操作的。 - -> 能复现吗? - -> 在我机器上可以跑啊! - -> 不可能有问题的,你换个浏览器试试? - -> 你清一下缓存试试?重启试一下? - -> 我很奇怪客栈这个词,难道后入住的必须先退房吗? - -> 总把KFC当成MFC - -> 要理解递归,首先要理解递归。 - -> 对象都找不到,谈什么OOP - -> 为什么life is short,而不是 int 或者 long呢? - -> 凌晨2点下班回家,路遇警察,警察问:一个int几个字节,我说4个。警察说:过去吧!我好奇问为何,答:这个点下班的不是小偷就是程序员! - -> 肯定是数据问题 - -> 世界上有两种人,一种是有女朋友的,一种是懂二进制的... - -> 又在写bug了? - -> 这个做不了!!! - -> 这个需要时间!!! - -**以下来自下面的评论** - -> 那个产品煞笔! - -> i = i + 1; - -> string啥意思 字符串啊,thread呢 线程,push对应是啥 pop - -> 上上下下左左右右 - -> php是世界上最好的语言,没有之一!!! - -> 天气见凉,原来夏季格子衬衫穿着太冷了,该换件内里加绒的格子衬衫了 - -> 每次听到Taylor Swift的新歌《Invisible String》,潜意识里就翻译成了 private String ... - -> 我头发少。 - -> 问:请问,// 是什么意思啊 答:什么事? 问:我说, // 是什么意思啊 答:别总是“请问、我说”好不好,有啥说啥 - -> 996 - -> 我没有女朋友。 - -> 什么都不用说,看一眼就知道了。 - -> 这个是操作系统环境的问题,重装一下系统 - -> 烫烫烫 - -> prop Tab - -> 千万不要对一个程序员说:你的代码有bug。若是这么说了,他的第一反应是:1.你的环境有问题吧;2.傻x你会用吗。如果你委婉地说:你这个程序和预期的有点不一致,你看看是不是我的使用方法有问题。他本能地会想:操,代码是不是有bug! - -> alert("缓存清除成功!") - -未完待续... \ No newline at end of file diff --git a/Life/study-vs-work.md b/Life/study-vs-work.md deleted file mode 100644 index 0d364d8..0000000 --- a/Life/study-vs-work.md +++ /dev/null @@ -1,430 +0,0 @@ -# 工作中如何做好技术积累 - -翻阅美团技术团队的博客时,无意间看到《工作中如何做好技术积累》这篇文章,收获很多。这里分享出来,让更多的朋友可以常常观之,以此自勉。 - - -原文链接: -[https://tech.meituan.com/2018/04/16/study-vs-work.html](https://tech.meituan.com/2018/04/16/study-vs-work.html) - - -## 引言 - - -古人云:“**活到老,学到老。**”互联网算是最辛苦的行业之一,“加班”对工程师来说已是“家常便饭”,同时互联网技术又日新月异,很多工程师都疲于应付,叫苦不堪。以至于长期以来流传一个很广的误解:35岁是程序员工作的终点。 - - -如何在繁忙的工作中做好技术积累,构建个人核心竞争力,相信是很多工程师同行都在思考的问题。本文是我自己的一些总结,试图从三个方面来解答: - -- 第一部分阐述了一些学习的原则。任何时候,遵循一些经过检验的原则,都是影响效率的重要因素,正确的方法是成功的秘诀。 -- 第二部分分析了我在工作中碰到和看到的一些典型困惑。提升工作和学习效率的另一个重要因素是释惑和良好心态。 -- 第三部分剖析架构师的能力模型,让大家对目标所需能力有一个比较清晰的认知。成为优秀的架构师是大部分初中级工程师的阶段性目标。 - - - -## 如何学习 - - -在繁忙的工作中,持之以恒、不断学习和进步是一件艰巨的任务,需要坚强的毅力和坚定的决心。如果方法不得当,更是事倍功半。幸好我们的古人和现在哲人已经总结了很多优秀的学习方法论,这里汇总了一些重要原则。遵循这些方法必会对大家的工作学习大有裨益。 - - -### 贵在坚持 - - -有报道指出,过去几十年的知识量超过之前人类几千年的知识量总和。而计算机领域绝对是当代知识更新最快的领域之一,因此,工程师必须要接受这样一个现实,现在所掌握的深厚知识体系很快就会被淘汰。要想在计算机领域持续做优秀架构师,就必须不停的学习,掌握最新技术。总之,学不可以已。 - - -所谓“**冰冻三尺,非一日之寒,水滴石穿,非一日之功**”,通往架构师的道路漫长而又艰巨,轻易放弃,则所有付出瞬间付之东流。要想成为优秀的架构师,贵在坚持! - - -虽然知识更新很快,但是基础理论的变化却非常缓慢。这就是“道”和“象”关系,**纵是世间万象,道却万变不离其宗**。对于那些非常基础的理论知识,我们需要经常复习,也就是“**学而时习之**”。 - - -### 重视实践 - - -古人云:“**纸上得来终觉浅,绝知此事要躬行。**” 学习领域有所谓721模型:**个人的成长70%来自于岗位实践,20%来自向他人学习,10%来自于培训**。虽然这种理论存在争议,但对于工程师们来说,按照实践、学习和培训的方式进行重要性排序,大致是不错的。所以重视实践,在实践中成长是最重要的学习原则。 - - -人类的认知有两种:感性认知和理性认知。这两种认知互相不可替代性。实践很大程度来自于感性学习,看书更像是理性学习。以学开汽车做例子,很难想象什么人能够仅仅通过学习书本知识就会开汽车。 - -书本知识主要是传道——讲述抽象原型,而对其具体应用场景的讲述往往含糊其辞,对抽象原型之间的关系也是浅尝辄止。采用同样精确的语言去描述应用场景和关联关系将会失去重点,让人摸不着头脑。所以,仅仅通过看书来获得成长就像是用一条腿走路。 - -重视实践,充分运用感性认知潜能,**在项目中磨炼自己,才是正确的学习之道**。在实践中,在某些关键动作上刻意练习,也会取得事半功倍的效果。 - - -### 重视交流 - - -牛顿说:“**如果说我看得比别人远一些,那是因为我站在巨人的肩膀上。**”我们需要从别人身上学习。从老师、领导、同事、下属甚至对手身上学习,是快速成长的重要手段。 - -向老师和领导学习已经是人们生活习惯的一部分了。但是从同事甚至对手那里学习也很重要,因为这些人和我们自身更相似。所以要多多观察,取其所长,弃其所短。对于团队的小兄弟和下属,也要“不耻下问”。 - - -此外,在项目中积极参与具体方案讨论也非常重要。参与者先验感知了相关背景,并且讨论的观点和建议也是综合了发言者多种知识和技能。所以,讨论让参与者能够非常全面,立体地理解书本知识。同时,和高手讨论,他们的观点就会像修剪机剪树枝一样,快速的剪掉自己知识领域里面的疑惑点。 - - -### 重视总结和输出 - - -工程师在实践中会掌握大量细节,但是,即使掌握了所有细节,却没有深刻的总结和思考,也会陷入到“**学而不思则罔**”的境地。成长的“量变”来自于对细节的逐渐深入地把控,而**真正的“质变”来自于对“道”的更深层次的理解**。 - - -将经验输出,接受别人的检验是高层次的总结。这种输出不仅帮助了别人,对自身更是大有裨益。总结的方式有很多,包括组织分享,撰写技术文章等等。当然“日三省吾身”也是不错的总结方式。**总之,多多总结,多多分享,善莫大焉**! - -**解答别人的问题也是个人成长的重要手段**。有时候,某个问题自己本来不太懂,但是在给别人讲解的时候却豁然开朗。所以,“诲人不倦”利人惠己。 - -### 重视规划 - - -凡事预则立,不预则废。对于漫长的学习生涯而言,好的计划是成功的一半。 - - -#### 长期规划 - - -长期规划的实施需要毅力和决心,但是做正确的长期规划还需要高瞻远瞩的眼界、超级敏感的神经和中大奖的运气。对于大部分人来说,长期规划定主要是“定方向”。但遵循如下原则能够减少犯方向性错误的概率: - -- 远离日暮西山的行业 -- 做自己感兴趣的事情 -- 做有积累的事情 -- 一边走一边看,切勿一条道走到黑 - - - -#### 短期规划 - - -良好的短期规划应该在生活、成长、绩效和晋升之间取得平衡。大部分公司都会制定一个考核周期——少则一个月,多则一年。所以不妨以考核周期作为短期学习规划周期。本质上,规划是一个多目标优化问题,它有一系列的理论方案,这里不一一细说。基于相关理论,我给出一个简单易行的方案: - -- 确定目标优先级。比如:成长、生活、绩效 -- 确定每个目标的下限。从优化理论的角度来看,这被称为约束。比如绩效必须在一般以上,之前已经规划好的旅行不能更改,必须读完《Effective Java》等等 -- 优先为下限目标分配足够的资源。比如,事先规划好的旅行需要10天,这10天就必须预算出去 -- 按照各主目标的顺序依次分配资源。比如,最终分配给学习的时间是10天 -- 在给定的学习预算下,制定学习目标,要激进。然后给出执行方案。比如,学习目标是掌握基本的统计学知识,并成为Java专家。具体方案为:完成《Effective Java》、《Java Performance》、《Design Pattern》、《Head First Statistics》四本书的阅读 -- 对规划中的各学习任务按目标优先级进行排序,并最先启动优先级最高的任务。比如,最高优先级是掌握统计理论,那么就要先看《Head First Statistics》 - - - -对于该方案,要注意以下几点: - -- 最低目标必须能够轻松达成的目标,否则,从优化理论的角度来讲,该命题无解。比如,类似“半年内完成晋级两次、绩效全部S、从菜鸟成为Java专家”就不太合适作为最低目标。总之,要区分理想和梦想。 -- 主要目标规划必须具备一定的挑战性,需要规划出不可能完成的目标。过度规划本质上是一种贪婪算法,目的是目标价值最大化。因为一切皆有变数,如果其他目标能够提前完成,就不妨利用这些时间去完成更多的学习目标。总之,前途必须光明,道路必须坎坷。 -- 各目标之间不一定共享资源,规划不一定互有冲突。 - - - -此外,短期规划还可以从如下几个方面进行优化: - -- 学习计划最好能结合工作计划,理论联系实际结合,快速学以致用。比如,本季度规划去做一些数据分析工作,那么不妨把学习目标设置为学习统计知识。 -- 要灵活对待规划的目标和具体执行步骤,需要避免“郑人买履”式的笑话。面临新的挑战和变化,规划需要不断地调整。 - - - -## 那些令人纠结的困惑 - - -人生是一场马拉松,在漫长的征途中,难免有很多困惑。困惑就像枷锁,使我们步履蹒跚,困惑就像死锁,让我们停滞不前。 - - -接下来我将总结自己在工作中碰到和看到的一些典型困惑。这些困惑或者长期困扰作者本人,或者困扰我身边的同事和朋友。当这些困惑被释然之后,大家都感觉如重获释,为下一阶段的征程提供满满的正能量。**人生就像一场旅途,不必在乎目的地,在乎的,应该是沿途的风景,以及看风景的心情**。良好的心态是技术之旅最好的伴侣。期望通过这个解惑之旅,让大家拥有一个愉快的心情去感受漫长的学习旅途。 - - -### 学无止境吗 - - -必须要承认一个残酷的现实:人的生命是有限的,知识却是无限的。**用有限的生命去学习无限的知识是不可能完成的任务**。一想到此,有些工程师不免产生一些悲观情绪。如果方法得当并且足够勤奋,悲伤大可不必。 - - -虽然,人类的整体知识体系一直在扩张。但是就很多重要的工程细分领域,基础理论并不高深。计算机的很多重要领域,工程师有能力在有限时间内抓住核心要害。 - - -比如,密码学被认为是门非常高深的学科,但是一大类密码技术的基础是数论中一个非常简单的理论——素因数分解:给出两个素数,很容易算出它们的积,然而反过来给定两个素数的积,分解的计算量却非常惊人。 - - -“一致性”算得上是计算机领域里面最经典的难题,它是所有分布式系统的基础,从多核多CPU到多线程,从跨机器到跨机房,无所不在,几乎所有的计算机从业人员都在解决这个问题,但是Paxos给出了一个很优雅的解决方案。 - - -权限管理是很多工程师的噩梦,但如果你能搞定“Attribute Based Access Control(ABAC)”和“Role-Based Access Control(RBAC)”,也能达到相当高度。 - - -另外,技术学习是一场对抗赛,虽然学无止境,超越大部分对手就是一种胜利。所以,**以正确的学习方式,长时间投入就会形成核心竞争力**。 - - -### 没有绝对高明的技术,只有真正的高手 - - -致力于在技术上有所成就的工程师,都梦想有朝一日成为技术高手。但技术高手的标准却存在很大的争议。这是一个有着悠久历史的误解:以某种技术的掌握作为技术高手的评判标准。我经常碰到这样一些情景:因为掌握了某些技术,比如Spring、Kafka、Elasticsearch等,一些工程师就自封为高手。有些工程师非常仰慕别的团队,原因竟是那个团队使用了某种技术。 - - -这种误解的产生有几个原因:首先,技多不压身,技术自然是掌握的越多越好,掌握很多技术的人自然不是菜鸟。其次,在互联网时代来临之前,信息获取是非常昂贵的事情。这就导致一项技能的掌握可以给个人甚至整个公司带来优势地位。互联网时代,各种框架的出现以及开源的普及快速淘汰或者降低了很多技能的价值,同时降低了很多技术的学习门槛。所以,在当前,掌握某项技能知识只能是一个短期目标。怀揣某些技能就沾沾自喜的人需要记住:骄傲使人退步。 - -所谓“麻雀虽小,五脏俱全”。如果让你来做造物主,设计麻雀和设计大象的复杂度并没有明显区别。**一个看起来很小的业务需求,为了达到极致,所需要的技术和能力是非常综合和高深的**。真正的高手不是拿着所掌握的技术去卡客户需求,而是倾听客户的需求,给出精益求精的方案。完成客户的需求是一场擂台赛,真正的高手,是会见招拆招的。 - - -### 不做项目就无法成长吗 - - -在项目中学习是最快的成长方式之一,很多工程师非常享受这个过程。但是一年到头都做项目,你可能是在一家外包公司。对于一个做产品的公司,如果年头到年尾都在做项目,要不然就是在初步创业阶段,要不然就是做了大量失败的项目,总之不算是特别理想的状态。正常情况,在项目之间都会有一些非项目时间。在这段时间,有些同学会产生迷茫,成长很慢。 - - -项目真的是越多越好吗?答案显然是否定的。**重复的项目不会给工程师们带来新的成长**。不停的做项目,从而缺乏学习新知识的时间,会导致“做而不学则殆”。真正让工程师出类拔萃的是项目的深度,而不是不停地做项目。所以,**在项目之间的空档期,工程师们应该珍惜难得的喘息之机,深入思考,把项目做深、做精。** - -如何提高项目的深度呢?一般而言,任何项目都有一个目标,当项目完成后,目标就算基本达成了。但是,客户真的满意了吗?系统的可用性、可靠性、可扩展性、可维护性已经做到极致了吗?这几个问题的答案永远是否定的。**所以,任何一个有价值的项目,都可以一直深挖**。深挖项目,深度思考还可以锻炼工程师的创造力。期望不停地做项目的人,就像一个致力于训练更多千里马的人是发明不出汽车的。锻炼创造力也不是一蹴而就的事情,需要长时间地思考。总之,工程师们应该总是觉得时间不够用,毕竟时间是最宝贵的资源。 - - -### 职责真的很小吗 - - -很多时候,一个工程师所负责系统的数量和团队规模与其“江湖地位”正相关。但是,江湖地位与技术成长没有必然关联。提升技术能力的关键是项目深度以及客户的挑剔程度。项目越多,在单个项目中投入的时间就越少,容易陷入肤浅。特别需要避免的是“在其位不谋其政”的情况。团队越大,在管理方面需要投入的精力就越多。在管理技巧不成熟,技术眼界不够高的前提强行负责大团队,可能会导致个人疲于应付,团队毫无建树。最终“一将无能,累死三军”,效果可能适得其反。 - - -从技术发展的角度来说,技术管理者应该关注自己所能把控的活跃项目的数量,并致力于提高活跃项目的影响力和技术深度。团队人数要与个人管理能力、规划能力和需求把控能力相适应。一份工作让多个人来干,每个人的成长都受限。每个人都做简单重复的工作,对技术成长没有任何好处。团队管理和项目管理需要循序渐进,忌“拔苗助长”。 - - -### 一定要当老大吗 - - -有一些工程师的人生理想是做团队里的技术老大,这当然是一个值得称赞的理想。可是,如果整个团队技术能力一般,发展潜力一般,而你是技术最强者,这与其说是幸运,不如说是悲哀。这种场景被称之为“武大郎开店”。 团队里的技术顶尖高手不是不能做,但为了能够持续成长,需要满足如下几个条件: - -- 首先你得是行业里面的顶尖专家了——实在很难找到比你更强的人了! -- 其次,你经常需要承担对你自己的能力有挑战的任务,但同时你拥有一批聪明能干的队友。虽然你的技术能力最高,但是在你不熟悉的领域,你的队友能够进行探索并扩展整个团队的知识。 -- 最后,你必须要敏而好学,不耻下问。 - - - -否则,加入更强的技术团队或许是更好的选择,最少不是什么值得骄傲的事情。 - - -### 平台化的传说 - - -平台化算得上是“高大上”的代名词了,很多工程师挤破头就为了和“平台化”沾点边。然而和其他业务需求相比,平台化需求并没有本质上的区别。无论是平台化需求还是普通业务需求,它的价值都来自于客户价值。不同点如下: - -- 很多平台化需求的客户来自于技术团队,普通需求的客户来自于业务方。 -- 产品经理不同。普通业务需求来自于产品经理,平台化需求的产品经理可能就是工程师自己。长期被产品经理“压迫”的工程师们,在平台化上终于找到“翻身农奴把歌唱”的感觉。 -- 很多平台化的关注点是接入能力和可扩展性,而普通业务的关注点更多。 - - - -归根结底,平台化就是一种普通需求。在实施平台化之前,一定要避免下面两个误区: - -- 平台化绝对不是诸如“统一”、“全面”之类形容词的堆砌。是否需要平台化,应该综合考虑:客户数量,为客户解决的问题,以及客户价值是否值得平台化的投入。 -- 平台化不是你做平台,让客户来服务你。一些平台化设计者的规划设计里面,把大量的平台接入工作、脏活累活交给了客户,然后自己专注于所谓“最高大上”的功能。恰恰相反,平台化应该是客户什么都不做,所有的脏活累活都由平台方来做。本质上讲,平台化的价值来自于技术深度。真正体现技术深度的恰恰是设计者能够很轻松的把所有的脏活累活搞定。 - - - -所以平台化的最佳实践是:**投入最少的资源,解决最多的问题。平台解决一切,客户坐享其成。** - -### 搞基础技术就一定很牛吗 - - -经常听到同学们表达对基础技术部同学的敬仰之情,而对搞业务技术的同学表现出很轻视,认为存储、消息队列、服务治理框架(比如美团点评内部使用的OCTO)、Hadoop等才能被称为真正的技术。事实并非如此,更基础的并不一定更高深。 - - -比如下面这个流传很久的段子:越高级的语言就越没有技术含量。但真是这样吗,就拿Java和C来说,这是完全不同的两种语言,所需要的技能完全不同。C或许跟操作系统更加接近一点,和CPU、内存打交道的机会更多一点。但是为了用好Java,程序员在面向对象、设计模式、框架技术方面必须要非常精通。Java工程师转到C方向确实不容易,但作者也见过很多转到Java语言的C工程师水土不服。 - - -基础技术和业务应用技术必然会有不同的关注点,没有高低之分。之所以产生这种误解,有两个原因: - -- 基础技术相对成熟,有比较完整的体系,这给人一个高大上的感觉。业务应用技术相对来说,由于每个团队使用的不一样,所以成熟度参差不齐,影响力没有那么大。 -- 基础技术的门槛相对来说高一点,考虑到影响面,对可靠性、可用性等有比较高的最低要求。但是门槛高不代表技术含量高,另外成熟技术相对来说在创新方面会受到很大的约束。但是最先进的技术都来自活跃的创新。 - - - -对比下来,业务技术和基础技术各有千秋。但**真正的高手关注的是解决问题,所有的技术都是技能而已**。 - - -### 可行性调研的那些坑 - - -工作中开展可行性调研时有发生。做可行性调研要避免如下情况: - -- 把可行性调研做成不可行性调研。这真的非常糟糕。不可行性的结论往往是:因为这样或者那样的原因,所以不可行。 -- 避免“老鼠给猫挂铃铛”式的高风险可行性方案。“天下大事必作于细”,可行性调研一定要细致入微,避免粗枝大叶。 -- 避免调研时间过长。如果发现调研进展进入到指数级复杂度,也就是每前进一步需要之前两倍的时间投入,就应该果断的停止调研。 - - - -可行性调研的结论应该是收益与成本的折衷,格式一般如下: - -- 首先明确预期的结果,并按照高中低收益进行分级 -- 阐述达成每种预期结果需要采取的措施和方案 -- 给出实施各方案需要付出的成本 - - - -### 工程师天生不善沟通吗 - - -实际工作中,沟通所导致的问题层出不穷。工程师有不少是比较内向的,总是被贴上“不善沟通”的标签。实际上,**沟通能力是工程师最重要的能力之一**,良好的沟通是高效工作学习的基础,也是通过学习可以掌握的。下面我按工程师的语言说说沟通方面的经验。 - - -第一类常见的问题是沟通的可靠性。从可靠性的角度来讲,沟通分为TCP模式和UDP模式。TCP模式的形象表述是:我知道你知道。UDP模式的形象表述是:希望你知道。TCP模式当然比较可靠,不过成本比较高,UDP模式成本低,但是不可靠。在沟通可靠性方面,常见错误有如下两种: - -- 经常听到的这样的争论。一方说:“我已经告诉他了”,另一方说:“我不知道这个事情呀”。把UDP模式被当作TCP模式来使用容易产生扯皮。 -- 过度沟通。有些同学对沟通的可靠性产生了过度焦虑,不断的重复讨论已有结论问题。把TCP模式当成UDP来使用,效率会比较低。 - - - -第二类沟通问题是时效性问题。从时效性讲,沟通分为:同步模式和异步模式。同步沟通形象地说就是:你现在给我听好了。异步沟通的形象表述是:记得给我做好了。在沟通时效性方面,有如下两种常见错误: - -- 已经出现线上事故,紧急万分。大家你一言,我一语,感觉事故可能和某几个人有关,但是也不能完全确定,所以没有通知相关人员。最终,一个普通的事故变成了严重事故。对于紧急的事情,必须要同步沟通。 -- 半夜三点你正在熟睡,或者周末正在逛街,接到一个电话:“现在有个需求,能否立刻帮忙做完。”这会非常令人郁闷,因为那并不是紧急的事情。不是所有的需求都需要立刻解决。 - - - -有效沟通的一个重要原则是**提前沟通**。沟通本质是信息交流和处理,可以把被沟通对象形象地比喻成串行信息处理的CPU。提前沟通,意味着将处理请求尽早放入处理队列里面。下面的例子让很多工程师深恶痛绝:一个需求策划了1个月,产品设计了2周。当开发工程是第一次听说该需求的时候,发现开发的时间是2天。工程师据理力争,加班加点1周搞定。最后的结论是工程师非常不给力,不配合。就像工程师讨厌类似需求一样。要协调一个大项目,希望获得别人的配合,也需要尽早沟通。 - - -有效沟通的另外一个重点是“**不要跑题**”。很多看起来很接近的问题,本质上是完全不同的问题。比如:一个会议的主题是“如何实施一个方案”,有人却可能提出“是否应该实施该方案”。 “如何实施”和“是否应该实施”是完全不同的两个问题,很多看起来相关的问题实际上跑题很远。“跑题”是导致无效沟通的重要原因。 - -良好沟通的奥秘在于能掌握TCP模式和UDP模式精髓,正确判断问题的紧急性,尽量提前沟通,避免跑题。 - - -### 带人之道 - - -有些初为导师的工程师由于担心毕业生的能力太弱,安排任务时候谆谆教诲,最后感觉还是有所顾虑,干脆自己写代码。同样的事情发生在很多刚刚管理小团队的工程师身上。最终的结果他们:写完所有的代码,让下属无代码可写。“事必躬亲”当然非常糟糕,最终的往往是团队的整体绩效不高,团队成员的成长很慢,而自己却很累。 - -古人说:“**用人不疑,疑人不用**。”这句话并非“放之四海而皆准”。在古代,受限于通信技术,反馈延迟显著,而且信息在传递过程中有大量噪音,变形严重。在这种情况下,如果根据短期内收集的少量变形的信息做快速决断,容易陷于草率。在公司里,这句话用于选人环节更为恰当,应该改为:**录用不疑,疑人不录。** - -考虑到招聘成本,就算是在录用层面,有时候也无法做到。作为一个小团队的管理者,能够快速准确的获取团队成员的各种反馈信息,完全不需要“用人不疑,疑人不用”。用人的真正理论基础来自于“探索和利用”(Exploration and Exploitation )。不能因为下属能做什么就只让他做什么,更不能因为下属一次失败就不给机会。 - -根据经典的“探索和利用”(Exploration and Exploitation )理论,良好的用人方式应该如下: - -- 首选选择相信,在面临失败后,收缩信任度 -- 查找失败的原因,提供改进意见,提升下属的能力 -- 总是给下属机会,在恰当地时机给下属更高的挑战。 总之,苍天大树来自一颗小种子,要相信成长的力量 - - - -### 效率、效率、效率 - - -经常看到有些同学给自己的绩效评分是100分——满分,原因是在过去一段时间太辛苦了,但最终的绩效却一般般。天道酬勤不错,但是天道更酬巧。工程师们都学过数据结构,不同算法的时间复杂度的差距,仅仅通过更长的工作时间是难以弥补的。为了提升工作学习效率,我们需要注意以下几点: - -- 主要关注效率提升。很多时候,与效率提升所带来的收益相比,延长时间所带来的成果往往不值得一提。 -- 要有清晰的结果导向思维。功劳和苦劳不是一回事。 -- 做正确的事情,而不仅仅正确地做事情。这是一个被不断提起的话题,但是错误每天都上演。为了在规定的时间内完成一个大项目,总是要有所取舍。如果没有重点,均匀发力,容易事倍功半。如果“南辕北辙”,更是可悲可叹。 - - - -## 架构师能力模型 - - -前面我们已经讲完了原则和一些困惑,那么工程师到底应该怎么提升自己呢? - -成为优秀的架构师是大部分初中级工程师的阶段性目标。**优秀的架构师往往具备八种核心能力:编程能力、调试能力、编译部署能力、性能优化能力、业务架构能力、在线运维能力、项目管理能力和规划能力。** - -这几种能力之间的关系大概如下图。编程能力、调试能力和编译部署能力属于最基础的能力。不能精通掌握这三种能力,很难在性能优化能力和业务架构能力方面有所成就。具备了一定的性能优化能力和业务架构能力之后,才能在线运维能力和项目管理能力方面表现优越。团队管理能力是最高能力,它对项目管理能力的依赖度更大。 -![](https://img2020.cnblogs.com/blog/1546632/202103/1546632-20210307223259842-983447200.png) - -### 编程能力 - - -对工程师而言,编程是最基础的能力,必备技能。其本质是一个翻译能力,将业务需求翻译成机器能懂的语言。 - -提升编程能力的书籍有很多。精通面向对象和设计模式是高效编程的基础。初级工程师应该多写代码、多看代码。找高手做Code Review,也是提升编程水平的捷径。 - -### 调试能力 - - -程序代码是系统的静态形式,调试的目的是通过查看程序的运行时状态来验证和优化系统。本质上讲,工程师们通过不断调试可以持续强化其通过静态代码去预测运行状态的能力。所以调试能力也是工程师编程能力提升的关键手段。很早之前有个传说:“调试能力有多强,编程能力就有多强。”不过现在很多编辑器的功能很强大,调试能力的门槛已经大大降低。 - -调试能力是项目能否按时、高质量提交的关键。即使一个稍具复杂度的项目,大部分工程师也无法一次性准确无误的完成。大项目都是通过不断地调试进行优化和纠错的。所以调试能力是不可或缺的能力。 - -多写程序,解决Bug,多请教高手是提升调试能力的重要手段。 - -### 编译部署能力 - - -编译并在线上部署运行程序是系统上线的最后一个环节。随着SOA架构的普及以及业务复杂度的增加,大部分系统只是一个完整业务的一个环节,因此,本地编译和运行并不能完全模拟系统在线运行。为了快速验证所编写程序的正确性,编译并在线上部署就成了必要环节。所以编译部署能力是一个必备技能。 - -让盘根错节的众多子系统运行起来是个不小的挑战。得益于SOA架构的普及以及大量编译、部署工具的发展,编译部署的门槛已经大大降低。基于应用层进行开发的公司,已经很少有“编译工程师”的角色了。但是对于初级工程师而言,编译部署仍然不是一个轻松的事情。 - -### 性能优化能力 - - -衡量一个系统成功的一个重要指标是使用量。随着使用量的增加和业务复杂度的增加,大部分系统最终都会碰到性能问题。 性能优化能力是一个综合能力。因为: - -- 影响系统性能的因素众多,包括:数据结构、操作系统、虚拟机、CPU、存储、网络等。为了对系统性能进行调优,架构师需要掌握所有相关的技术 -- 精通性能优化意味着深刻理解可用性、可靠性、一致性、可维护性、可扩展性等的本质 -- 性能优化与业务强耦合,最终所采取的手段是往往折衷的结果。所以,性能优化要深谙妥协的艺术 - - - -可以说,性能优化能力是工程师们成长过程中各种技能开始融会贯通的一个标志。这方面可以参考之前的博客文章“常见性能优化策略的总结”。市场上还有很多与性能优化相关的书籍,大家可以参考。**多多阅读开源框架中关于性能优化方面的文档和代码也不失为好的提升手段。动手解决线上性能问题也是提升性能优化能力的关键。如果有机会,跟着高手学习,分析性能优化解决方案案例**(我们技术博客之前也发表了很多这方面的文章),也是快速提升性能优化能力的手段。 - - -### 在线运维能力 - - -如果说性能优化能力体现的是架构师的静态思考能力,在线运维能力考验的就是动态反应能力。残酷的现实是,无论程序多么完美,Bug永远存在。与此同时,职位越高、责任越大,很多架构师需要负责非常重要的在线系统。对于线上故障,如果不能提前预防以及快速解决,损失可能不堪设想,所以在线运维能力是优秀架构师的必备技能。 - -为了对线上故障进行快速处理,标准化的监控、上报、升级,以及基本应对机制当然很重要。通过所观察到的现象,快速定位、缓解以及解决相关症状也相当关键。这要求架构师对故障系统的业务、技术具备通盘解读能力。解决线上故障的架构师就好比一个在参加比赛F1的车手。赛车手必须要了解自身、赛车、对手、同伴、天气、场地等所有因素,快速决策,不断调整。架构师必须要了解所有技术细节、业务细节、处理规范、同伴等众多因素,快速决断,迅速调整。 - -在线运维本质上是一个强化学习的过程。很多能力都可以通过看书、查资料来完成,但在线运维能力往往需要大量的实践来提升。 - -### 业务架构能力 - - -工程师抱怨产品经理的故事屡见不鲜,抱怨最多的主要原因来自于需求的频繁变更。需求变更主要有两个来源:第一个原因是市场改变或战略调整,第二个原因是伪需求。对于第一个原因,无论是工程师还是产品经理,都只能无奈的接受。优秀的架构师应该具备减少第二种原因所导致的需求变更的概率。 - -**伪需求的产生有两个原因**: - -- 第一个原因是需求传递变形。从信息论的角度来讲,任何沟通都是一个编码和解码的过程。典型的需求从需求方到产品经理,最终到开发工程师,最少需要经历三次编码和解码过程。而信息的每一次传递都存在一些损失并带来一些噪音,这导致有些时候开发出来的产品完全对不上需求。此外,需求方和产品经理在需求可行性、系统可靠性,开发成本控制方面的把控比较弱,也会导致需求变形。 -- 第二个原因就是需求方完全没有想好自己的需求。 - - - -优秀的架构师应该具备辨别真伪需求的能力。应该花时间去了解客户的真实业务场景,具备较强的业务抽象能力,洞悉客户的真实需求。系统的真正实施方是工程师,在明确客户真实需求后,高明的架构师应该具备准确判断项目对可行性、可靠性、可用性等方面的要求,并能具备成本意识。最后,由于需求与在线系统的紧耦合关系,掌握在线系统的各种细节也是成功的业务架构的关键。随着级别的提升,工程师所面对的需求会越来越抽象。**承接抽象需求,提供抽象架构是架构师走向卓越的必经之途**。 - -市场上有一些关于如何成为架构师的书,大家可以参考。**但是架构能力的提升,实践可能是更重要的方式**。业务架构师应该关注客户的痛点而不是PRD文档,应该深入关注真实业务。掌握现存系统的大量技术和业务细节也是业务架构师的必备知识。 - -### 项目管理能力 - - -作为工业时代的产物,分工合作融入在互联网项目基因里面。架构师也需要负责几个重大项目才能给自己正名。以架构师角色去管理项目,业务架构能力当然是必备技能。此外,人员管理和成本控制意识也非常重要。 - -项目管理还意味着要有一个大心脏。重大项目涉及技术攻关、人员变动、需求更改等众多可变因素。面临各种变化,还要在确保目标顺利达成,需要较强的抗压能力。 - -人员管理需要注意的方面包括:知人善用,优化关系,简化沟通,坚持真理。 - -- 知人善用意味着架构师需要了解每个参与者的硬技能和软素质。同时,关注团队成员在项目过程中的表现,按能分配 -- 优化关系意味着管理团队的情绪,毕竟项目的核心是团队,有士气的团队才能高效达成目标。 -- 简化沟通意味着快速决策,该妥协的时候妥协,权责分明。 -- 坚持真理意味着顶住压力,在原则性问题上绝不退步。 - - - -成本控制意味着对项目进行精细化管理,需要遵循如下几个原则: - -- 以终为始、确定里程碑。为了达成目标,所有的计划必须以终为始来制定。将大项目分解成几个小阶段,控制每个阶段的里程碑可以大大降低项目失败的风险。 -- 把控关键路径和关键项目。按照关键路径管理理论(CPM)的要求,架构师需要确定每个子项目的关键路径,确定其最早和最晚启动时间。同时,架构师需要关注那些可能会导致项目整体延期的关键节点,并集中力量攻破。 -- 掌控团队成员的张弛度。大项目持续时间会比较长,也包含不同工种。项目实施是一个不断变化的动态过程,在这个过程中不是整个周期都很紧张,不是所有的工种都一样忙。优秀的架构师必须要具备精细阅读整体项目以及快速反应和实时调整的能力。这不仅仅可以大大降低项目成本,还可以提高产出质量和团队满意度。总体来说,“前紧后松”是项目管理的一个重要原则。 - - - -项目管理方面的书籍很多。但是,提高业务架构能力同样重要。积极参与大项目并观察别人管理项目的方式也是非常重要的提升手段。 - -### 团队管理能力 - - -不想做CTO的工程师不是一个好的架构师。走向技术管理应该是工程师的一个主流职业规划。团队管理的一个核心能力就是规划能力,这包括项目规划和人员规划。良好的规划需要遵循如下原则: - -- 规划是利益的博弈。良好的规划上面对得起老板,中间对得起自己,下面对得起团队。在三者利益者寻找平衡点,实现多方共赢考验着管理者的智慧和精细拿捏的能力。 -- 任何规划都比没有规划好。没有规划的团队就是没头的苍蝇,不符合所有人的利益。 -- 规划不是本本主义。市场在变,团队在变,规划也不应该一成不变。 -- 客户至上的是项目规划的出发点。 -- 就人员规划而言,规划需要考量团队成员的能力、绩效、成长等多方面的因素。 - - - -市场上有很多规划管理方面的书籍,值得阅读。最优化理论虽然是技术书籍,但它是规划的理论基础,所以不妨多看看翻阅一下。从自我规划开始,多多学习别人的规划也是规划能力提升的重要手段。 - -## 写在后面 - - -该文系统性地阐述了学习原则、分析了常见困惑,并制定明确学习目标,期望对工程师们的工作学习有所帮助。 - -欢迎大家在评论中分享自己在学习成长方面的心得。 \ No newline at end of file diff --git a/Life/what-are-the-most-common-lies-told-by-programmers.md b/Life/what-are-the-most-common-lies-told-by-programmers.md deleted file mode 100644 index 743b563..0000000 --- a/Life/what-are-the-most-common-lies-told-by-programmers.md +++ /dev/null @@ -1,259 +0,0 @@ -# 程序员的谎言 - -梦短梦长俱是梦,年来年去是何年。去年的今天,我还在上海,整理了 [请用一句话证明你是程序员](https://www.cnblogs.com/bytesfly/p/how-to-prove-that-you-are-a-programmer.html) 。而今天的我已回合肥工作半年多时间,很庆幸又见故乡庐州月。 - -> 儿时凿壁偷了谁家的光 -> 宿昔不梳 忆苦十年寒窗 -> ... -> 半生浮名只是虚妄 -> 三月 一路烟霞 莺飞草长 -> 柳絮纷飞里看见了故乡 -> ... - - -又忍不住感概了几句,打住打住。在1024这个特殊的日子里,总想整理一些非技术相关的东西。 - -很多人都知道销售人员的话不能全信。在IT行业中,上身格子衬衫、双肩包,下身牛仔裤、运动鞋,生活低调、工作热情、代码风骚、内心狂热的程序员经常被贴上“少言寡语”、“老实人”等标签,那么这个“老实人”群体真的“老实”吗? - -[https://www.quora.com/What-are-the-most-common-lies-told-by-programmers](https://www.quora.com/What-are-the-most-common-lies-told-by-programmers) - -本篇参考上面这一篇帖子,结合自身工作日常以及国内程序员的调侃,总结了如下程序员常说的“谎言”。 - -
- -> Yeah, I tested it. - -放心,我测过! - -
- -> Only need to change one line of code, and will not affect other programs. - -只需要改一行代码,不会影响其他地方的。 - -
- -> I'll comment and document my code later. - -我以后再给代码写注释和文档。 - -
- -> TODO -> FIXME - -后续再优化完善 - -
- -> That's easy, it'll just take a couple of days. - -那个简单,几天就能搞定。 - -
- -> It works on my machine. - -在我机器上没问题。 - -
- -> I'll add unit tests in the next code change. - -下次提交代码我再添加单元测试。 - -
- -> I am 90% finished. - -我已经完成90%了。 - -
- -> // This can never happen. - -// 这种情况不可能发生。 - -
- -> It's not a bug, it's a feature! - -这不是Bug,这是特性! - -
- -> I'll be done in like 2 hours. - -大概2小时能搞定。 - -
- -> I only have like 3 things left to finish. - -只剩下3个小问题了。 - -
- -> It's almost done. - -就快完成了。 - -
- -> Yeah, it is a known bug. - -这是一个已知(潜台词当前无法解决)问题。 - -
- -> It will just take 2 minutes to fix ! - -这个两分钟就可以修复! - -
- -> You must have the wrong version. - -一定是你使用的版本不对。 - -
- -> I haven't touched that module in weeks! - -我已经好几周没动过那个模块了! - -
- -> It must be a hardware problem, nothing to do with software. - -这是硬件问题,与程序无关。 - -
- -> It's just some unlucky coincidence. - -这只是偶发事件。 - -
- -> There has to be something funky in your data. - -你的数据肯定有问题。 - -
- -> I'll improve the solution later. - -以后再改进方案。 - -
- -> This code is self-documenting. - -代码即文档。 - -
- -> This fix is so simple that we can put it straight into production. - -这次修复改动很小,可以直接放到生产环境。 - -
- -> It was working earlier. Some one changed something! - -先前还是好的,肯定有人动了什么东西! - -
- -> It's just a temporary solution, it won't be used in production. - -这只是个临时方案,不会用在生产环境。 - -
- -> We can solve this problem in the next version. - -下个版本解决这个问题。 - -
- -> git commit -m "changing the log message", but the truth is fixing some bug. - -提交代码时描述信息是修改日志,实际这个提交修复了一些bug。 - -
- -> Yes, I have a girlfriend. - -我有女朋友。 - -
- -> The bug isn't in my code; you are using it incorrectly. - -这不是代码问题,只是你不会用而已。 - -
- -> I'll just cut-and-paste this code for now, but I'll make a super-class when I have a moment. - -我先拷贝代码,等以后有时间再提取一个父类出来。 - -
- -> Dev: This will take 10 days. -> Boss: Can you do it in 5? -> Dev: Sure! - -开发人员:这个需要10天做完。 -老板:5天不行? -开发人员:差不多吧! - -
- -> Environment issue. Was working fine on my dev box! - -肯定是环境问题,在开发环境不跑得挺好嘛! - -
- -> Code is compiling. - -代码正在编译。 - -
- -> It was working ...did something on the network change? - -现在来看一切正常,当时是不是网络抖动引起的? - -
- -> This refactor is small and low risk. - -这次重构很小,基本没什么风险。 - -
- -> Documentation is coming soon. - -文档再等等。 - -
- -> Just comment this code for now , it may be used later. - -先注释掉这里的代码,以后说不定还能用到。 - -
- -在面对一个不想接的需求时,程序员的几个理由: -1. 这个需求太out了 -2. 目前服务器性能跟不上 -3. 现有的技术实现不了 -4. 去找老板排期吧,这个很费时间的 - -
- -同行的朋友们,回忆回忆这些年你们说过哪些“谎言”,欢迎评论区补充! \ No newline at end of file diff --git a/MySQL/how-to-use-mysql-explain.md b/MySQL/how-to-use-mysql-explain.md deleted file mode 100644 index 96815dc..0000000 --- a/MySQL/how-to-use-mysql-explain.md +++ /dev/null @@ -1,692 +0,0 @@ -# 一文学会MySQL的explain工具 - -## 开篇说明 - -(1) 本文将细致介绍MySQL的explain工具,是下一篇《一文读懂MySQL的索引机制及查询优化》的准备篇。 - -(2) 本文主要基于MySQL`5.7`版本(`https://dev.mysql.com/doc/refman/5.7/en/`),MySQL`8.x`版本可另行翻阅对应版本文档(`https://dev.mysql.com/doc/refman/8.0/en/`)。 - -(3) 演示过程中的建库、建表、建索引等语句仅为了测试explain工具的使用,并未考虑实际应用场景的合理性。 - -## explain工具介绍 - -相关文档: -`https://dev.mysql.com/doc/refman/5.7/en/explain.html` -`https://dev.mysql.com/doc/refman/5.7/en/using-explain.html` - -> EXPLAIN is used to obtain a query execution plan (that is, an explanation of how MySQL would execute a query). - -简单翻译一下,就是explain用于获取查询执行计划(即MySQL是如何执行一个查询的)。 - -工作中,我们会遇到慢查询,这个时候我们就可以在`select`语句之前增加`explain`关键字,模拟MySQL优化器执行SQL语句,从而分析该SQL语句有没有用上索引、是否全表扫描、能否进一步优化等。 - -还是来个快速入门的案例比较直观,依次在mysql的命令行执行下面几条语句(建库、建表sql脚本见下面的`数据准备`部分): -```sh -mysql> use `explain_test`; -mysql> select * from tb_hero where hero_name = '李寻欢' and book_id = 1; -mysql> explain select * from tb_hero where hero_name = '李寻欢' and book_id = 1; -mysql> show warnings \G -``` -得到下面的输出: - -```sh -mysql> use `explain_test`; -Database changed -mysql> select * from tb_hero where hero_name = '李寻欢' and book_id = 1; -+---------+-----------+--------------+---------+ -| hero_id | hero_name | skill | book_id | -+---------+-----------+--------------+---------+ -| 1 | 李寻欢 | 小李飞刀 | 1 | -+---------+-----------+--------------+---------+ -1 row in set (0.00 sec) - -mysql> explain select * from tb_hero where hero_name = '李寻欢' and book_id = 1; -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------------+------+----------+-------+ -| 1 | SIMPLE | tb_hero | NULL | ref | idx_book_id_hero_name | idx_book_id_hero_name | 136 | const,const | 1 | 100.00 | NULL | -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------------+------+----------+-------+ -1 row in set, 1 warning (0.00 sec) - -mysql> show warnings \G -*************************** 1. row *************************** - Level: Note - Code: 1003 -Message: /* select#1 */ select `explain_test`.`tb_hero`.`hero_id` AS `hero_id`,`explain_test`.`tb_hero`.`hero_name` AS `hero_name`,`explain_test`.`tb_hero`.`skill` AS `skill`,`explain_test`.`tb_hero`.`book_id` AS `book_id` from `explain_test`.`tb_hero` where ((`explain_test`.`tb_hero`.`book_id` = 1) and (`explain_test`.`tb_hero`.`hero_name` = '李寻欢')) -1 row in set (0.00 sec) -``` - -先别急`explain`语句输出结果每一列表示什么意思(后面会具体描述),用`show warnings`命令可以得到优化后的查询语句大致长什么样子。 - -补充: -- 有关`show warnings`更详细的使用见`https://dev.mysql.com/doc/refman/5.7/en/show-warnings.html` -- 有关获取`explain`额外的输出信息见`https://dev.mysql.com/doc/refman/5.7/en/explain-extended.html` - -原SQL语句: -```sh -select * from tb_hero where hero_name = '李寻欢' and book_id = 1; -``` -优化后的SQL语句: -```sh -select `explain_test`.`tb_hero`.`hero_id` AS `hero_id`, - `explain_test`.`tb_hero`.`hero_name` AS `hero_name`, - `explain_test`.`tb_hero`.`skill` AS `skill`, - `explain_test`.`tb_hero`.`book_id` AS `book_id` -from `explain_test`.`tb_hero` -where ((`explain_test`.`tb_hero`.`book_id` = 1) and (`explain_test`.`tb_hero`.`hero_name` = '李寻欢')) -``` -可以看出,MySQL优化器把`*`优化成具体的列名,另外把我`where`中的两个过滤条件`hero_name`、`book_id`先后顺序调换了一下,这种顺序调换是概率性事件还是另有文章? -(哈哈哈,(●´ω`●)留个悬念,本篇仅介绍explain工具,读了下篇《一文读懂MySQL的索引机制及查询优化》后自然豁然开朗) - -## 数据准备 - -为了方便演示explain工具的使用以及输出结果的含义,准备了一些测试数据,初始化sql脚本如下: - -```sh --- ---------------------------- --- create database --- ---------------------------- -DROP database IF EXISTS `explain_test`; -create database `explain_test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- switch database -use `explain_test`; - --- ---------------------------- --- table structure for `tb_book` --- ---------------------------- -DROP TABLE IF EXISTS `tb_book`; -CREATE TABLE `tb_book` ( - `book_id` int(11) NOT NULL, - `book_name` varchar(64) DEFAULT NULL, - `author` varchar(32) DEFAULT NULL, - PRIMARY KEY (`book_id`), - UNIQUE KEY `uk_book_name` (`book_name`) USING BTREE, - INDEX `idx_author` (`author`) USING BTREE -) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -BEGIN; -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (1, '多情剑客无情剑', '古龙'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (2, '笑傲江湖', '金庸'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (3, '倚天屠龙记', '金庸'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (4, '射雕英雄传', '金庸'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (5, '绝代双骄', '古龙'); -COMMIT; - --- ---------------------------- --- table structure for `tb_hero` --- ---------------------------- -DROP TABLE IF EXISTS `tb_hero`; -CREATE TABLE `tb_hero` ( - `hero_id` int(11) NOT NULL, - `hero_name` varchar(32) DEFAULT NULL, - `skill` varchar(64) DEFAULT NULL, - `book_id` int(11) DEFAULT NULL, - PRIMARY KEY (`hero_id`), - INDEX `idx_book_id_hero_name`(`book_id`, `hero_name`) USING BTREE -) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -BEGIN; -INSERT INTO `tb_hero`(`hero_id`, `hero_name`, `skill`, `book_id`) VALUES (1, '李寻欢', '小李飞刀', 1); -INSERT INTO `tb_hero`(`hero_id`, `hero_name`, `skill`, `book_id`) VALUES (2, '令狐冲', '独孤九剑', 2); -INSERT INTO `tb_hero`(`hero_id`, `hero_name`, `skill`, `book_id`) VALUES (3, '张无忌', '九阳神功', 3); -INSERT INTO `tb_hero`(`hero_id`, `hero_name`, `skill`, `book_id`) VALUES (4, '郭靖', '降龙十八掌', 4); -INSERT INTO `tb_hero`(`hero_id`, `hero_name`, `skill`, `book_id`) VALUES (5, '花无缺', '移花接玉', 5); -INSERT INTO `tb_hero`(`hero_id`, `hero_name`, `skill`, `book_id`) VALUES (6, '任我行', '吸星大法', 2); -COMMIT; - --- ---------------------------- --- Table structure for `tb_book_hero` --- ---------------------------- -DROP TABLE IF EXISTS `tb_book_hero`; -CREATE TABLE `tb_book_hero` ( - `book_id` int(11) NOT NULL, - `hero_id` int(11) NOT NULL, - `user_comment` varchar(255) DEFAULT NULL, - PRIMARY KEY (`book_id`, `hero_id`) USING BTREE -) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -BEGIN; -INSERT INTO `tb_book_hero`(`book_id`, `hero_id`, `user_comment`) VALUES (1, 1, '小李飞刀,例无虚发,夺魂索命,弹指之间'); -INSERT INTO `tb_book_hero`(`book_id`, `hero_id`, `user_comment`) VALUES (2, 2, '令狐少侠留步!'); -INSERT INTO `tb_book_hero`(`book_id`, `hero_id`, `user_comment`) VALUES (3, 3, '尝遍世间善恶,归来仍是少年'); -INSERT INTO `tb_book_hero`(`book_id`, `hero_id`, `user_comment`) VALUES (4, 4, '我只要我的靖哥哥!'); -INSERT INTO `tb_book_hero`(`book_id`, `hero_id`, `user_comment`) VALUES (5, 5, '风采儒雅亦坦荡,武艺精深兼明智。'); -INSERT INTO `tb_book_hero`(`book_id`, `hero_id`, `user_comment`) VALUES (2, 6, '有人就有恩怨,有恩怨就有江湖,人心即是江湖,你如何退出!'); -COMMIT; -``` - -## explain的输出结果 - -相关文档: -`https://dev.mysql.com/doc/refman/5.7/en/explain-output.html` - -看一下官方文档显示的关于explain输出结果列(`explain output columns`)的含义: - -|Column |JSON Name| Meaning| -|--|--|--| -|id| select_id |The SELECT identifier -|select_type| None| The SELECT type| -|table| table_name| The table for the output row| -|partitions| partitions| The matching partitions| -|type| access_type| The join type| -|possible_keys| possible_keys| The possible indexes to choose| -|key| key| The index actually chosen| -|key_len| key_length| The length of the chosen key| -|ref| ref| The columns compared to the index| -|rows| rows| Estimate of rows to be examined| -|filtered| filtered| Percentage of rows filtered by table condition| -|Extra| None| Additional information| - -其中`JSON Name`指的是当设定`FORMAT=JSON`时,列名在json中显示的name,见下面的演示就明白了 -```sh -mysql> explain select * from tb_book \G -*************************** 1. row *************************** - id: 1 - select_type: SIMPLE - table: tb_book - partitions: NULL - type: ALL -possible_keys: NULL - key: NULL - key_len: NULL - ref: NULL - rows: 5 - filtered: 100.00 - Extra: NULL -1 row in set, 1 warning (0.00 sec) - -mysql> explain FORMAT=JSON select * from tb_book \G -*************************** 1. row *************************** -EXPLAIN: { - "query_block": { - "select_id": 1, - "cost_info": { - "query_cost": "2.00" - }, - "table": { - "table_name": "tb_book", - "access_type": "ALL", - "rows_examined_per_scan": 5, - "rows_produced_per_join": 5, - "filtered": "100.00", - "cost_info": { - "read_cost": "1.00", - "eval_cost": "1.00", - "prefix_cost": "2.00", - "data_read_per_join": "1K" - }, - "used_columns": [ - "book_id", - "book_name", - "author" - ] - } - } -} -1 row in set, 1 warning (0.00 sec) -``` -下面重点看一下比较重要的几个字段。 - -### id列 - -`id`是`select`的唯一标识,有几个`select`就有几个id,并且id的顺序是按`select`出现的顺序增长的,id值越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。 - -为了验证上面的结论,临时关闭mysql`5.7`对子查询(`sub queries`)产生的衍生表(`derived tables`)的合并优化 -```sh -set session optimizer_switch='derived_merge=off'; -``` -详情见: -`https://dev.mysql.com/doc/refman/5.7/en/switchable-optimizations.html` - -`https://dev.mysql.com/doc/refman/5.7/en/derived-table-optimization.html` - -```sh -mysql> set session optimizer_switch='derived_merge=off'; -Query OK, 0 rows affected (0.00 sec) - -mysql> select (select count(1) from tb_book) as book_count, (select count(1) from tb_hero) as hero_count from (select * from tb_book_hero) as book_hero; -+------------+------------+ -| book_count | hero_count | -+------------+------------+ -| 5 | 6 | -| 5 | 6 | -| 5 | 6 | -| 5 | 6 | -| 5 | 6 | -| 5 | 6 | -+------------+------------+ -6 rows in set (0.00 sec) - -mysql> explain select (select count(1) from tb_book) as book_count, (select count(1) from tb_hero) as hero_count from (select * from tb_book_hero) as book_hero; -+----+-------------+--------------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ -| 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 100.00 | NULL | -| 4 | DERIVED | tb_book_hero | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 100.00 | NULL | -| 3 | SUBQUERY | tb_hero | NULL | index | NULL | idx_book_id_hero_name | 136 | NULL | 6 | 100.00 | Using index | -| 2 | SUBQUERY | tb_book | NULL | index | NULL | uk_book_name | 259 | NULL | 5 | 100.00 | Using index | -+----+-------------+--------------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ -4 rows in set, 1 warning (0.00 sec) - -mysql> set session optimizer_switch='derived_merge=on'; -Query OK, 0 rows affected (0.00 sec) -``` -可见,查询语句中有4个select,先执行的是`select * from tb_book_hero`,然后执行`select count(1) from tb_hero`,再执行`select count(1) from tb_book`,最后执行`select book_count, hero_count from book_hero` - -### select_type列 - -`select_type`表示的是查询类型,常见的包括`SIMPLE`、`PRIMARY`、`SUBQUERY`、`DERIVED`、`UNION` - -(1) SIMPLE:简单查询(不包含子查询和UNION查询) - -```sh -mysql> explain select * from tb_book where book_id = 1; -+----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ -| 1 | SIMPLE | tb_book | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | -+----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ -1 row in set, 1 warning (0.00 sec) -``` - -(2) PRIMARY:复杂查询中最外层的查询 -(3) SUBQUERY:包含在select中的子查询(不在from子句中) -(4) DERIVED:包含在from子句中的子查询,MySQL会将结果存放在一个临时表中,也称为派生表(`derived tables`) - -这3种select_type见下面的例子 -```sh -mysql> set session optimizer_switch='derived_merge=off'; -Query OK, 0 rows affected (0.00 sec) - -mysql> explain select (select count(1) from tb_book) as book_count from (select * from tb_book_hero) as book_hero; -+----+-------------+--------------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -| 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 100.00 | NULL | -| 3 | DERIVED | tb_book_hero | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 100.00 | NULL | -| 2 | SUBQUERY | tb_book | NULL | index | NULL | uk_book_name | 259 | NULL | 5 | 100.00 | Using index | -+----+-------------+--------------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -3 rows in set, 1 warning (0.00 sec) - -mysql> set session optimizer_switch='derived_merge=on'; -Query OK, 0 rows affected (0.00 sec) -``` - -(5) UNION:在UNION中的第二个和随后的select - -```sh -mysql> select * from tb_book where book_id = 1 union all select * from tb_book where book_name = '笑傲江湖'; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情剑客无情剑 | 古龙 | -| 2 | 笑傲江湖 | 金庸 | -+---------+-----------------------+--------+ -2 rows in set (0.00 sec) - -mysql> explain select * from tb_book where book_id = 1 union all select * from tb_book where book_name = '笑傲江湖'; -+----+-------------+---------+------------+-------+---------------+--------------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+--------------+---------+-------+------+----------+-------+ -| 1 | PRIMARY | tb_book | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | -| 2 | UNION | tb_book | NULL | const | uk_book_name | uk_book_name | 259 | const | 1 | 100.00 | NULL | -+----+-------------+---------+------------+-------+---------------+--------------+---------+-------+------+----------+-------+ -2 rows in set, 1 warning (0.00 sec) -``` - -### table列 - -`table`表示查询涉及的表或衍生表。 - -常见table列是``格式,表示当前查询依赖`id=N`的查询,需先执行`id=N`的查询。上面含`select_type`为`DERIVED`的查询就是这种情况,这里不再重复举例。 - -### type列 - -相关文档: -`https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types` - -type列是判断查询是否高效的重要依据,我们可以通过type字段的值,判断此次查询是`全表扫描`还是`索引扫描`等,进而进一步优化查询。 - -一般来说表示查询性能最优到最差依次为:`NULL > system > const > eq_ref > ref > range > index > ALL` - -前面的几种类型都是利用到了索引来查询数据, 因此可以过滤部分或大部分数据, 查询效率自然就比较高了。 -而后面的`index`类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比`ALL`类型稍快。 -所以,应当尽可能地保证查询达到`range`级别,最好达到`ref`。 - - -(0) NULL: 不用访问表或者索引,直接就能得到结果,如:在索引列中选取最大值,执行时不需要再访问表 -```sh -mysql> explain select max(book_id) from tb_book; -+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ -| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away | -+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ -1 row in set, 1 warning (0.00 sec) -``` - -(1) system:The table has only one row. This is a special case of the `const` join type. - -当查询的表只有一行的情况下,`system`是`const`类型的特例, - -(2) const:It is used when you compare all parts of a `PRIMARY KEY` or `UNIQUE index` to `constant values`. - -针对`主键`或`唯一索引`的等值查询扫描, 最多只返回一行数据。`const`查询速度非常快, 因为它仅仅读取一次即可。 - -关于type列为`system`、`const`的情况,见下面的示例: -```sh -mysql> set session optimizer_switch='derived_merge=off'; -Query OK, 0 rows affected (0.00 sec) - -mysql> explain select * from (select * from tb_book where book_id = 5) as book; -+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------+ -| 1 | PRIMARY | | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | -| 2 | DERIVED | tb_book | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | -+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------+ -2 rows in set, 1 warning (0.00 sec) - -mysql> set session optimizer_switch='derived_merge=on'; -Query OK, 0 rows affected (0.00 sec) -``` - -(3) eq_ref:It is used when all parts of an index are used by the join and the index is a `PRIMARY KEY` or `UNIQUE NOT NULL index`. - -此类型通常出现在多表的join查询,表示对于前表的每一个结果,都只能匹配到后表的一行结果,并且查询的比较操作通常是`=`,查询效率较高。 - -```sh -mysql> select tb_hero.*, tb_book_hero.user_comment from tb_book_hero, tb_hero where tb_book_hero.book_id = 2 and tb_book_hero.hero_id = tb_hero.hero_id; -+---------+-----------+--------------+---------+--------------------------------------------------------------------------------------+ -| hero_id | hero_name | skill | book_id | user_comment | -+---------+-----------+--------------+---------+--------------------------------------------------------------------------------------+ -| 2 | 令狐冲 | 独孤九剑 | 2 | 令狐少侠留步! | -| 6 | 任我行 | 吸星大法 | 2 | 有人就有恩怨,有恩怨就有江湖,人心即是江湖,你如何退出! | -+---------+-----------+--------------+---------+--------------------------------------------------------------------------------------+ -2 rows in set (0.00 sec) - -mysql> explain select tb_hero.*, tb_book_hero.user_comment from tb_book_hero, tb_hero where tb_book_hero.book_id = 2 and tb_book_hero.hero_id = tb_hero.hero_id; -+----+-------------+--------------+------------+--------+---------------+---------+---------+-----------------------------------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+--------+---------------+---------+---------+-----------------------------------+------+----------+-------+ -| 1 | SIMPLE | tb_book_hero | NULL | ref | PRIMARY | PRIMARY | 4 | const | 2 | 100.00 | NULL | -| 1 | SIMPLE | tb_hero | NULL | eq_ref | PRIMARY | PRIMARY | 4 | explain_test.tb_book_hero.hero_id | 1 | 100.00 | NULL | -+----+-------------+--------------+------------+--------+---------------+---------+---------+-----------------------------------+------+----------+-------+ -2 rows in set, 1 warning (0.00 sec) - -mysql> select tb_hero.*, tb_book_hero.user_comment from tb_book_hero join tb_hero on tb_book_hero.book_id = 2 and tb_book_hero.hero_id = tb_hero.hero_id; -+---------+-----------+--------------+---------+--------------------------------------------------------------------------------------+ -| hero_id | hero_name | skill | book_id | user_comment | -+---------+-----------+--------------+---------+--------------------------------------------------------------------------------------+ -| 2 | 令狐冲 | 独孤九剑 | 2 | 令狐少侠留步! | -| 6 | 任我行 | 吸星大法 | 2 | 有人就有恩怨,有恩怨就有江湖,人心即是江湖,你如何退出! | -+---------+-----------+--------------+---------+--------------------------------------------------------------------------------------+ -2 rows in set (0.00 sec) - -mysql> explain select tb_hero.*, tb_book_hero.user_comment from tb_book_hero join tb_hero on tb_book_hero.book_id = 2 and tb_book_hero.hero_id = tb_hero.hero_id; -+----+-------------+--------------+------------+--------+---------------+---------+---------+-----------------------------------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+--------+---------------+---------+---------+-----------------------------------+------+----------+-------+ -| 1 | SIMPLE | tb_book_hero | NULL | ref | PRIMARY | PRIMARY | 4 | const | 2 | 100.00 | NULL | -| 1 | SIMPLE | tb_hero | NULL | eq_ref | PRIMARY | PRIMARY | 4 | explain_test.tb_book_hero.hero_id | 1 | 100.00 | NULL | -+----+-------------+--------------+------------+--------+---------------+---------+---------+-----------------------------------+------+----------+-------+ -2 rows in set, 1 warning (0.00 sec) -``` - -(4) ref: It is used if the join uses only a leftmost prefix of the key or if the key is not a PRIMARY KEY or UNIQUE index (in other words, if the join cannot select a single row based on the key value). - -相比`eq_ref`,不使用唯一索引,而是使用普通索引或者唯一性索引的最左前缀,可能会找到多个符合条件的行。 - -- 简单的`select`查询,`author`列上建有普通索引(非唯一索引) -```sh -mysql> explain select * from tb_book where author = '古龙'; -+----+-------------+---------+------------+------+---------------+------------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+---------------+------------+---------+-------+------+----------+-------+ -| 1 | SIMPLE | tb_book | NULL | ref | idx_author | idx_author | 131 | const | 2 | 100.00 | NULL | -+----+-------------+---------+------------+------+---------------+------------+---------+-------+------+----------+-------+ -1 row in set, 1 warning (0.00 sec) -``` - -- 关联表查询,`tb_book_hero`表使用了联合主键`PRIMARY KEY (book_id, hero_id)`,这里使用到了左边前缀`book_id`进行过滤。 -```sh -mysql> explain select * from tb_book_hero where book_id = 3; -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------+ -| 1 | SIMPLE | tb_book_hero | NULL | ref | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------+ -1 row in set, 1 warning (0.00 sec) -``` - -(5) range: It can be used when a key column is compared to a constant using any of the =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, LIKE, or IN() operators -扫描部分索引(范围扫描),对索引的扫描开始于某一点,返回匹配值域的行,常见于between、<、>、in等查询 -```sh -mysql> explain select * from tb_book where book_id > 3; -+----+-------------+---------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_book | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 2 | 100.00 | Using where | -+----+-------------+---------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` - -(6) index:the index tree is scanned, MySQL can use this type when the query uses only columns that are part of a single index. -表示全索引扫描(full index scan), 和ALL类型类似, 只不过ALL类型是全表扫描, 而index类型则仅仅扫描所有的索引, 而不扫描数据. -```sh -mysql> explain select book_name from tb_book; -+----+-------------+---------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_book | NULL | index | NULL | uk_book_name | 259 | NULL | 5 | 100.00 | Using index | -+----+-------------+---------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` -上面的例子中, 我们查询的`book_name`字段上恰好有索引, 因此我们直接从索引中获取数据就可以满足查询的需求了, 而不需要查询表中的数据。因此这样的情况下, type的值是index, 并且Extra的值大多是`Using index`。 - -(7) ALL: A full table scan is done -表示全表扫描, 这个类型的查询是性能最差的查询之一。通常来说, 我们的查询不应该出现ALL类型的查询, 因为这样的查询在数据量大的情况下, 严重降低数据库的性能。如果一个查询是ALL类型查询, 那么大多可以对相应的字段添加索引来避免。 -```sh -mysql> explain select * from tb_hero where hero_name = '令狐冲'; -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_hero | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` - -### possible_keys列 -表示MySQL在查询时, 能够使用到的索引。注意, 即使有些索引在possible_keys中出现, 但是并不表示此索引会真正地被MySQL使用到。MySQL在查询时具体使用了哪些索引, 由key字段决定。 - -### key列 -这一列显示mysql实际采用哪个索引来优化对该表的访问。如果没有使用索引,则该列是NULL。 - -### key_len列 -表示查询优化器使用了索引的字节数,这个字段可以评估联合索引是否完全被使用, 或只有最左部分字段被使用到。 -举例来说,`tb_hero`表的联合索引`idx_book_id_hero_name`由`book_id`和`hero_name`两个列组成,int类型占4字节,另外如果字段允许为NULL,需要1字节记录是否为NULL,通过结果中的key_len=5(`tb_hero`.`book_id`允许为NULL)可推断出查询使用了第一个列`book_id`列来执行索引查找;再拿`tb_book_hero`表联合主键`PRIMARY KEY (book_id, hero_id)`举例,通过key_len=4(`tb_book_hero`.`book_id`不允许为NULL)可推断出查询使用了第一个列`book_id`列来执行索引查找 -```sh -mysql> explain select * from tb_hero where book_id = 2; -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------+ -| 1 | SIMPLE | tb_hero | NULL | ref | idx_book_id_hero_name | idx_book_id_hero_name | 5 | const | 2 | 100.00 | NULL | -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------+ -1 row in set, 1 warning (0.00 sec) - -mysql> explain select * from tb_book_hero where book_id = 2; -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------+ -| 1 | SIMPLE | tb_book_hero | NULL | ref | PRIMARY | PRIMARY | 4 | const | 2 | 100.00 | NULL | -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------+ -1 row in set, 1 warning (0.01 sec) -``` - -`key_len`的计算规则如下: - -- 字符串: - - char(n): n字节长度 - - varchar(n): 如果是`utf8`编码, 则是`3n + 2`字节; 如果是`utf8mb4`编码, 则是`4n + 2`字节. - -- 数值类型: - - TINYINT: 1字节 - - SMALLINT: 2字节 - - MEDIUMINT: 3字节 - - INT: 4字节 - - BIGINT: 8字节 - -- 时间类型 - - DATE: 3字节 - - TIMESTAMP: 4字节 - - DATETIME: 8字节 - -- 字段属性: - - NULL属性占用一个字节 - - 如果一个字段是NOT NULL的, 则没有此属性 - -再看下面的计算: -`4 [book_id是int类型] + 1 [book_id允许为NULL] + (4 * 32 + 2) [hero_name是varchar32,且用的是utf8mb4编码] + 1 [hero_name允许为NULL] = 136` -```sh -mysql> explain select * from tb_hero where book_id = 2 and hero_name = '令狐冲'; -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------------+------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------------+------+----------+-------+ -| 1 | SIMPLE | tb_hero | NULL | ref | idx_book_id_hero_name | idx_book_id_hero_name | 136 | const,const | 1 | 100.00 | NULL | -+----+-------------+---------+------------+------+-----------------------+-----------------------+---------+-------------+------+----------+-------+ -1 row in set, 1 warning (0.00 sec) -``` - -### ref列 - -> The ref column shows which columns or constants are compared to the index named in the key column to select rows from the table. -显示的是哪个字段或常数与key一起被使用 - -### rows列 - -MySQL查询优化器根据统计信息, 估算SQL要查找到结果集需要扫描读取的数据行数,注意这个不是结果集里的行数。这个值非常直观显示SQL的效率好坏, 原则上rows越少越好。 - -### Extra列 - -这一列展示的是额外信息。常见的重要值如下: - -(1) Using index - -表示查询在索引树中就可查到所需数据, 不用扫描表数据文件 - -```sh -mysql> explain select hero_id from tb_book_hero where book_id = 2; -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------------+ -| 1 | SIMPLE | tb_book_hero | NULL | ref | PRIMARY | PRIMARY | 4 | const | 2 | 100.00 | Using index | -+----+-------------+--------------+------------+------+---------------+---------+---------+-------+------+----------+-------------+ -1 row in set, 1 warning (0.01 sec) - -mysql> explain select book_id from tb_book where author = '金庸'; -+----+-------------+---------+------------+------+---------------+------------+---------+-------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+---------------+------------+---------+-------+------+----------+-------------+ -| 1 | SIMPLE | tb_book | NULL | ref | idx_author | idx_author | 131 | const | 3 | 100.00 | Using index | -+----+-------------+---------+------------+------+---------------+------------+---------+-------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` - -(2) Using where -查询的列没有全部被索引覆盖 -```sh -mysql> explain select book_id, book_name from tb_book where author = '金庸'; -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_book | NULL | ALL | idx_author | NULL | NULL | NULL | 5 | 60.00 | Using where | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` - -(3) Using temporary - -查询有使用临时表,一般出现于排序、分组、多表join、distinct查询等等。 - -举例子如下:`tb_book`表对`book_name`字段建立了唯一性索引,这时候distinct查询Extra列为`Using index`; `tb_hero`表的`skill`字段上没有任何索引,这时候distinct查询Extra列为`Using temporary` - -```sh -mysql> select distinct book_name from tb_book; -+-----------------------+ -| book_name | -+-----------------------+ -| 倚天屠龙记 | -| 多情剑客无情剑 | -| 射雕英雄传 | -| 笑傲江湖 | -| 绝代双骄 | -+-----------------------+ -5 rows in set (0.00 sec) - -mysql> explain select distinct book_name from tb_book; -+----+-------------+---------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_book | NULL | index | uk_book_name | uk_book_name | 259 | NULL | 5 | 100.00 | Using index | -+----+-------------+---------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) - -mysql> select distinct skill from tb_hero; -+-----------------+ -| skill | -+-----------------+ -| 小李飞刀 | -| 独孤九剑 | -| 九阳神功 | -| 降龙十八掌 | -| 移花接玉 | -| 吸星大法 | -+-----------------+ -6 rows in set (0.00 sec) - -mysql> explain select distinct skill from tb_hero; -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+ -| 1 | SIMPLE | tb_hero | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 100.00 | Using temporary | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+ -1 row in set, 1 warning (0.00 sec) -``` - -(4) Using filesort - -表示MySQL不能通过索引顺序达到排序效果,需额外的排序操作,数据较小时在内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。 - -```sh -mysql> explain select book_id, hero_name from tb_hero order by hero_name; -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ -| 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_book_id_hero_name | 136 | NULL | 6 | 100.00 | Using index; Using filesort | -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ -1 row in set, 1 warning (0.00 sec) - -mysql> explain select book_id, hero_name from tb_hero order by book_id, hero_name; -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_book_id_hero_name | 136 | NULL | 6 | 100.00 | Using index | -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` -`tb_hero`表上有联合索引`INDEX idx_book_id_hero_name(book_id, hero_name) USING BTREE` -但是`order by hero_name`, 不能使用索引进行优化(下一篇博客会介绍联合索引的结构), 进而会产生`Using filesort` -如果将排序依据改为`order by book_id, hero_name`, 就不会出现`Using filesort`了。 - -(5) Select tables optimized away -比如下面的例子: -```sh -mysql> explain select min(book_id), max(book_id) from tb_book; -+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ -| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away | -+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+ -1 row in set, 1 warning (0.00 sec) -``` \ No newline at end of file diff --git a/MySQL/mysql-index-theory-and-best-practice.md b/MySQL/mysql-index-theory-and-best-practice.md deleted file mode 100644 index e9d77ab..0000000 --- a/MySQL/mysql-index-theory-and-best-practice.md +++ /dev/null @@ -1,479 +0,0 @@ -# 一文读懂MySQL的索引结构及查询优化 - -回顾前文: [一文学会MySQL的explain工具](https://www.cnblogs.com/itwild/p/13424113.html) - -(同时再次强调,这几篇关于MySQL的探究都是基于`5.7`版本,相关总结与结论`不一定适用`于其他版本) - -MySQL官方文档中(`https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html`)有这样一段描述: - -> The best way to improve the performance of SELECT operations is to create indexes on one or more of the columns that are tested in the query. But unnecessary indexes waste space and waste time for MySQL to determine which indexes to use. Indexes also add to the cost of inserts, updates, and deletes because each index must be updated. You must find the right balance to achieve fast queries using the optimal set of indexes. - -就是说提高查询性能最直接有效的方法就是建立索引,但是不必要的索引会浪费空间,同时也增加了额外的时间成本去判断应该走哪个索引,此外,索引还会增加插入、更新、删除数据的成本,因为做这些操作的同时还要去维护(更新)索引树。因此,应该学会使用最佳索引集来优化查询。 - -## 索引结构 - -参考: -1. 《MySQL索引背后的数据结构及算法原理》`http://blog.codinglabs.org/articles/theory-of-mysql-index.html` - -2. 《Mysql BTree和B+Tree详解》`https://www.cnblogs.com/Transkai/p/11595405.html` - -3. 《为什么MySQL使用B+树》`https://draveness.me/whys-the-design-mysql-b-plus-tree/` - -4. 《浅入浅出MySQL和InnoDB》`https://draveness.me/mysql-innodb/` - -5. 《漫画:什么是B树?》`https://mp.weixin.qq.com/s/rDCEFzoKHIjyHfI_bsz5Rw` - -### 什么是索引 - -在MySQL中,索引(`Index`)是帮助高效获取数据的数据结构。这种数据结构MySQL中最常用的就是B+树(`B+Tree`)。 - -> Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows. - -就好比给你一本书和一篇文章标题,如果没有目录,让你找此标题对应的文章,可能需要从第一页翻到最后一页;如果有目录大纲,你可能只需要在目录页寻找此标题,然后迅速定位文章。 - -这里我们可以把`书(book)`看成是MySQL中的`table`,把`文章(article)`看成是`table`中的一行记录,即`row`,`文章标题(title)`看成`row`中的一列`column`,`目录`自然就是对`title`列建立的索引`index`了,这样`根据文章标题从书中检索文章`就对应sql语句`select * from book where title = ?`,相应的,书中每增加一篇文章(即`insert into book (title, ...) values ('华山论剑', ...)`),都需要维护一下`目录`,这样才能从目录中找到新增的文章`华山论剑`,这一操作对应的是MySQL中每插入(`insert`)一条记录需要维护`title`列的索引树(`B+Tree`)。 - -### 为什么使用B+Tree - -首先需要澄清的一点是,MySQL跟B+树没有直接的关系,真正与B+树有关系的是MySQL的默认存储引擎`InnoDB`,MySQL中存储引擎的主要作用是`负责数据的存储和提取`,除了`InnoDB`之外,MySQL中也支持比如`MyISAM`等其他存储引擎(详情见`https://dev.mysql.com/doc/refman/5.7/en/storage-engine-setting.html`)作为表的底层存储引擎。 - -```sh -mysql> show engines; -+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ -| Engine | Support | Comment | Transactions | XA | Savepoints | -+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ -| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO | -| CSV | YES | CSV storage engine | NO | NO | NO | -| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO | -| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO | -| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES | -| MyISAM | YES | MyISAM storage engine | NO | NO | NO | -| ARCHIVE | YES | Archive storage engine | NO | NO | NO | -| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO | -| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL | -+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ -``` -提到索引,我们可能会立马想到下面几种数据结构来实现。 - -(1) 哈希表 -哈希虽然能够提供`O(1)`的单数据行的查询性能,但是对于`范围查询`和`排序`却无法很好支持,需全表扫描。 - -(2) 红黑树 -红黑树(`Red Black Tree`)是一种自平衡二叉查找树,在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。 - -一般来说,索引本身也很大,往往不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗远远高于内存,所以评价一个数据结构作为索引的优劣最重要的指标就是查找过程中磁盘I/O次数。换句话说,`索引的结构组织要尽量减少查找过程中磁盘I/O的次数。` - -在这里,磁盘I/O的次数取决于树的高度,所以,在数据量较大时,`红黑树会因树的高度较大而造成磁盘IO较多`,从而影响查询效率。 - -(3) B-Tree -B树中的B代表平衡(`Balance`),而不是二叉(`Binary`),B树是从平衡二叉树演化而来的。 - -为了降低树的高度(也就是减少磁盘I/O次数),把原来`瘦高`的树结构变得`矮胖`,B树会在`每个节点存储多个元素`(红黑树每个节点只会存储一个元素),并且节点中的元素从左到右递增排列。如下图所示: - -![B-Tree结构图](https://img2020.cnblogs.com/blog/1546632/202008/1546632-20200830195348368-1304078258.png) - -`B-Tree`在查询的时候比较次数其实不比二叉查找树少,但在内存中的大小比较、二分查找的耗时相比磁盘IO耗时几乎可以忽略。 `B-Tree大大降低了树的高度`,所以也就极大地提升了查找性能。 - -(4) B+Tree -`B+Tree`是在`B-Tree`基础上进一步优化,使其更适合实现存储索引结构。InnoDB存储引擎就是用`B+Tree`实现其索引结构。 - -`B-Tree`结构图中可以看到每个节点中不仅包含数据的`key`值,还有`data`值。而每一个节点的存储空间是有限的,如果`data`值较大时将会导致每个节点能存储的`key`的数量很小,这样会导致B-Tree的高度变大,增加了查询时的磁盘I/O次数,进而影响查询性能。在`B+Tree`中,所有`data`值都是按照键值大小顺序存放在同一层的叶子节点上,而`非叶子节点上只存储key值信息`,这样可以增大每个非叶子节点存储的`key`值数量,降低B+Tree的高度,提高效率。 - -![B+Tree结构图](https://img2020.cnblogs.com/blog/1546632/202008/1546632-20200830201413134-394816073.png) - -**这里补充一点相关知识** 在计算机中,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的`局部性原理`: - -> 当一个数据被用到时,其附近的数据也通常会马上被使用。 - -由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。预读的长度一般为页(`page`)的整数倍。 - -`页`是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(许多操作系统的页默认大小为`4KB`),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时操作系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。(如下命令可以查看操作系统的默认页大小) -```sh -$ getconf PAGE_SIZE -4096 -``` -数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为操作系统的页大小的整数倍,这样每个节点只需要一次I/O就可以完全载入。 - -`InnoDB`存储引擎中也有页(`Page`)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB。 -```sh -mysql> show variables like 'innodb_page_size'; -+------------------+-------+ -| Variable_name | Value | -+------------------+-------+ -| innodb_page_size | 16384 | -+------------------+-------+ -1 row in set (0.01 sec) -``` -一般表的主键类型为`INT`(占4个字节)或`BIGINT`(占8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储`16KB/(8B+8B)=1K`个键值(因为是估值,为方便计算,这里的K取值为`10^3`)。也就是说一个深度为3的B+Tree索引可以维护`10^3 * 10^3 * 10^3 = 10亿`条记录。 - -`B+Tree`的高度一般都在2到4层。mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1到3次磁盘I/O操作。 - -随机I/O对于MySQL的查询性能影响会非常大,而顺序读取磁盘中的数据会很快,由此我们也应该尽量减少随机I/O的次数,这样才能提高性能。在`B-Tree`中由于所有的节点都可能包含目标数据,我们总是要从根节点向下遍历子树查找满足条件的数据行,这会带来大量的随机I/O,而`B+Tree`所有的数据行都存储在叶子节点中,而这些叶子节点通过`双向链表`依次按顺序连接,当我们在B+树遍历数据(比如说`范围查询`)时可以直接在多个叶子节点之间进行跳转,保证`顺序`、`倒序`遍历的性能。 - -另外,对以上提到的数据结构不熟悉的朋友,这里推荐一个在线数据结构可视化演示工具,有助于快速理解这些数据结构的机制:`https://www.cs.usfca.edu/~galles/visualization/Algorithms.html` -### 主键索引 - -上面也有提及,在MySQL中,索引属于存储引擎级别的概念。不同存储引擎对索引的实现方式是不同的,这里主要看下`MyISAM`和`InnoDB`两种存储引擎的索引实现方式。 - -#### MyISAM索引实现 - -`MyISAM`引擎使用`B+Tree`作为索引结构时叶子节点的`data`域存放的是数据记录的地址。如下图所示: - -![MyISAM主键索引原理图](https://img2020.cnblogs.com/blog/1546632/202009/1546632-20200919221036646-2109444544.png) - -由上图可以看出:MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,因此MyISAM的索引方式也叫做`非聚集`的,之所以这么称呼是为了与InnoDB的`聚集索引`区分。 - -#### InnoDB索引实现 - -`InnoDB`的`主键索引`也使用`B+Tree`作为索引结构时的实现方式却与MyISAM截然不同。`InnoDB的数据文件本身就是索引文件`。在InnoDB中,表数据文件本身就是按`B+Tree`组织的一个索引结构,这棵树的叶子节点`data`域保存了完整的数据记录,这个索引的`key`是数据表的主键,因此InnoDB表数据文件本身就是主索引。 - -![InnoDB主键索引原理图](https://img2020.cnblogs.com/blog/1546632/202009/1546632-20200919222451055-995072120.png) - -`InnoDB`存储引擎中的主键索引(`primary key`)又叫做聚集索引(`clustered index`)。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。(详情见官方文档:`https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.html`) - -聚集索引这种实现方式使得按主键搜索十分高效,直接能查出整行数据。 - -在InnoDB中,用非单调递增的字段作为主键不是个好主意,因为InnoDB数据文件本身是一棵`B+Tree`,非单增的主键会造成在插入新记录时数据文件为了`维持B+Tree的特性`而频繁的分裂调整,十分低效,因而使用`递增`字段作为主键则是一个很好的选择。 - -### 非主键索引 - -#### MyISAM索引实现 - -MyISAM中,主键索引和非主键索引(`Secondary key`,也有人叫做`辅助索引`)在结构上没有任何区别,只是主键索引要求key是唯一的,而辅助索引的key可以重复。这里不再多加叙述。 - -#### InnoDB索引实现 - -InnoDB的非主键索引`data`域存储相应记录`主键的值`。换句话说,InnoDB的所有非主键索引都引用主键的值作为data域。如下图所示: - -![InnoDB非主键索引原理图](https://img2020.cnblogs.com/blog/1546632/202009/1546632-20200919225813936-1490118290.png) - -由上图可知:使用非主键索引搜索时需要检索两遍索引,首先检索非主键索引获得主键(`primary key`),然后用主键到`主键索引树`中检索获得完整记录。 - -那么为什么非主键索引结构叶子节点存储的是主键值,而不像主键索引那样直接存储完整的一行数据,这样就能避免回表二次检索?显然,这样做一方面节省了大量的存储空间,另一方面多份冗余数据,更新数据的效率肯定低下,另外保证数据的一致性是个麻烦事。 - -到了这里,也很容易明白为什么`不建议使用过长的字段作为主键`,因为所有的非主键索引都引用主键值,过长的主键值会让非主键索引变得过大。 - -### 联合索引 - -官方文档:`https://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html` - -比如`INDEX idx_book_id_hero_name (book_id, hero_name) USING BTREE`,即对`book_id, hero_name`两列建立了一个联合索引。 - -> A multiple-column index can be considered a sorted array, the rows of which contain values that are created by concatenating the values of the indexed columns. - -联合索引是多列按照次序一列一列比较大小,拿`idx_book_id_hero_name`这个联合索引来说,先比较`book_id`,book_id小的排在左边,book_id大的排在右边,book_id相同时再比较`hero_name`。如下图所示: - -![InnoDB联合索引原理图](https://img2020.cnblogs.com/blog/1546632/202009/1546632-20200920111026527-1672463564.png) - -了解了联合索引的结构,就能引入`最左前缀法则`: - -> If the table has a multiple-column index, any leftmost prefix of the index can be used by the optimizer to look up rows. For example, if you have a three-column index on (col1, col2, col3), you have indexed search capabilities on (col1), (col1, col2), and (col1, col2, col3). - -就是说联合索引中的多列是按照列的次序排列的,如果查询的时候不能满足列的次序,比如说where条件中缺少`col1 = ?`,直接就是`col2 = ? and col3 = ?`,那么就走不了联合索引,从上面联合索引的结构图应该能明显看出,只有`col2`列无法通过索引树检索符合条件的数据。 - -根据最左前缀法则,我们知道对`INDEX idx_book_id_hero_name (book_id, hero_name)`来说,`where book_id = ? and hero_name = ?`的查询来说,肯定可以走索引,但是如果是`where hero_name = ? and book_id = ?`呢,表面上看起来不符合最左前缀法则啊,但MySQL优化器会根据已有的索引,调整查询条件中这两列的顺序,让它符合最左前缀法则,走索引,这里也就回答了上篇《一文学会MySQL的explain工具》中为什么用`show warnings`命令查看时,`where`中的两个过滤条件`hero_name`、`book_id`先后顺序被调换了。 - -至于对联合索引中的列进行范围查询等各种情况,都可以先想联合索引的结构是如何创建出来的,然后看过滤条件是否满足最左前缀法则。比如说范围查询时,范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。同时,索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。 - -## 优化建议 - -### 主键的选择 - -在使用`InnoDB`存储引擎时,如果没有特别的需要,尽量使用一个与业务无关的`递增字段`作为主键,主键字段不宜过长。原因上面在讲索引结构时已提过。比如说常用雪花算法生成64bit大小的整数(占8个字节,用`BIGINT`类型)作为主键就是一个不错的选择。 - -### 索引的选择 - -(1) 表记录比较少的时候,比如说只有几百条记录的表,对一些列建立索引的意义可能并不大,所以表记录不大时酌情考虑索引。但是业务上具有`唯一特性`的字段,即使是多个字段的组合,也建议使用唯一索引(`UNIQUE KEY`)。 - -(2) 当索引的选择性非常低时,索引的意义可能也不大。所谓索引的选择性(`Selectivity`),是指不重复的索引值(也叫基数`Cardinality`)与表记录数的比值,即`count(distinct 列名)/count(*)`,常见的场景就是有一列`status`标识数据行的状态,可能`status`非0即1,总数据100万行有50万行`status`为0,50万行`status`为1,那么是否有必要对这一列单独建立索引呢? - -> An index is best used when you need to select a small number of rows in comparison to the total rows. - -这句话我摘自stackoverflow上《MySQL: low selectivity columns = how to index?》下面一个人的回答。(详情见:`https://stackoverflow.com/questions/2386852/mysql-low-cardinality-selectivity-columns-how-to-index`) - -对于上面说的`status`非0即1,而且这两种情况分布比较均匀的情况,索引可能并没有实际意义,实际查询时,MySQL优化器在计算全表扫描和索引树扫描代价后,可能会放弃走索引,因为先从`status`索引树中遍历出来主键值,再去主键索引树中查最终数据,代价可能比全表扫描还高。 - -但是如果对于`status`为1的数据只有1万行,其他99万行数据`status`为0的情况呢,你怎么看?欢迎有兴趣的朋友在文章下面留言讨论! - -**补充**: 关于MySQL如何选择走不走索引或者选择走哪个最佳索引,可以使用MySQL自带的trace工具一探究竟。具体使用见下面的官方文档。 -`https://dev.mysql.com/doc/internals/en/optimizer-tracing.html` -`https://dev.mysql.com/doc/refman/5.7/en/information-schema-optimizer-trace-table.html` - -使用方法: -```sh -mysql> set session optimizer_trace="enabled=on",end_markers_in_json=on; -mysql> select * from tb_hero where hero_id = 1; -mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE; -``` -`注意`:开启trace工具会影响MySQL性能,所以只能临时分析sql使用,用完之后应当立即关闭 -```sh -mysql> set session optimizer_trace="enabled=off"; -``` - -(3) 在`varchar`类型字段上建立索引时,建议指定`索引长度`,有些时候可能没必要对全字段建立索引,根据实际文本区分度决定索引长度即可【说明:索引的长度与区分度是一对矛盾体,`一般对字符串类型数据,长度为20的索引,区分度会高达90%以上`,可以使用`count(distinct left(列名, 索引长度))/count(*)`来确定区分度】。 - -这种指定索引长度的索引叫做`前缀索引`(详情见`https://dev.mysql.com/doc/refman/5.7/en/column-indexes.html#column-indexes-prefix`)。 -> With col_name(N) syntax in an index specification for a string column, you can create an index that uses only the first N characters of the column. Indexing only a prefix of column values in this way can make the index file much smaller. When you index a BLOB or TEXT column, you must specify a prefix length for the index. - -前缀索引语法如下: -```sh -mysql> alter table tb_hero add index idx_hero_name_skill2 (hero_name, skill(2)); -``` -前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于`group by`和`order by`操作,也不能用于`covering index`(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。 - -(4) 当查询语句的`where`条件或`group by`、`order by`含多列时,可根据实际情况优先考虑联合索引(`multiple-column index`),这样可以减少单列索引(`single-column index)`的个数,有助于高效查询。 - -> If you specify the columns in the right order in the index definition, a single composite index can speed up several kinds of queries on the same table. - -建立联合索引时要特别注意`column`的次序,应结合上面提到的`最左前缀法则`以及实际的过滤、分组、排序需求。`区分度最高的建议放最左边`。 - -说明: -- `order by`的字段可以作为联合索引的一部分,并且放在最后,避免出现`file_sort`的情况,影响查询性能。正例:`where a=? and b=? order by c`会走索引`idx_a_b_c`,但是`WHERE a>10 order by b`却无法完全使用上索引`idx_a_b`,只会使用上联合索引的第一列a - -- 存在非等号和等号混合时,在建联合索引时,应该把等号条件的列前置。如:`where c>? and d=?`那么即使c的区分度更高,也应该把d放在索引的最前列,即索引`idx_d_c` - -- 如果`where a=? and b=?`,如果a列的几乎接近于唯一值,那么只需要建立单列索引`idx_a`即可 - -### order by与group by - -尽量在索引列上完成分组、排序,遵循索引`最左前缀法则`,如果`order by`的条件不在索引列上,就会产生`Using filesort`,降低查询性能。 - -### 分页查询 - -MySQL分页查询大多数写法可能如下: -```sh -mysql> select * from tb_hero limit offset,N; -``` -MySQL并不是跳过`offset`行,而是取`offset+N`行,然后返回放弃前`offset`行,返回`N`行,那当`offset`特别大的时候,效率就非常的低下。 - -可以对超过特定阈值的页数进行SQL改写如下: - -先快速定位需要获取的id段,然后再关联 -```sh -mysql> select a.* from tb_hero a, (select hero_id from tb_hero where 条件 limit 100000,20 ) b where a.hero_id = b.hero_id; -``` -或者这种写法 -```sh -mysql> select a.* from tb_hero a inner join (select hero_id from tb_hero where 条件 limit 100000,20) b on a.hero_id = b.hero_id; -``` - -### 多表join - -(1) 需要join的字段,数据类型必须绝对一致; -(2) 多表join时,保证被关联的字段有索引 - -### 覆盖索引 - -利用覆盖索引(`covering index`)来进行查询操作,避免回表,从而增加磁盘I/O。换句话说就是,尽可能避免`select *`语句,只选择必要的列,去除无用的列。 - -> An index that includes all the columns retrieved by a query. Instead of using the index values as pointers to find the full table rows, the query returns values from the index structure, saving disk I/O. InnoDB can apply this optimization technique to more indexes than MyISAM can, because InnoDB secondary indexes also include the primary key columns. InnoDB cannot apply this technique for queries against tables modified by a transaction, until that transaction ends. - -> Any column index or composite index could act as a covering index, given the right query. Design your indexes and queries to take advantage of this optimization technique wherever possible. - -当索引本身包含查询所需全部列时,无需回表查询完整的行记录。对于`InnoDB`来说,非主键索引中包含了`所有的索引列`以及`主键值`,查询的时候尽量用这种特性避免回表操作,数据量很大时,查询性能提升很明显。 - -### in和exsits - -原则:`小表驱动大表`,即小的数据集驱动大的数据集 - -(1) 当A表的数据集大于B表的数据集时,`in`优于`exists` -```sh -mysql> select * from A where id in (select id from B) -``` - -(2) 当A表的数据集小于B表的数据集时,`exists`优于`in` -```sh -mysql> select * from A where exists (select 1 from B where B.id = A.id) -``` - -### like - -索引文件具有`B+Tree`最左前缀匹配特性,如果左边的值未确定,那么无法使用索引,所以应尽量避免左模糊(即`%xxx`)或者全模糊(即`%xxx%`)。 - -```sh -mysql> select * from tb_hero where hero_name like '%无%'; -+---------+-----------+--------------+---------+ -| hero_id | hero_name | skill | book_id | -+---------+-----------+--------------+---------+ -| 3 | 张无忌 | 九阳神功 | 3 | -| 5 | 花无缺 | 移花接玉 | 5 | -+---------+-----------+--------------+---------+ -2 rows in set (0.00 sec) - -mysql> explain select * from tb_hero where hero_name like '%无%'; -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_hero | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where | -+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` -可以看出全模糊查询时全表扫了,这个时候使用`覆盖索引`的特性,只选择索引字段可以有所优化。如下: -```sh -mysql> explain select book_id, hero_name from tb_hero where hero_name like '%无%'; -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+ -| 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_book_id_hero_name | 136 | NULL | 6 | 16.67 | Using where; Using index | -+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+ -1 row in set, 1 warning (0.00 sec) -``` - -### count(*) - -阿里巴巴Java开发手册中有这样的规约: -> 不要使用`count(列名)`或`count(常量)`来替代`count(*)`,`count(*)`是SQL92定义的标准统计行数的语法,跟数据库无关,跟`NULL`和`非NULL`无关【说明:`count(*)`会统计值为`NULL`的行,而`count(列名)`不会统计此列为`NULL`值的行】。 -`count(distinct col)`计算该列除`NULL`之外的不重复行数,注意`count(distinct col1, col2)`如果其中一列全为`NULL`,那么即使另一列有不同的值,也返回为0 - -截取一段官方文档对`count`的描述(具体见:`https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_count`) - -> COUNT(expr): Returns a count of the number of non-NULL values of expr in the rows.The result is a BIGINT value.If there are no matching rows, COUNT(expr) returns 0. - -> COUNT(*) is somewhat different in that it returns a count of the number of rows, whether or not they contain NULL values. - -> Prior to MySQL 5.7.18, InnoDB processes SELECT `COUNT(*)` statements by scanning the clustered index. As of MySQL 5.7.18, InnoDB processes SELECT COUNT(*) statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present, the clustered index is scanned. - -可见`5.7.18`之前,MySQL处理`count(*)`会扫描主键索引,`5.7.18`之后从非主键索引中选择较小的合适的索引扫描。可以用`explain`看下执行计划。 -```sh -mysql> select version(); -+-----------+ -| version() | -+-----------+ -| 5.7.18 | -+-----------+ -1 row in set (0.00 sec) - -mysql> explain select count(*) from tb_hero; -+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_skill | 15 | NULL | 6 | 100.00 | Using index | -+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) - -mysql> explain select count(1) from tb_hero; -+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ -| 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_skill | 15 | NULL | 6 | 100.00 | Using index | -+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ -1 row in set, 1 warning (0.00 sec) -``` -有人纠结`count(*)`、`count(1)`到底哪种写法更高效,从上面的执行计划来看都一样,如果你还不放心的话,官方文档中也明确指明了`InnoDB`对`count(*)`、`count(1)`的处理完全一致。 -> InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference. - -### 其他 - -索引列上做任何操作(`表达式`、`函数计算`、`类型转换`等)时无法使用索引会导致全表扫描 - -## 实战 - -前几周测试同事对公司的某产品进行压测,某单表写入了近2亿条数据,过程中发现配的报表有几个数据查询时间太长,所以重点看了几个慢查询SQL。避免敏感信息,这里对其提取简化做个记录。 -```sh -mysql> select count(*) from tb_alert; -+-----------+ -| count(*) | -+-----------+ -| 198101877 | -+-----------+ -``` - -### 表join慢 - -表join后,取前10条数据就花了15秒,看了下SQL执行计划,如下: - -```sh -mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 10; -10 rows in set (15.46 sec) - -mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 10; -+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+ -| 1 | SIMPLE | tb_alert | NULL | ALL | NULL | NULL | NULL | NULL | 190097118 | 100.00 | NULL | -| 1 | SIMPLE | tb_situation_alert | NULL | ALL | NULL | NULL | NULL | NULL | 8026988 | 100.00 | Using where; Using join buffer (Block Nested Loop) | -+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+ -2 rows in set, 1 warning (0.00 sec) -``` - -可以看出join的时候没有用上索引,`tb_situation_alert`表上`联合主键`是这样的`PRIMARY KEY (situation_id, alert_id)`,参与表join字段是`alert_id`,原来是不符合联合索引的最左前缀法则,仅从这条sql看,解决方案有两种,一种是对`tb_situation_alert`表上的`alert_id`单独建立索引,另外一种是调换联合主键的列的次序,改为`PRIMARY KEY (alert_id, situation_id)`。当然不能因为多配一张报表,就改其他产线的表的主键索引,这并不合理。在这里,应该对`alert_id`列单独建立索引。 - -```sh -mysql> create index idx_alert_id on tb_situation_alert (alert_id); - -mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 100; -100 rows in set (0.01 sec) - -mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 100; -+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+ -| 1 | SIMPLE | tb_alert | NULL | ALL | NULL | NULL | NULL | NULL | 190097118 | 100.00 | NULL | -| 1 | SIMPLE | tb_situation_alert | NULL | ref | idx_alert_id | idx_alert_id | 8 | tb_alert.alert_id | 2 | 100.00 | NULL | -+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+ -2 rows in set, 1 warning (0.00 sec) -``` -优化后,执行计划可以看出join的时候走了索引,查询前100条0.01秒,和之前的取前10条数据就花了15秒天壤之别。 - -### 分页查询慢 - -从第10000000条数据往后翻页时,25秒才能出结果,这里就能使用上面的分页查询优化技巧了。上面讲优化建议时,没看执行计划,这里正好看一下。 - -```sh -mysql> select * from tb_alert limit 10000000, 10; -10 rows in set (25.23 sec) - -mysql> explain select * from tb_alert limit 10000000, 10; -+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+ -| 1 | SIMPLE | tb_alert | NULL | ALL | NULL | NULL | NULL | NULL | 190097118 | 100.00 | NULL | -+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+ -1 row in set, 1 warning (0.00 sec) -``` -再看下使用上分页查询优化技巧的sql的执行计划 -```sh -mysql> select * from tb_alert a inner join (select alert_id from tb_alert limit 10000000, 10) b on a.alert_id = b.alert_id; -10 rows in set (2.29 sec) - -mysql> explain select * from tb_alert a inner join (select alert_id from tb_alert a2 limit 10000000, 10) b on a.alert_id = b.alert_id; -+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+ -| 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 10000010 | 100.00 | NULL | -| 1 | PRIMARY | a | NULL | eq_ref | PRIMARY | PRIMARY | 8 | b.alert_id | 1 | 100.00 | NULL | -| 2 | DERIVED | a2 | NULL | index | NULL | idx_processed | 5 | NULL | 190097118 | 100.00 | Using index | -+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+ -3 rows in set, 1 warning (0.00 sec) -``` - -### 分组聚合慢 - -分析SQL后,发现根本上并非分组聚合慢,而是扫描联合索引后,回表导致性能低下,去除不必要的字段,使用覆盖索引。 - -这里避免敏感信息,只演示分组聚合前的简化SQL,主要问题也是在这。 -表上有联合索引`KEY idx_alert_start_host_template_id ( alert_start, alert_host, template_id)`,优化前的sql为 -```sh -mysql> select alert_start, alert_host, template_id, alert_service from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000; -10000 rows in set (1 min 5.22 sec) -``` -使用覆盖索引,去掉`alert_service`列,就能避免回表,查询时间从1min多变为0.03秒,如下: -```sh -mysql> select alert_start, alert_host, template_id from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000; -10000 rows in set (0.03 sec) - -mysql> explain select alert_start, alert_host, template_id from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000; -+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+ -| 1 | SIMPLE | tb_alert | NULL | range | idx_alert_start_host_template_id | idx_alert_start_host_template_id | 9 | NULL | 95048559 | 100.00 | Using where; Using index | -+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+ -1 row in set, 1 warning (0.01 sec) -``` - -## 总结 - -任何不考虑应用场景的设计都不是最好的设计,就比如说表结构的设计、索引的创建,都应该权衡数据量大小、查询需求、数据更新频率等。 -另外正如`《阿里巴巴java开发手册》`中提到的`索引规约`(详情见:[《Java开发手册》之"异常处理、MySQL 数据库"](https://www.cnblogs.com/itwild/p/12353164.html)): `创建索引时避免有如下极端误解:` -> 1)宁滥勿缺。认为一个查询就需要建一个索引 -> 2)宁缺勿滥。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度 \ No newline at end of file diff --git a/MySQL/mysql-transaction-innodb-mvcc.md b/MySQL/mysql-transaction-innodb-mvcc.md deleted file mode 100644 index b161807..0000000 --- a/MySQL/mysql-transaction-innodb-mvcc.md +++ /dev/null @@ -1,667 +0,0 @@ -# 一文读懂MySQL的事务隔离级别及MVCC机制 - -回顾前文: -[一文学会MySQL的explain工具](https://www.cnblogs.com/itwild/p/13424113.html) - -[一文读懂MySQL的索引结构及查询优化](https://www.cnblogs.com/itwild/p/13703259.html) - -(同时再次强调,这几篇关于MySQL的探究都是基于`5.7`版本,相关总结与结论`不一定适用`于其他版本) - -就软件开发而言,既要保证数据读写的`效率`,还要保证`并发读写`数据的`可靠性`、`正确性`。因此,除了要对MySQL的索引结构及查询优化有所了解外,还需要对MySQL的事务隔离级别及MVCC机制有所认知。 - -MySQL官方文档中的词汇表(`https://dev.mysql.com/doc/refman/5.7/en/glossary.html`)有助于我们对相关概念、理论的理解。下文中我会从概念表中摘录部分原文描述,以加深对原理机制的理解。 - -## 事务隔离级别 - -### 事务是什么 - -> Transactions are atomic units of work that can be committed or rolled back. When a transaction makes multiple changes to the database, either all the changes succeed when the transaction is committed, or all the changes are undone when the transaction is rolled back. - -事务是由一组SQL语句组成的原子操作单元,其对数据的变更,要么全都执行成功(`Committed`),要么全都不执行(`Rollback`)。 - -![事务的示意图](https://img2020.cnblogs.com/blog/1546632/202010/1546632-20201003090232837-258120887.png) - -> Database transactions, as implemented by InnoDB, have properties that are collectively known by the acronym ACID, for atomicity, consistency, isolation, and durability. - -`InnoDB`实现的数据库事务具有常说的`ACID`属性,即原子性(`atomicity`),一致性(`consistency`)、隔离性(`isolation`)和持久性(`durability`)。 - -- `原子性`:事务被视为不可分割的最小单元,所有操作要么全部执行成功,要么失败回滚(即还原到事务开始前的状态,就像这个事务从来没有执行过一样) -- `一致性`:在成功提交或失败回滚之后以及正在进行的事务期间,数据库始终保持一致的状态。如果正在多个表之间更新相关数据,那么查询将看到所有旧值或所有新值,而不会一部分是新值,一部分是旧值 -- `隔离性`:事务处理过程中的中间状态应该对外部不可见,换句话说,事务在进行过程中是隔离的,事务之间不能互相干扰,不能访问到彼此未提交的数据。这种隔离可通过锁机制实现。有经验的用户可以根据实际的业务场景,通过调整事务隔离级别,以提高并发能力 -- `持久性`:一旦事务提交,其所做的修改将会永远保存到数据库中。即使系统发生故障,事务执行的结果也不能丢失 - -> In InnoDB, all user activity occurs inside a transaction. If autocommit mode is enabled, each SQL statement forms a single transaction on its own. By default, MySQL starts the session for each new connection with autocommit enabled, so MySQL does a commit after each SQL statement if that statement did not return an error. If a statement returns an error, the commit or rollback behavior depends on the error - -MySQL默认采用自动提交(`autocommit`)模式。也就是说,如果不显式使用`START TRANSACTION`或`BEGIN`语句来开启一个事务,那么每个SQL语句都会被当做一个事务自动提交。 - -> A session that has autocommit enabled can perform a multiple-statement transaction by starting it with an explicit START TRANSACTION or BEGIN statement and ending it with a COMMIT or ROLLBACK statement. - -多个SQL语句开启一个事务也很简单,以`START TRANSACTION`或者`BEGIN`语句开头,以`COMMIT`或`ROLLBACK`语句结尾。 - -> If autocommit mode is disabled within a session with SET autocommit = 0, the session always has a transaction open. A COMMIT or ROLLBACK statement ends the current transaction and a new one starts. - -使用`SET autocommit = 0`可手动关闭当前`session`自动提交模式。 - -### 并发事务的问题 - -#### 引出事务隔离级别 - -相关文档:`https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html` - -> Isolation is the I in the acronym ACID; the isolation level is the setting that fine-tunes the balance between performance and reliability, consistency, and reproducibility of results when multiple transactions are making changes and performing queries at the same time. - -也就是说当多个并发请求访问MySQL,其中有对数据的增删改请求时,考虑到并发性,又为了避免`脏读`、`不可重复读`、`幻读`等问题,就需要对事务之间的读写进行隔离,至于隔离到啥程度需要看具体的业务场景,这时就要引出事务的隔离级别了。 - -> InnoDB offers all four transaction isolation levels described by the SQL:1992 standard: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE. The default isolation level for InnoDB is REPEATABLE READ. - -`InnoDB`存储引擎实现了SQL标准中描述的4个事务隔离级别:读未提交(`READ UNCOMMITTED`)、读已提交(`READ COMMITTED`)、可重复读(`REPEATABLE READ`)、可串行化(`SERIALIZABLE`)。`InnoDB`默认隔离级别是可重复读(`REPEATABLE READ`)。 - -#### 设置事务隔离级别 - -既然可以调整隔离级别,那么如何设置事务隔离级别呢?详情见官方文档:`https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html` - -MySQL`5.7.18`版本演示如下: -```sh -mysql> select version(); -+-----------+ -| version() | -+-----------+ -| 5.7.18 | -+-----------+ -1 row in set (0.00 sec) - -mysql> set global transaction isolation level REPEATABLE READ; -Query OK, 0 rows affected (0.00 sec) - -mysql> set session transaction isolation level READ COMMITTED; -Query OK, 0 rows affected (0.00 sec) - -mysql> select @@global.tx_isolation, @@session.tx_isolation, @@tx_isolation; -+-----------------------+------------------------+----------------+ -| @@global.tx_isolation | @@session.tx_isolation | @@tx_isolation | -+-----------------------+------------------------+----------------+ -| REPEATABLE-READ | READ-COMMITTED | READ-COMMITTED | -+-----------------------+------------------------+----------------+ -1 row in set (0.00 sec) -``` - -MySQL`8.0.21`版本演示如下: -```sh -mysql> select version(); -+-----------+ -| version() | -+-----------+ -| 8.0.21 | -+-----------+ -1 row in set (0.01 sec) - -mysql> set global transaction isolation level REPEATABLE READ; -Query OK, 0 rows affected (0.00 sec) - -mysql> set session transaction isolation level READ COMMITTED; -Query OK, 0 rows affected (0.00 sec) - -mysql> select @@global.transaction_isolation, @@session.transaction_isolation, @@transaction_isolation; -+--------------------------------+---------------------------------+-------------------------+ -| @@global.transaction_isolation | @@session.transaction_isolation | @@transaction_isolation | -+--------------------------------+---------------------------------+-------------------------+ -| REPEATABLE-READ | READ-COMMITTED | READ-COMMITTED | -+--------------------------------+---------------------------------+-------------------------+ -1 row in set (0.00 sec) -``` - -**注意**: - -> transaction_isolation was added in MySQL 5.7.20 as a synonym for tx_isolation, which is now deprecated and is removed in MySQL 8.0. Applications should be adjusted to use transaction_isolation in preference to tx_isolation. - -> Prior to MySQL 5.7.20, use tx_isolation and tx_read_only rather than transaction_isolation and transaction_read_only. - -如果使用系统变量(`system variables`)来查看或者设置事务隔离级别,需要注意MySQL的版本。在MySQL`5.7.20`之前,应使用`tx_isolation`;在MySQL`5.7.20`之后,应使用`transaction_isolation`。 - -> You can set transaction characteristics globally, for the current session, or for the next transaction only. - -事务的隔离级别范围(`Transaction Characteristic Scope`)可以精确到全局(`global`)、当前会话(`session`)、甚至是仅针对下一个事务生效(`the next transaction only`)。 - -- 含`global`关键词时,事务隔离级别的设置应用于所有后续`session`,已存在的`session`不受影响 -- 含`session`关键词时,事务隔离级别的设置应用于在当前`session`中执行的所有后续事务,不会影响当前正在进行的事务 -- 不含`global`以及`session`关键词时,事务隔离级别的设置仅应用于在当前`session`中执行的下一个事务 - -#### 数据准备 - -为了演示`脏读`、`不可重复读`、`幻读`等问题,准备了一些初始化数据如下: - -```sql --- ---------------------------- --- create database --- ---------------------------- -create database `transaction_test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- switch database -use `transaction_test`; - --- ---------------------------- --- table structure for `tb_book` --- ---------------------------- -CREATE TABLE `tb_book` ( - `book_id` int(11) NOT NULL, - `book_name` varchar(64) DEFAULT NULL, - `author` varchar(32) DEFAULT NULL, - PRIMARY KEY (`book_id`), - UNIQUE KEY `uk_book_name` (`book_name`) USING BTREE -) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -BEGIN; -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (1, '多情剑客无情剑', '古龙'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (2, '笑傲江湖', '金庸'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (3, '倚天屠龙记', '金庸'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (4, '射雕英雄传', '金庸'); -INSERT INTO `tb_book`(`book_id`, `book_name`, `author`) VALUES (5, '绝代双骄', '古龙'); -COMMIT; -``` - -#### 脏读(read uncommitted) - -**事务A读到了事务B已经修改但尚未提交的数据** - -操作: -1. `session A`事务隔离级别设置为`read uncommitted`并开启事务,首次查询`book_id`为1的记录; -2. 然后`session B`开启事务,并修改`book_id`为1的记录,不提交事务,在`session A`中再次查询`book_id`为1的记录; -3. 最后让`session B`中的事务回滚,再在`session A`中查询`book_id`为1的记录。 - -session A: - -```sql -mysql> set session transaction isolation level read uncommitted; -Query OK, 0 rows affected (0.00 sec) - -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> select * from tb_book where book_id = 1; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情剑客无情剑 | 古龙 | -+---------+-----------------------+--------+ -1 row in set (0.00 sec) - -mysql> select * from tb_book where book_id = 1; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -+---------+-----------------------+--------+ -1 row in set (0.00 sec) - -mysql> select * from tb_book where book_id = 1; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情剑客无情剑 | 古龙 | -+---------+-----------------------+--------+ -1 row in set (0.00 sec) - -mysql> commit; -Query OK, 0 rows affected (0.00 sec) -``` - -session B: - -```sql -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> update tb_book set book_name = '多情刀客无情刀' where book_id = 1; -Query OK, 1 row affected (0.00 sec) -Rows matched: 1 Changed: 1 Warnings: 0 - -mysql> rollback; -Query OK, 0 rows affected (0.00 sec) -``` - -结果:`事务A`读到了`事务B`还没提交的中间状态,即产生了`脏读`。 - -#### 不可重复读(read committed) - -**事务A读到了事务B已经提交的修改数据** - -操作: -1. `session A`事务隔离级别设置为`read committed`并开启事务,首次查询`book_id`为1的记录; -2. 然后`session B`开启事务,并修改`book_id`为1的记录,不提交事务,在`session A`中再次查询`book_id`为1的记录; -3. 最后提交`session B`中的事务,再在`session A`中查看`book_id`为1的记录。 - -session A: - -```sql -mysql> set session transaction isolation level read committed; -Query OK, 0 rows affected (0.01 sec) - -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> select * from tb_book where book_id = 1; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情剑客无情剑 | 古龙 | -+---------+-----------------------+--------+ -1 row in set (0.00 sec) - -mysql> select * from tb_book where book_id = 1; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情剑客无情剑 | 古龙 | -+---------+-----------------------+--------+ -1 row in set (0.00 sec) - -mysql> select * from tb_book where book_id = 1; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -+---------+-----------------------+--------+ -1 row in set (0.00 sec) - -mysql> commit; -Query OK, 0 rows affected (0.00 sec) -``` - -session B: - -```sql -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> update tb_book set book_name = '多情刀客无情刀' where book_id = 1; -Query OK, 1 row affected (0.00 sec) -Rows matched: 1 Changed: 1 Warnings: 0 - -mysql> commit; -Query OK, 0 rows affected (0.00 sec) -``` -结果:`事务B`没有提交事务时,`事务A`不会读到`事务B`修改的中间状态,即`read committed`解决了上面所说的`脏读`问题,但是当`事务B`中的事务提交后,`事务A`读到了修改后的记录,而对于`事务A`来说,仅仅读了两次,却读到了两个不同的结果,违背了事务之间的隔离性,所以说该事务隔离级别下产生了`不可重复读`的问题。 - -#### 幻读(repeatable read) - -**事务A读到了事务B提交的新增数据** - -操作: -1. `session A`事务隔离级别设置为`repeatable read`并开启事务,并查询`book`列表 -2. `session B`开启事务,先修改`book_id`为5的记录,再插入一条新的数据,提交事务,在`session A`中再次查询`book`列表 -3. 在`session A`中更新`session B`中新插入的那条数据,再查询`book`列表 - -session A: - -```sql -mysql> set session transaction isolation level repeatable read; -Query OK, 0 rows affected (0.00 sec) - -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> select * from tb_book; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -| 2 | 笑傲江湖 | 金庸 | -| 3 | 倚天屠龙记 | 金庸 | -| 4 | 射雕英雄传 | 金庸 | -| 5 | 绝代双骄 | 古龙 | -+---------+-----------------------+--------+ -5 rows in set (0.00 sec) - -mysql> select * from tb_book; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -| 2 | 笑傲江湖 | 金庸 | -| 3 | 倚天屠龙记 | 金庸 | -| 4 | 射雕英雄传 | 金庸 | -| 5 | 绝代双骄 | 古龙 | -+---------+-----------------------+--------+ -5 rows in set (0.00 sec) - -mysql> update tb_book set book_name = '圆月弯剑' where book_id = 6; -Query OK, 1 row affected (0.00 sec) -Rows matched: 1 Changed: 1 Warnings: 0 - -mysql> select * from tb_book; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -| 2 | 笑傲江湖 | 金庸 | -| 3 | 倚天屠龙记 | 金庸 | -| 4 | 射雕英雄传 | 金庸 | -| 5 | 绝代双骄 | 古龙 | -| 6 | 圆月弯剑 | 古龙 | -+---------+-----------------------+--------+ -6 rows in set (0.00 sec) - -mysql> rollback; -Query OK, 0 rows affected (0.00 sec) -``` - -session B: - -```sql -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> update tb_book set book_name = '绝代双雄' where book_id = 5; -Query OK, 1 row affected (0.00 sec) -Rows matched: 1 Changed: 1 Warnings: 0 - -mysql> insert into tb_book values (6, '圆月弯刀', '古龙'); -Query OK, 1 row affected (0.00 sec) - -mysql> commit; -Query OK, 0 rows affected (0.00 sec) - -mysql> select * from tb_book; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -| 2 | 笑傲江湖 | 金庸 | -| 3 | 倚天屠龙记 | 金庸 | -| 4 | 射雕英雄传 | 金庸 | -| 5 | 绝代双雄 | 古龙 | -| 6 | 圆月弯刀 | 古龙 | -+---------+-----------------------+--------+ -6 rows in set (0.00 sec) -``` - -结果:`事务B`已提交的修改记录(即`绝代双骄`修改为`绝代双雄`)在`事务A`中是不可见的,说明该事务隔离级别下解决了上面`不可重复读`的问题,但魔幻的是一开始`事务A`中虽然读不到`事务B`中的新增记录,却可以更新这条新增记录,执行更新(`update`)后,在`事务A`中居然可见该新增记录了,这便产生了所谓的`幻读`问题。 - -**为什么会出现这样莫名其妙的结果?** 别急,后文会慢慢揭开这个神秘的面纱。先看如何解决幻读问题。 - -#### 串行化(serializable) - -`serializable`事务隔离级别可以避免幻读问题,但会极大的降低数据库的并发能力。 -> SERIALIZABLE: the isolation level that uses the most conservative locking strategy, to prevent any other transactions from inserting or changing data that was read by this transaction, until it is finished. - -操作: -1. `session A`事务隔离级别设置为`serializable`并开启事务,并查询`book`列表,不提交事务; -2. 然后`session B`中分别执行`insert`、`delete`、`update`操作 - -session A: - -```sql -mysql> set session transaction isolation level serializable; -Query OK, 0 rows affected (0.00 sec) - -mysql> begin; -Query OK, 0 rows affected (0.00 sec) - -mysql> select * from tb_book; -+---------+-----------------------+--------+ -| book_id | book_name | author | -+---------+-----------------------+--------+ -| 1 | 多情刀客无情刀 | 古龙 | -| 2 | 笑傲江湖 | 金庸 | -| 3 | 倚天屠龙记 | 金庸 | -| 4 | 射雕英雄传 | 金庸 | -| 5 | 绝代双雄 | 古龙 | -| 6 | 圆月弯刀 | 古龙 | -+---------+-----------------------+--------+ -6 rows in set (0.00 sec) -``` - -session B: - -```sql -mysql> insert into tb_book values (7, '神雕侠侣', '金庸'); -ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction - -mysql> delete from tb_book where book_id = 1; -ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction - -mysql> update tb_book set book_name = '绝代双骄' where book_id = 5; -ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction -``` - -结果:只要`session A`中的事务一直不提交,`session B`中尝试更改数据(`insert`、`delete`、`update`)的事务都会被阻塞至超时(`timeout`)。显然,该事务隔离级别下能有效解决上面`幻读`、`不可重复读`、`脏读`等问题。 - -注意:除非是一些特殊的应用场景需要`serializable`事务隔离级别,否则很少会使用该隔离级别,因为并发性极低。 - -### 事务隔离级别小结 - -|事务隔离级别|脏读|不可重复读|幻读| -|----|----|----|----| -|read uncommitted|可能|可能|可能| -|read committed|不可能|可能|可能| -|repeatable read|不可能|不可能|可能| -|serializable|不可能|不可能|不可能| - -## MVCC机制 - -上面在演示`幻读`问题时,出现的结果让人捉摸不透。原来`InnoDB`存储引擎的默认事务隔离级别可重复读(`repeatable read`),是通过 "行级锁+MVCC"一起实现的。这就不得不去了解MVCC机制了。 - -相关文档:`https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html` - -参考: -《MySQL中MVCC的正确打开方式(源码佐证)》 `https://blog.csdn.net/Waves___/article/details/105295060` - -《InnoDB事务分析-MVCC》`http://www.leviathan.vip/2019/03/20/InnoDB的事务分析-MVCC/` - -《Innodb中的事务隔离级别和锁的关系》 `https://tech.meituan.com/2014/08/20/innodb-lock.html` - -### MVCC概念 - -多版本并发控制(`multiversion concurrency control`,即`MVCC`): 指的是一种提高并发的技术。最早期的数据库系统,只有读读之间可以并发,读写、写读、写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了`InnoDB`的并发性能。在内部实现中,`InnoDB`通过`undo log`保存每条数据的多个版本,并且能够提供数据历史版本给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。 - -简单来说,`MVCC`表达的是**维持一个数据的多个版本,使得读写操作没有冲突**这么一个思想。 - -MVCC在`read committed`和`repeatable read`两个事务隔离级别下工作。 - -#### 隐藏字段 - -> Internally, InnoDB adds three fields to each row stored in the database. A 6-byte DB_TRX_ID field indicates the transaction identifier for the last transaction that inserted or updated the row. Also, a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted. Each row also contains a 7-byte DB_ROLL_PTR field called the roll pointer. The roll pointer points to an undo log record written to the rollback segment. If the row was updated, the undo log record contains the information necessary to rebuild the content of the row before it was updated. A 6-byte DB_ROW_ID field contains a row ID that increases monotonically as new rows are inserted. If InnoDB generates a clustered index automatically, the index contains row ID values. Otherwise, the DB_ROW_ID column does not appear in any index. - -`InnoDB`存储引擎在每行数据的后面添加了三个隐藏字段,如下图所示: - -![表中某行数据示意图](https://img2020.cnblogs.com/blog/1546632/202010/1546632-20201018163556324-1921643603.png) - -1. `DB_TRX_ID`(6字节):表示最近一次对本记录行做修改(`insert`或`update`)的事务ID。至于`delete`操作,`InnoDB`认为是一个`update`操作,不过会更新一个另外的删除位,将行标识为deleted。并非真正删除。 - -2. `DB_ROLL_PTR`(7字节):回滚指针,指向当前记录行的`undo log`信息。 - -3. `DB_ROW_ID`(6字节):随着新行插入而单调递增的行ID。当表没有主键或唯一非空索引时,`InnoDB`就会使用这个行ID自动产生聚集索引。前文《一文读懂MySQL的索引结构及查询优化》中也有所提及。这个`DB_ROW_ID`跟`MVCC`关系不大。 - - -#### undo log - -`undo log`中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着`undo log`链表找到满足其可见性条件的记录行版本。 - -对数据的变更操作主要包括`insert/update/delete`,在`InnoDB`中,`undo log`分为如下两类: -- `insert undo log`: 事务对`insert`新记录时产生的`undo log`, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。 -- `update undo log`: 事务对记录进行`delete`和`update`操作时产生的`undo log`,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被`purge`线程删除。 - -> `Purge`线程:为了实现`InnoDB`的`MVCC`机制,更新或者删除操作都只是设置一下旧记录的`deleted_bit`,并不真正将旧记录删除。为了节省磁盘空间,`InnoDB`有专门的`purge`线程来清理`deleted_bit`为`true`的记录。`purge`线程自己也维护了一个`read view`,如果某个记录的`deleted_bit`为`true`,并且`DB_TRX_ID`相对于`purge`线程的`read view`可见,那么这条记录一定是可以被安全清除的。 - -不同事务或者相同事务的对同一记录行的修改形成的`undo log`如下图所示: - -![undo log的示意图](https://img2020.cnblogs.com/blog/1546632/202010/1546632-20201018214116601-1252297826.png) - -可见链首就是最新的记录,链尾就是最早的旧记录。 - -#### Read View结构 - -`Read View`(读视图)提供了某一时刻事务系统的快照,主要是用来做`可见性`判断的, 里面保存了"对本事务不可见的其他活跃事务"。 - -MySQL`5.7`源码中对`Read View`定义如下(详情见`https://github.com/mysql/mysql-server/blob/5.7/storage/innobase/include/read0types.h#L306`): - -```cpp -class ReadView { - private: - /** The read should not see any transaction with trx id >= this - value. In other words, this is the "high water mark". */ - trx_id_t m_low_limit_id; - - /** The read should see all trx ids which are strictly - smaller (<) than this value. In other words, this is the - low water mark". */ - trx_id_t m_up_limit_id; - - /** trx id of creating transaction, set to TRX_ID_MAX for free - views. */ - trx_id_t m_creator_trx_id; - - /** Set of RW transactions that was active when this snapshot - was taken */ - ids_t m_ids; - - /** The view does not need to see the undo logs for transactions - whose transaction number is strictly smaller (<) than this value: - they can be removed in purge if not needed by other views */ - trx_id_t m_low_limit_no; - - /** AC-NL-RO transaction view that has been "closed". */ - bool m_closed; - - typedef UT_LIST_NODE_T(ReadView) node_t; - - /** List of read views in trx_sys */ - byte pad1[64 - sizeof(node_t)]; - node_t m_view_list; -}; -``` -重点解释下面几个变量(建议仔细看上面的源码注释,以下仅为个人理解,有理解不到位的地方欢迎指出(●´ω`●)): - -(1) `m_ids`: `Read View`创建时其他未提交的活跃事务ID列表。具体说来就是创建`Read View`时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。注意:该事务ID列表不包括当前事务自己和已提交的事务。 - -(2) `m_low_limit_id`:某行数据的`DB_TRX_ID >= m_low_limit_id`的任何版本对该查询`不可见`。那么这个值是怎么确定的呢?其实就是读的时刻出现过的最大的事务ID+1,即下一个将被分配的事务ID。见`https://github.com/mysql/mysql-server/blob/5.7/storage/innobase/read/read0read.cc#L459` - -```cpp -/** -Opens a read view where exactly the transactions serialized before this -point in time are seen in the view. -@param id Creator transaction id */ - -void -ReadView::prepare(trx_id_t id) -{ - m_creator_trx_id = id; - - m_low_limit_no = m_low_limit_id = trx_sys->max_trx_id; -} -``` -`max_trx_id`见`https://github.com/mysql/mysql-server/blob/5.7/storage/innobase/include/trx0sys.h#L576`中的描述,翻译过来就是“还未分配的最小事务ID”,也就是下一个将被分配的事务ID。(注意,`m_low_limit_id`并不是活跃事务列表中最大的事务ID) -```cpp -struct trx_sys_t { -/*!< The smallest number not yet - assigned as a transaction id or - transaction number. This is declared - volatile because it can be accessed - without holding any mutex during - AC-NL-RO view creation. */ - volatile trx_id_t max_trx_id; -} -``` - -(3) `m_up_limit_id`:某行数据的`DB_TRX_ID < m_up_limit_id`的所有版本对该查询`可见`。同样这个值又是如何确定的呢?`m_up_limit_id`是活跃事务列表`m_ids`中最小的事务ID,如果trx_ids为空,则`m_up_limit_id`为`m_low_limit_id`。代码见`https://github.com/mysql/mysql-server/blob/5.7/storage/innobase/read/read0read.cc#L485` -```cpp -void -ReadView::complete() -{ - /* The first active transaction has the smallest id. */ - m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id; - - ut_ad(m_up_limit_id <= m_low_limit_id); - - m_closed = false; -} -``` -这样就有下面的可见性比较算法了。代码见`https://github.com/mysql/mysql-server/blob/5.7/storage/innobase/include/read0types.h#L169` -```cpp -/** Check whether the changes by id are visible. - @param[in] id transaction id to check against the view - @param[in] name table name - @return whether the view sees the modifications of id. */ -bool changes_visible( - trx_id_t id, - const table_name_t& name) const - MY_ATTRIBUTE((warn_unused_result)) -{ - ut_ad(id > 0); - - - /* 假如 trx_id 小于 Read view 限制的最小活跃事务ID m_up_limit_id 或者等于正在创建的事务ID m_creator_trx_id - * 即满足事务的可见性. - */ - if (id < m_up_limit_id || id == m_creator_trx_id) { - return(true); - } - - /* 检查 trx_id 是否有效. */ - check_trx_id_sanity(id, name); - - if (id >= m_low_limit_id) { - /* 假如 trx_id 大于等于m_low_limit_id, 即不可见. */ - return(false); - - } else if (m_ids.empty()) { - /* 假如目前不存在活跃的事务,即可见. */ - return(true); - } - - const ids_t::value_type* p = m_ids.data(); - - /* 利用二分查找搜索活跃事务列表 - * 当 trx_id 在 m_up_limit_id 和 m_low_limit_id 之间 - * 如果 id 在 m_ids 数组中, 表明 ReadView 创建时候,事务处于活跃状态,因此记录不可见. - */ - return (!std::binary_search(p, p + m_ids.size(), id)); -} -``` - -![事务可见性比较算法图示](https://img2020.cnblogs.com/blog/1546632/202010/1546632-20201018232747852-703197062.png) - -完整梳理一下整个过程。 - -在`InnoDB`中,创建一个新事务后,执行第一个`select`语句的时候,`InnoDB`会创建一个快照(`read view`),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即`m_ids`)。当用户在这个事务中要读取某个记录行的时候,`InnoDB`会将该记录行的`DB_TRX_ID`与该`Read View`中的一些变量进行比较,判断是否满足可见性条件。 - -假设当前事务要读取某一个记录行,该记录行的`DB_TRX_ID`(即最新修改该行的事务ID)为`trx_id`,`Read View`的活跃事务列表`m_ids`中最早的事务ID为`m_up_limit_id`,将在生成这个`Read Vew`时系统出现过的最大的事务ID+1记为`m_low_limit_id`(即还未分配的事务ID)。 - -具体的比较算法如下: - -1. 如果`trx_id < m_up_limit_id`,那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的。跳到步骤5。 - -2. 如果`trx_id >= m_low_limit_id`, 那么表明“最新修改该行的事务”在“当前事务”创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤4。 - -3. 如果`m_up_limit_id <= trx_id < m_low_limit_id`, 表明“最新修改该行的事务”在“当前事务”创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表trx_ids进行查找(源码中是用的二分查找,因为是有序的) - -(1) 如果在活跃事务列表`m_ids`中能找到id为`trx_id`的事务,表明①在“当前事务”创建快照前,“该记录行的值”被“id为`trx_id`的事务”修改了,但没有提交;或者②在“当前事务”创建快照后,“该记录行的值”被“id为`trx_id`的事务”修改了(不管有无提交);这些情况下,这个记录行的值对当前事务都是不可见的,跳到步骤4; - -(2) 在活跃事务列表中找不到,则表明“id为`trx_id`的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见,跳到步骤5。 - -4. 在该记录行的`DB_ROLL_PTR`指针所指向的`undo log`回滚段中,取出最新的的旧事务号`DB_TRX_ID`, 将它赋给`trx_id`,然后跳到步骤1重新开始判断。 - -5. 将该可见行的值返回。 - -### read committed与repeatable read的区别 - -有了上面的知识铺垫后,就可以从本质上区别`read committed`与`repeatable read`这两种事务隔离级别了。 - -> With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTED isolation level, the snapshot is reset to the time of each consistent read operation. - -在`InnoDB`中的`repeatable read`级别, 事务`begin`之后,执行第一条`select`(读操作)时, 会创建一个快照(`read view`),将当前系统中活跃的其他事务记录起来;并且在此事务中之后的其他`select`操作都是使用的这个`read view`对象,不会重新创建,直到事务结束。 - -在`InnoDB`中的`read committed`级别, 事务`begin`之后,执行每条`select`(读操作)语句时,快照会被重置,即会基于当前`select`重新创建一个快照(`read view`),所以显然该事务隔离级别下会读到其他事务已经提交的修改数据。 - -那么,现在能解释上面演示`幻读`问题时,出现的诡异结果吗?我的理解是,因为是在`repeatable read`隔离级别下,肯定还是快照读,即第一次`select`后创建的`read view`对象还是不变的,但是在当前事务中`update`一条记录时,会把当前事务ID设置到更新后的记录的隐藏字段`DB_TRX_ID`上,即`id == m_creator_trx_id`显然成立,于是该条记录就可见了,再次执行`select`操作时就多出这条记录了。 -```cpp -if (id < m_up_limit_id || id == m_creator_trx_id) { - return(true); - } -``` - -另外,有了这样的基本认知后,如果你在MySQL事务隔离相关问题遇到一些其他看似很神奇的现象,也可以试试能不能解释得通。 - -## 总结 - -通过学习MySQL事务隔离级别及`MVCC`原理机制,有助于加深对MySQL的理解与掌握,更为重要的是,如果让你编写一个并发读写的存储程序,`MVCC`的设计与实现或许能给你一些启发。 diff --git a/Netty/the-truth-of-netty.md b/Netty/the-truth-of-netty.md deleted file mode 100644 index 171bdd1..0000000 --- a/Netty/the-truth-of-netty.md +++ /dev/null @@ -1,1714 +0,0 @@ -# 不识Netty真面目,只缘未读此真经 - - -Netty官网:[https://netty.io/](https://netty.io/) - - -> Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. - - - -Java技术栈方向的朋友应该或多或少都听说过Netty是对Java中nio ( `Non Blocking IO` )的封装,让我们能快速开发出性能更高、扩展性更好的网络应用程序。那么Netty究竟对nio做了怎样的封装呢?本文主要从源码角度揭开这层面纱。 - -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130170906583-412859080.png) - -源码追踪中,我使用阿里的语雀产品的思维图记录主要方法调用,上面的图片是部分截图,完整原貌见: - -[https://www.yuque.com/docs/share/02fa3e3d-d485-48e1-9cfe-6722a3ad8915](https://www.yuque.com/docs/share/02fa3e3d-d485-48e1-9cfe-6722a3ad8915?#) - -## 预备知识 - - -在初探Netty源码之前,至少需要理解Reactor Pattern、java.nio基本使用、Netty基本使用,这样后面才能把Netty的源码与java.nio对比着来看。 - - -### Reactor Pattern - - -不识Netty真面目,只缘未读此真经。`Doug Lea` (java.util.concurrent包的作者) 在《Scalable IO in Java》中循序渐进地分析了如何构建可伸缩的高性能IO服务以及服务模型的演变与进化。文中描述的`Reactor Pattern`,也被Netty等大多数高性能IO服务框架所借鉴。因此仔细阅读《Scalable IO in Java》有助于更好地理解Netty框架的架构与设计。详情见: - -[http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf](http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf) - -#### 传统的服务模式 -Server端为每一个Client端的连接请求都开启一个独立线程,也就是所谓的BIO (Blocking IO),即`java.net.ServerSocket`包下api的使用。 -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130171559689-867470723.png) - -#### 基于事件驱动模式 -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130171709458-1727117422.png) -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130171802264-732332822.png) - -#### Reactor模式 - - -> Reactor responds to IO events by dispatching the appropriate handler (Similar to AWT thread) - - - -> Handlers perform non-blocking actions (Similar to AWT ActionListeners) - - - -> Manage by binding handlers to events (Similar to AWT addActionListener) - - - -(1) **单线程版本** -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130171936671-61651517.png) - -(2) **多线程版本** -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130172022313-1674668933.png) - - -(3) **多Reactor版本 (一主多从、多主多从)** -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130172337275-1686188238.png) -**Netty正是借鉴了这种多Reactor版本的设计。** - - -### 快速上手java.nio - - - -- Channels:Connections to files, sockets etc that support non-blocking reads -- Buffers:Array-like objects that can be directly read or written by Channels -- Selectors:Tell which of a set of Channels have IO events -- SelectionKeys:Maintain IO event status and bindings - - - -**注意:以下Demo仅专注于主逻辑,没有处理异常,也没有关闭资源。** -#### Server端 -```java -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.nio.channels.spi.SelectorProvider; -import java.util.Iterator; - -public class NIOServer { - - private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider(); - - public static void main(String[] args) throws IOException { - // ServerSocketChannel.open() - ServerSocketChannel serverSocketChannel = DEFAULT_SELECTOR_PROVIDER.openServerSocketChannel(); - serverSocketChannel.configureBlocking(false); - - serverSocketChannel.socket().bind(new InetSocketAddress(8080)); - - // Selector.open() - Selector selector = DEFAULT_SELECTOR_PROVIDER.openSelector(); - - // register this serverSocketChannel with the selector - serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - - // selector.select() - while (!Thread.interrupted()) { - selector.select(); - - Iterator it = selector.selectedKeys().iterator(); - while (it.hasNext()) { - SelectionKey key = it.next(); - it.remove(); - - // handle IO events - handle(key); - } - } - } - - private static void handle(SelectionKey key) throws IOException { - if (key.isAcceptable()) { - ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); - - SocketChannel socketChannel = serverSocketChannel.accept(); - socketChannel.configureBlocking(false); - socketChannel.register(key.selector(), SelectionKey.OP_READ); - - } else if (key.isReadable()) { - SocketChannel socketChannel = (SocketChannel) key.channel(); - // read client data - ByteBuffer buffer = ByteBuffer.allocate(1024); - int len = socketChannel.read(buffer); - if (len != -1) { - String msg = String.format("recv client[%s] data:%s", socketChannel.getRemoteAddress(), - new String(buffer.array(), 0, len)); - System.out.println(msg); - } - // response client - ByteBuffer data = ByteBuffer.wrap("Hello, NIOClient!".getBytes()); - socketChannel.write(data); - key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); - - } else if (key.isWritable()) { - // ... - } - } -} -``` - - -#### Client端 -```java -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; -import java.nio.channels.spi.SelectorProvider; -import java.util.Iterator; - -public class NIOClient { - - private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider(); - - public static void main(String[] args) throws IOException { - // SocketChannel.open() - SocketChannel socketChannel = DEFAULT_SELECTOR_PROVIDER.openSocketChannel(); - socketChannel.configureBlocking(false); - socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080)); - - // Selector.open() - Selector selector = DEFAULT_SELECTOR_PROVIDER.openSelector(); - - // register this socketChannel with the selector - socketChannel.register(selector, SelectionKey.OP_CONNECT); - - // selector.select() - while (!Thread.interrupted()) { - selector.select(); - - Iterator it = selector.selectedKeys().iterator(); - while (it.hasNext()) { - SelectionKey key = it.next(); - it.remove(); - - // handle IO events - if (key.isConnectable()) { - SocketChannel channel = (SocketChannel) key.channel(); - if (channel.isConnectionPending()) { - channel.finishConnect(); - } - - channel.configureBlocking(false); - // request server - ByteBuffer buffer = ByteBuffer.wrap("Hello, NIOServer!".getBytes()); - channel.write(buffer); - channel.register(selector, SelectionKey.OP_READ); - - } else if (key.isReadable()) { - SocketChannel channel = (SocketChannel) key.channel(); - // read server data - ByteBuffer buffer = ByteBuffer.allocate(1024); - int len = channel.read(buffer); - if (len != -1) { - String msg = String.format("recv server[%s] data:%s", channel.getRemoteAddress(), - new String(buffer.array(), 0, len)); - System.out.println(msg); - } - } - } - } - } -} -``` - - -### 快速上手Netty - - -更多官方`example`,请参考: - -[https://github.com/netty/netty/tree/4.1/example/](https://github.com/netty/netty/tree/4.1/example/) -#### Server端 -```java -import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.util.CharsetUtil; - -public class NettyServer { - public static void main(String[] args) throws Exception { - EventLoopGroup bossGroup = new NioEventLoopGroup(1); - EventLoopGroup workerGroup = new NioEventLoopGroup(); - try { - ServerBootstrap bootstrap = new ServerBootstrap() - .group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .option(ChannelOption.SO_BACKLOG, 1024) - .childHandler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) { - ch.pipeline().addLast(new NettyServerHandler()); - } - }); - - ChannelFuture cf = bootstrap.bind(8080).sync(); - cf.channel().closeFuture().sync(); - } finally { - bossGroup.shutdownGracefully(); - workerGroup.shutdownGracefully(); - } - } - - static class NettyServerHandler extends ChannelInboundHandlerAdapter { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - String message = String.format("recv client[%s] data:%s", ctx.channel().remoteAddress(), - ((ByteBuf) msg).toString(CharsetUtil.UTF_8)); - System.out.println(message); - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - ByteBuf buf = Unpooled.copiedBuffer("Hello, NettyClient!".getBytes(CharsetUtil.UTF_8)); - ctx.writeAndFlush(buf); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } - } -} -``` - - -#### Client端 -```java -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.util.CharsetUtil; - -public class NettyClient { - public static void main(String[] args) throws Exception { - EventLoopGroup group = new NioEventLoopGroup(1); - try { - Bootstrap bootstrap = new Bootstrap() - .group(group) - .channel(NioSocketChannel.class) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) { - ch.pipeline().addLast(new NettyClientHandler()); - } - }); - - ChannelFuture cf = bootstrap.connect("127.0.0.1", 8080).sync(); - cf.channel().closeFuture().sync(); - } finally { - group.shutdownGracefully(); - } - } - - static class NettyClientHandler extends ChannelInboundHandlerAdapter { - @Override - public void channelActive(ChannelHandlerContext ctx) { - ByteBuf buf = Unpooled.copiedBuffer("Hello, NettyServer!".getBytes(CharsetUtil.UTF_8)); - ctx.writeAndFlush(buf); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - String message = String.format("recv server[%s] data:%s", ctx.channel().remoteAddress(), - ((ByteBuf) msg).toString(CharsetUtil.UTF_8)); - System.out.println(message); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } - } -} -``` - - -## 源码追踪 - -建议跟着我画的源码走向图,跟下面的内容,最好也开着`debug`模式,不理解的地方调试几遍。这里再次贴一下链接: - -[https://www.yuque.com/docs/share/02fa3e3d-d485-48e1-9cfe-6722a3ad8915](https://www.yuque.com/docs/share/02fa3e3d-d485-48e1-9cfe-6722a3ad8915) - -注意:追踪的是当前最新release的 `4.1.58.Final` 版本的源码。 -```xml - - io.netty - netty-all - 4.1.58.Final - -``` -本文出自 `行无际的博客` : - - -[https://www.cnblogs.com/itwild/](https://www.cnblogs.com/itwild/) -### 关键类 - - -下面先重点看几个关键类的大致情况,方便我们读代码 。因为面向抽象编程,如果对常见类的继承层次一点不了解,读代码的过程会让人崩溃。你懂的!!! - -#### **NioEventLoopGroup** - - -**类定义**: - - -io.netty.channel.nio.**NioEventLoopGroup** -```java -/** - * {@link MultithreadEventLoopGroup} implementations which is used for NIO {@link Selector} based {@link Channel}s. - */ -public class NioEventLoopGroup extends MultithreadEventLoopGroup -``` -**类图:** -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130175435414-575557158.png) -#### **NioEventLoop** - - -**类定义**: - - -io.netty.channel.nio.**NioEventLoop** -```java -/** - * {@link SingleThreadEventLoop} implementation which register the {@link Channel}'s to a - * {@link Selector} and so does the multi-plexing of these in the event loop. - * - */ -public final class NioEventLoop extends SingleThreadEventLoop -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130175633683-1482258872.png) -#### NioServerSocketChannel - - -**类定义**: - - -io.netty.channel.socket.nio.**NioServerSocketChannel** -```java -/** - * A {@link io.netty.channel.socket.ServerSocketChannel} implementation which uses - * NIO selector based implementation to accept new connections. - */ -public class NioServerSocketChannel extends AbstractNioMessageChannel - implements io.netty.channel.socket.ServerSocketChannel -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130175733342-799796289.png) -#### NioSocketChannel - - -**类定义**: - - -io.netty.channel.socket.nio.**NioSocketChannel** -```java -/** - * {@link io.netty.channel.socket.SocketChannel} which uses NIO selector based implementation. - */ -public class NioSocketChannel extends AbstractNioByteChannel - implements io.netty.channel.socket.SocketChannel -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130175836009-1642980937.png) - - -#### ChannelInitializer - - -**类定义**: - - -io.netty.channel.**ChannelInitializer** -```java -/** - * A special {@link ChannelInboundHandler} which offers an easy way to initialize a {@link Channel} once it was - * registered to its {@link EventLoop}. - * - * Implementations are most often used in the context of {@link Bootstrap#handler(ChannelHandler)} , - * {@link ServerBootstrap#handler(ChannelHandler)} and {@link ServerBootstrap#childHandler(ChannelHandler)} to - * setup the {@link ChannelPipeline} of a {@link Channel}. - * - *
- *
- * public class MyChannelInitializer extends {@link ChannelInitializer} {
- *     public void initChannel({@link Channel} channel) {
- *         channel.pipeline().addLast("myHandler", new MyHandler());
- *     }
- * }
- *
- * {@link ServerBootstrap} bootstrap = ...;
- * ...
- * bootstrap.childHandler(new MyChannelInitializer());
- * ...
- * 
- * Be aware that this class is marked as {@link Sharable} and so the implementation must be safe to be re-used. - * - * @param A sub-type of {@link Channel} - */ -@Sharable -public abstract class ChannelInitializer extends ChannelInboundHandlerAdapter -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130180015718-634009333.png) -#### ChannelInboundHandlerAdapter - - -**类定义**: - - -io.netty.channel.**ChannelInboundHandlerAdapter** -```java -/** - * Abstract base class for {@link ChannelInboundHandler} implementations which provide - * implementations of all of their methods. - * - *

- * This implementation just forward the operation to the next {@link ChannelHandler} in the - * {@link ChannelPipeline}. Sub-classes may override a method implementation to change this. - *

- *

- * Be aware that messages are not released after the {@link #channelRead(ChannelHandlerContext, Object)} - * method returns automatically. If you are looking for a {@link ChannelInboundHandler} implementation that - * releases the received messages automatically, please see {@link SimpleChannelInboundHandler}. - *

- */ -public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130180109769-699346310.png) -#### ServerBootstrap - - -**类定义**: - - -io.netty.bootstrap.**ServerBootstrap** -```java -/** - * {@link Bootstrap} sub-class which allows easy bootstrap of {@link ServerChannel} - * - */ -public class ServerBootstrap extends AbstractBootstrap -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130180202875-1218796062.png) -#### Bootstrap - - -**类定义**: - -io.netty.bootstrap.**Bootstrap** -```java -/** - * A {@link Bootstrap} that makes it easy to bootstrap a {@link Channel} to use - * for clients. - * - *

The {@link #bind()} methods are useful in combination with connectionless transports such as datagram (UDP). - * For regular TCP connections, please use the provided {@link #connect()} methods.

- */ -public class Bootstrap extends AbstractBootstrap -``` -**类图**: -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130180259793-1300896922.png) -### Server端启动过程 - - -下面就正式开始追源码。 - - -#### 创建Selector - - -`Selector`的创建起于这行代码`EventLoopGroup bossGroup = new NioEventLoopGroup(1)` - - -io.netty.channel.nio.**NioEventLoopGroup** -```java -/** - * Create a new instance using the specified number of threads, {@link ThreadFactory} and the - * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. - */ -public NioEventLoopGroup(int nThreads) { - this(nThreads, (Executor) null); -} - - public NioEventLoopGroup(int nThreads, Executor executor) { - this(nThreads, executor, SelectorProvider.provider()); - } -``` -这里我们看到了熟悉的`SelectorProvider.provider()`,如果觉得陌生,建议回到上面**快速上手java.nio**的代码。 - - -往里面追几层,就到了`NioEventLoopGroup`的父类 `MultithreadEventExecutorGroup` 。 - - -io.netty.util.concurrent.**MultithreadEventExecutorGroup** -```java -protected MultithreadEventExecutorGroup(int nThreads, Executor executor, - EventExecutorChooserFactory chooserFactory, Object... args) { - if (executor == null) { - executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); - } - - children = new EventExecutor[nThreads]; - - for (int i = 0; i < nThreads; i ++) { - children[i] = newChild(executor, args); - } -} -``` -注意: 创建`NioEventLoopGroup(int nThreads)`时的参数`nThreads`就传到了上面代码中的`children = new EventExecutor[nThreads]`。看`newChild(executor, args)`做了什么。 - - -io.netty.channel.nio.**NioEventLoopGroup** -```java -@Override -protected EventLoop newChild(Executor executor, Object... args) throws Exception { - EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null; - return new NioEventLoop(this, executor, (SelectorProvider) args[0], - ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory); -} -``` - - -io.netty.channel.nio.**NioEventLoop** -```java -NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, - SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, - EventLoopTaskQueueFactory queueFactory) { - super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), - rejectedExecutionHandler); - this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider"); - this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy"); - final SelectorTuple selectorTuple = openSelector(); - this.selector = selectorTuple.selector; - this.unwrappedSelector = selectorTuple.unwrappedSelector; -} -``` -`Selector`的创建就发生在这行代码`final SelectorTuple selectorTuple = openSelector();`进去看看。 - - -io.netty.channel.nio.**NioEventLoop** -```java -private SelectorTuple openSelector() { - final Selector unwrappedSelector; - try { - unwrappedSelector = provider.openSelector(); - } catch (IOException e) { - throw new ChannelException("failed to open a new selector", e); - } - - if (DISABLE_KEY_SET_OPTIMIZATION) { - return new SelectorTuple(unwrappedSelector); - } - // 省略其他代码... - return new SelectorTuple(unwrappedSelector, - new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet)); -} -``` -这里我们看到了`provider.openSelector()`,到这里,创建出来的`Selector`就与 `EventLoop` 关联在一起了。 - - -同时在创建`NioEventLoop`时,看看`super(parent, executor, false, newTaskQueue(queueFactory), ...)`在父类`SingleThreadEventLoop`干了什么。 - - -io.netty.channel.**SingleThreadEventLoop** -```java -protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, - boolean addTaskWakesUp, Queue taskQueue, Queue tailTaskQueue, - RejectedExecutionHandler rejectedExecutionHandler) { - super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler); - tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue"); -} -``` -再往下; - - -io.netty.util.concurrent.**SingleThreadEventExecutor** -```java -private final Queue taskQueue; - -protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, - boolean addTaskWakesUp, Queue taskQueue, - RejectedExecutionHandler rejectedHandler) { - super(parent); - this.addTaskWakesUp = addTaskWakesUp; - this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS; - this.executor = ThreadExecutorMap.apply(executor, this); - this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue"); - this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler"); -} -``` -这里我们看到了对`Queue taskQueue`的赋值。 - - -#### 创建ServerSocketChannel - - -`AbstractBootstrap`中的`initAndRegister()`方法是`ServerSocketChannel`的创建入口。 - - -io.netty.bootstrap.**AbstractBootstrap** -```java -final ChannelFuture initAndRegister() { - Channel channel = null; - try { - // 1.创建ServerSocketChannel - channel = channelFactory.newChannel(); - // 2.初始化ServerSocketChannel - init(channel); - } catch (Throwable t) { - } - // 3.将ServerSocketChannel注册到Selector上 - ChannelFuture regFuture = config().group().register(channel); - return regFuture; - } -``` -Server端的启动最核心的也就是上面加注释的三步。按照顺序先从`ServerSocketChannel`的创建讲起。 - - -`ServerSocketChannel`的创建用了**工厂模式**+**反射机制**。具体见`ReflectiveChannelFactory` - - -io.netty.channel.**ReflectiveChannelFactory** -```java -/** - * A {@link ChannelFactory} that instantiates a new {@link Channel} by invoking its default constructor reflectively. - */ -public class ReflectiveChannelFactory implements ChannelFactory { - - private final Constructor constructor; - - public ReflectiveChannelFactory(Class clazz) { - this.constructor = clazz.getConstructor(); - } - - @Override - public T newChannel() { - return constructor.newInstance(); - } -} -``` -还记得在前面的`bootstrap.channel(NioServerSocketChannel.class)`这行代码吗?传入的`Class`就是用于反射生成`Channel`实例的。这里是Server端,显然需要进`NioServerSocketChannel`看如何创建的。 - - -io.netty.channel.socket.nio.**NioServerSocketChannel** -```java -private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider(); - -private static ServerSocketChannel newSocket(SelectorProvider provider) { - try { - /** - * Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in - * {@link SelectorProvider#provider()} which is called by each ServerSocketChannel.open() otherwise. - * - * See #2308. - */ - return provider.openServerSocketChannel(); - } catch (IOException e) { - throw new ChannelException( - "Failed to open a server socket.", e); - } -} - -public NioServerSocketChannel() { - this(newSocket(DEFAULT_SELECTOR_PROVIDER)); -} - - public NioServerSocketChannel(ServerSocketChannel channel) { - super(null, channel, SelectionKey.OP_ACCEPT); - config = new NioServerSocketChannelConfig(this, javaChannel().socket()); - } -``` -`provider.openServerSocketChannel()`这行代码也就创建出来了`ServerSocketChannel`。再往父类里面追,看做了些什么。`super(null, channel, SelectionKey.OP_ACCEPT);` - - -io.netty.channel.nio.**AbstractNioChannel** -```java -protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { - super(parent); - this.ch = ch; - this.readInterestOp = readInterestOp; - ch.configureBlocking(false); -} -``` -`this.readInterestOp = readInterestOp`把感兴趣的操作赋值给`readInterestOp`,上面传过来的是`SelectionKey.OP_ACCEPT`。 - -`ch.configureBlocking(false)`把刚才创建出来的`channel`设置为非阻塞。继续往父类追。 - - -io.netty.channel.**AbstractChannel** -```java -protected AbstractChannel(Channel parent) { - this.parent = parent; - id = newId(); - unsafe = newUnsafe(); - pipeline = newChannelPipeline(); -} - -protected DefaultChannelPipeline newChannelPipeline() { - return new DefaultChannelPipeline(this); -} -``` -这里看到创建了`ChannelPipeline`,并关联到`Channel`上。再往下走一步。 - - -io.netty.channel.**DefaultChannelPipeline** -```java -protected DefaultChannelPipeline(Channel channel) { - this.channel = ObjectUtil.checkNotNull(channel, "channel"); - succeededFuture = new SucceededChannelFuture(channel, null); - voidPromise = new VoidChannelPromise(channel, true); - - tail = new TailContext(this); - head = new HeadContext(this); - - head.next = tail; - tail.prev = head; -} -``` -此时`ChannelPipeline`大致如下: -```bash -head --> tail -``` - - -#### 初始化ServerSocketChannel - - -回到上面提到的重要的第2步: `init(channel);` 注意,实现类为`ServerBootstrap`,因为是Server端嘛。 - - -io.netty.bootstrap.**ServerBootstrap** -```java -@Override -void init(Channel channel) { - ChannelPipeline p = channel.pipeline(); - - final EventLoopGroup currentChildGroup = childGroup; - final ChannelHandler currentChildHandler = childHandler; - - p.addLast(new ChannelInitializer() { - @Override - public void initChannel(final Channel ch) { - final ChannelPipeline pipeline = ch.pipeline(); - ChannelHandler handler = config.handler(); - if (handler != null) { - pipeline.addLast(handler); - } - - ch.eventLoop().execute(new Runnable() { - @Override - public void run() { - pipeline.addLast(new ServerBootstrapAcceptor( - ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); - } - }); - } - }); -} -``` -在`ChannelPipeline`加了一个`ChannelHandler`。此时`ChannelPipeline`大致如下: -```bash -head --> ChannelInitializer --> tail -``` - - -一旦`serverSocketChannel`注册到`EventLoop`(或者说`Selector`)上,便会触发这里`initChannel`的调用。避免绕晕了,这里暂时不去探究具体的调用逻辑。后面调用到这里的时候,再回过头来仔细探究。 - - -#### ServerSocketChannel注册到Selector上 - - -回到上面提到的重要的第3步:`config().group().register(channel);` - - -通过分析类的继承层次(或者debug也行)可以跟踪调用到`SingleThreadEventLoop`的`register`方法。 - - -io.netty.channel.**SingleThreadEventLoop** -```java -@Override -public ChannelFuture register(Channel channel) { - return register(new DefaultChannelPromise(channel, this)); -} - -@Override -public ChannelFuture register(final ChannelPromise promise) { - ObjectUtil.checkNotNull(promise, "promise"); - promise.channel().unsafe().register(this, promise); - return promise; -} -``` -再往下跟,最终调用的是`AbstractChannel`的`register`方法,如下: - - -io.netty.channel.**AbstractChannel** -```java -@Override -public final void register(EventLoop eventLoop, final ChannelPromise promise) { - AbstractChannel.this.eventLoop = eventLoop; - eventLoop.execute(new Runnable() { - @Override - public void run() { - register0(promise); - } - }); -} -``` -往下跟`eventLoop.execute()` - - -io.netty.util.concurrent.**SingleThreadEventExecutor** -```java -private void execute(Runnable task, boolean immediate) { - addTask(task); - startThread(); -} -``` -`addTask(task)`把上面的`Runnable`放入到上面提到的`Queue taskQueue`,过程见如下代码: - - -io.netty.util.concurrent.**SingleThreadEventExecutor** -```java - /** - * Add a task to the task queue, or throws a {@link RejectedExecutionException} if this instance was shutdown - * before. - */ -protected void addTask(Runnable task) { - ObjectUtil.checkNotNull(task, "task"); - if (!offerTask(task)) { - reject(task); - } -} - -final boolean offerTask(Runnable task) { - if (isShutdown()) { - reject(); - } - return taskQueue.offer(task); -} -``` -把`task`放入`taskQueue`后,就到`startThread()`这行代码了,进去瞧瞧。 - - -io.netty.util.concurrent.**SingleThreadEventExecutor** -```java -private void startThread() { - doStartThread(); -} - -private void doStartThread() { - executor.execute(new Runnable() { - @Override - public void run() { - SingleThreadEventExecutor.this.run(); - } - }); -} -``` -继续追`executor.execute`,到这里才真正创建新的线程执行`SingleThreadEventExecutor.this.run()`, thread名称大致为`nioEventLoopGroup-2-1`,见如下代码: - - -io.netty.util.concurrent.**ThreadPerTaskExecutor** -```java -@Override -public void execute(Runnable command) { - threadFactory.newThread(command).start(); -} -``` -`SingleThreadEventExecutor.this.run()`实际执行的代码如下: - - -io.netty.channel.nio.**NioEventLoop** -```java -@Override -protected void run() { - int selectCnt = 0; - for (;;) { - try { - int strategy; - try { - strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); - switch (strategy) { - case SelectStrategy.CONTINUE: - continue; - - case SelectStrategy.BUSY_WAIT: - // fall-through to SELECT since the busy-wait is not supported with NIO - - case SelectStrategy.SELECT: - long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); - if (curDeadlineNanos == -1L) { - curDeadlineNanos = NONE; // nothing on the calendar - } - nextWakeupNanos.set(curDeadlineNanos); - try { - if (!hasTasks()) { - strategy = select(curDeadlineNanos); - } - } finally { - // This update is just to help block unnecessary selector wakeups - // so use of lazySet is ok (no race condition) - nextWakeupNanos.lazySet(AWAKE); - } - // fall through - default: - } - } catch (IOException e) { - // If we receive an IOException here its because the Selector is messed up. Let's rebuild - // the selector and retry. https://github.com/netty/netty/issues/8566 - // ... - continue; - } - - selectCnt++; - cancelledKeys = 0; - needsToSelectAgain = false; - final int ioRatio = this.ioRatio; - boolean ranTasks; - if (ioRatio == 100) { - try { - if (strategy > 0) { - processSelectedKeys(); - } - } finally { - // Ensure we always run tasks. - ranTasks = runAllTasks(); - } - } else if (strategy > 0) { - final long ioStartTime = System.nanoTime(); - try { - processSelectedKeys(); - } finally { - // Ensure we always run tasks. - final long ioTime = System.nanoTime() - ioStartTime; - ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio); - } - } else { - ranTasks = runAllTasks(0); // This will run the minimum number of tasks - } - } finally { - // Always handle shutdown even if the loop processing threw an exception. - } - } -} -``` -先简单解释一下上面的代码,部分细节后面再扣。`run()`方法里面是个死循环,大致是这样的,这里的描述并不完全准确,是这么个意思,`taskQueue`里面如果有**task**,就不断`poll`执行队列里的**task**,具体见`runAllTasks()`;否则,就`selector.select()`,若有IO事件,则通过`processSelectedKeys()`来处理。 - - -讲到这里,正好刚才不是往`taskQueue`里放了个`Runnable`吗,再贴一下上面那个`Runnable`的代码 -```java -new Runnable() { - @Override - public void run() { - register0(promise); - } -}; -``` - - -于是就要执行`Runnable`里面`register0(promise)`了。 - - -io.netty.channel.**AbstractChannel** -```java -private void register0(ChannelPromise promise) { - //(1)把ServerSocketChannel注册到了Selector上 - doRegister(); - - // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the - // user may already fire events through the pipeline in the ChannelFutureListener. - - //(2)触发pipeline中的ChannelHandler的handlerAdded()方法调用 - pipeline.invokeHandlerAddedIfNeeded(); - - safeSetSuccess(promise); - - //(3)触发pipeline中的ChannelInboundHandler的channelRegistered()方法调用 - pipeline.fireChannelRegistered(); - - // Only fire a channelActive if the channel has never been registered. This prevents firing - // multiple channel actives if the channel is deregistered and re-registered. - if (isActive()) { - if (firstRegistration) { - pipeline.fireChannelActive(); - } else if (config().isAutoRead()) { - // This channel was registered before and autoRead() is set. This means we need to begin read - // again so that we process inbound data. - // - // See https://github.com/netty/netty/issues/4805 - beginRead(); - } - } -} -``` -上面我按照自己的理解,在代码中加了少许注释,下面按照我注释的顺序依次解释一下。 - - -(1) `doRegister()` - - -io.netty.channel.nio.**AbstractNioChannel** -```java -@Override -protected void doRegister() throws Exception { - boolean selected = false; - for (;;) { - selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); - return; - } -} -``` -这里显然是把`ServerSocketChannel`注册到了`Selector`上。 - - -(2) `pipeline.invokeHandlerAddedIfNeeded()` - - -作用:触发`pipeline`中的`ChannelHandler`的`handlerAdded()`方法调用 - - -io.netty.channel.**DefaultChannelPipeline** -```java - final void invokeHandlerAddedIfNeeded() { - if (firstRegistration) { - firstRegistration = false; - // We are now registered to the EventLoop. It's time to call the callbacks for the ChannelHandlers, - // that were added before the registration was done. - callHandlerAddedForAllHandlers(); - } - } -``` -上面的注释清晰地告诉我们,现在`ServerSocketChannel`已经注册到`EventLoop`上,是时候该调用`Pipeline`中的`ChannelHandlers`。到这里,就能与上面**初始化ServerSocketChannel**对接起来了,猜测应该会触发上面的`ChannelInitializer`的调用。 - - -io.netty.channel.**DefaultChannelPipeline** -```java -private void callHandlerAddedForAllHandlers() { - final PendingHandlerCallback pendingHandlerCallbackHead; - synchronized (this) { - pendingHandlerCallbackHead = this.pendingHandlerCallbackHead; - // Null out so it can be GC'ed. - this.pendingHandlerCallbackHead = null; - } - - // This must happen outside of the synchronized(...) block as otherwise handlerAdded(...) may be called while - // holding the lock and so produce a deadlock if handlerAdded(...) will try to add another handler from outside - // the EventLoop. - PendingHandlerCallback task = pendingHandlerCallbackHead; - while (task != null) { - task.execute(); - task = task.next; - } -} -``` -这里需要先解释一下为什么又突然冒出来`PendingHandlerCallback`。是这样的,在`addLast(ChannelHandler... handlers)`时,实际上调了下面的方法。 - - -io.netty.channel.**DefaultChannelPipeline** -```java -public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) { - final AbstractChannelHandlerContext newCtx; - synchronized (this) { - newCtx = newContext(group, filterName(name, handler), handler); - - addLast0(newCtx); - - // If the registered is false it means that the channel was not registered on an eventLoop yet. - // In this case we add the context to the pipeline and add a task that will call - // ChannelHandler.handlerAdded(...) once the channel is registered. - if (!registered) { - newCtx.setAddPending(); - callHandlerCallbackLater(newCtx, true); - return this; - } - - EventExecutor executor = newCtx.executor(); - if (!executor.inEventLoop()) { - callHandlerAddedInEventLoop(newCtx, executor); - return this; - } - } - callHandlerAdded0(newCtx); - return this; -} -``` -看到上面的3行注释没有,就解释了上面的`PendingHandlerCallback`从哪里来的。翻译一下就是,在往`Pipeline`中添加`ChannelHandler`时,如果`Channel`还没有注册到`EventLoop`上,就将当前的`AbstractChannelHandlerContext`封装到`PendingHandlerCallback`里去,等着后面触发调用。 - - -回到正题,`PendingHandlerCallback.execute()`几经周折,会调用`ChannelHandler`的`handlerAdded()`,如下所示: - - -io.netty.channel.**AbstractChannelHandlerContext** -```java -final void callHandlerAdded() throws Exception { - // We must call setAddComplete before calling handlerAdded. Otherwise if the handlerAdded method generates - // any pipeline events ctx.handler() will miss them because the state will not allow it. - if (setAddComplete()) { - handler().handlerAdded(this); - } -} -``` -那么再回头看看`ChannelInitializer` - - -io.netty.channel.**ChannelInitializer** -```java - /** - * {@inheritDoc} If override this method ensure you call super! - */ -@Override -public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - if (ctx.channel().isRegistered()) { - if (initChannel(ctx)) { - removeState(ctx); - } - } -} - -private boolean initChannel(ChannelHandlerContext ctx) throws Exception { - if (initMap.add(ctx)) { // Guard against re-entrance. - initChannel((C) ctx.channel()); - return true; - } - return false; -} - - /** - * This method will be called once the {@link Channel} was registered. After the method returns this instance - * will be removed from the {@link ChannelPipeline} of the {@link Channel}. - * - * @param ch the {@link Channel} which was registered. - * @throws Exception is thrown if an error occurs. In that case it will be handled by - * {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close - * the {@link Channel}. - */ -protected abstract void initChannel(C ch) throws Exception; -``` -原来,最终会触发`initChannel`调用,所以上面**初始化ServerSocketChannel**时重写的`initChannel`会在这时执行。 -```java -p.addLast(new ChannelInitializer() { - @Override - public void initChannel(final Channel ch) { - final ChannelPipeline pipeline = ch.pipeline(); - ChannelHandler handler = config.handler(); - if (handler != null) { - pipeline.addLast(handler); - } - - ch.eventLoop().execute(new Runnable() { - @Override - public void run() { - pipeline.addLast(new ServerBootstrapAcceptor( - ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); - } - }); - } -}); -``` - - -这里的`initChannel`执行之后,此时`ChannelPipeline`大致如下: -```bash -head --> tail -``` -值得注意的是,此时`ServerBootstrapAcceptor`暂时并没有被放入`ChannelPipeline`中,而同样是放到了上面提到的`Queue taskQueue`队列中,如下: -```java -ch.eventLoop().execute(new Runnable() { - @Override - public void run() { - pipeline.addLast(new ServerBootstrapAcceptor( - ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); - } -}); -``` -至于`ServerBootstrapAcceptor`里面干了啥,等到后面再细说。 - - -来,继续。上面讲清楚了`doRegister()`和`pipeline.invokeHandlerAddedIfNeeded()`,接下来看`pipeline.fireChannelRegistered()`。 - - -(3) `pipeline.fireChannelRegistered()` - - -作用:触发`pipeline`中的`ChannelInboundHandler`的`channelRegistered()`方法调用 - - -还是往里面简单追一下源码。 - - -io.netty.channel.**AbstractChannelHandlerContext** -```java -static void invokeChannelRegistered(final AbstractChannelHandlerContext next) { - EventExecutor executor = next.executor(); - if (executor.inEventLoop()) { - next.invokeChannelRegistered(); - } else { - executor.execute(new Runnable() { - @Override - public void run() { - next.invokeChannelRegistered(); - } - }); - } -} - -private void invokeChannelRegistered() { - if (invokeHandler()) { - try { - // 这里触发了channelRegistered()方法调用 - ((ChannelInboundHandler) handler()).channelRegistered(this); - } catch (Throwable t) { - invokeExceptionCaught(t); - } - } else { - fireChannelRegistered(); - } -} -``` - - -到这里,`register0()`这个**task**就执行完了。但是还记得这个**task**执行过程中,又往`taskQueue`中添加了一个`Runnable`吗? -```java -new Runnable() { - @Override - public void run() { - pipeline.addLast(new ServerBootstrapAcceptor( - ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); - } -} -``` -此时会**poll**到新加的**task**,见如下代码: - - -io.netty.util.concurrent.**SingleThreadEventExecutor** -```java -protected boolean runAllTasks(long timeoutNanos) { - for (;;) { - safeExecute(task); - task = pollTask(); - if (task == null) { - lastExecutionTime = ScheduledFutureTask.nanoTime(); - break; - } - } - - afterRunningAllTasks(); - this.lastExecutionTime = lastExecutionTime; - return true; -} -``` - - -执行完这个新增的`Runnable`后,此时`ChannelPipeline`大致如下: -```bash -head --> ServerBootstrapAcceptor --> tail -``` -此时,`taskQueue`中的**task**都执行完了,EventLoop线程执行`selector.select()`,等待客户端的连接。 - - -到这里,Server端也就成功启动了。 - - -### Client端启动过程 - - -#### 创建Selector - - -与Server端完全一致。 - - -#### 创建SocketChannel - - -入口与Server端一样,不一样的地方在于Client端是`bootstrap.channel(NioSocketChannel.class)`,所以需要看`NioSocketChannel`的实现。这里也不必多说。 - - -#### 初始化SocketChannel - - -Client端的就比较简单了,如下: - -io.netty.bootstrap.**Bootstrap** -```java -@Override -void init(Channel channel) { - ChannelPipeline p = channel.pipeline(); - p.addLast(config.handler()); -} -``` - - -#### SocketChannel注册到Selector上 - - - -前面的过程与Server端基本一样,执行完`doRegister()`,执行`pipeline.invokeHandlerAddedIfNeeded()`时,没有Server端复杂(因为Server端`初始化SocketChannel`,加了个添加`ServerBootstrapAcceptor`到`ChannelPipeline`的task)。 - - -前面分析过,这个过程会触发`initChannel`调用,所以这时会执行用户编写的`ChannelInitializer`,也就是会执行`ch.pipeline().addLast(new NettyClientHandler())`,将用户编写的`NettyClientHandler`插入到`ChannelPipeline`中。 - - -#### 连接Server - - -注册成功后,会执行**连接Server**的回调。 - - -io.netty.bootstrap.**Bootstrap** -```java -private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) { - final ChannelFuture regFuture = initAndRegister(); - final Channel channel = regFuture.channel(); - - if (regFuture.isDone()) { - return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise()); - } else { - // Registration future is almost always fulfilled already, but just in case it's not. - final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); - regFuture.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - // Directly obtain the cause and do a null check so we only need one volatile read in case of a - // failure. - Throwable cause = future.cause(); - if (cause != null) { - // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an - // IllegalStateException once we try to access the EventLoop of the Channel. - promise.setFailure(cause); - } else { - // Registration was successful, so set the correct executor to use. - // See https://github.com/netty/netty/issues/2586 - promise.registered(); - doResolveAndConnect0(channel, remoteAddress, localAddress, promise); - } - } - }); - return promise; - } -} -``` -需要看`doResolveAndConnect0()`, 里面又调用的是`doConnect()` - - -io.netty.bootstrap.**Bootstrap** -```java -private static void doConnect( - final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) { - - // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up - // the pipeline in its channelRegistered() implementation. - final Channel channel = connectPromise.channel(); - channel.eventLoop().execute(new Runnable() { - @Override - public void run() { - if (localAddress == null) { - channel.connect(remoteAddress, connectPromise); - } else { - channel.connect(remoteAddress, localAddress, connectPromise); - } - connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); - } - }); -} -``` -最终调用的是: - - -io.netty.channel.socket.nio.**NioSocketChannel**#doConnect() -```java -@Override -protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { - boolean success = false; - try { - boolean connected = SocketUtils.connect(javaChannel(), remoteAddress); - if (!connected) { - selectionKey().interestOps(SelectionKey.OP_CONNECT); - } - success = true; - return connected; - } finally { - if (!success) { - doClose(); - } - } -} -``` -再看`SocketUtils.connect(javaChannel(), remoteAddress)` - - -io.netty.util.internal.**SocketUtils** -```java -public static boolean connect(final SocketChannel socketChannel, final SocketAddress remoteAddress) - throws IOException { - try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public Boolean run() throws IOException { - return socketChannel.connect(remoteAddress); - } - }); - } catch (PrivilegedActionException e) { - throw (IOException) e.getCause(); - } -} -``` -这里我们看到了熟悉的`socketChannel.connect(remoteAddress)`。 - - -### Server与Client通信 - - -上面详细介绍了Server端的启动过程,Client端的启动过程,Client也向Server发出了连接请求。这时再回过头来看Server端。 - - -Server端感知到了IO事件,会在io.netty.channel.nio.**NioEventLoop**的`run()`方法里,调用`processSelectedKeys()`,对于每个IO事件,最终调用的是`processSelectedKey()`来处理。 - - -io.netty.channel.nio.**NioEventLoop** -```java -private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { - final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); - try { - int readyOps = k.readyOps(); - // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise - // the NIO JDK channel implementation may throw a NotYetConnectedException. - if ((readyOps & SelectionKey.OP_CONNECT) != 0) { - // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking - // See https://github.com/netty/netty/issues/924 - int ops = k.interestOps(); - ops &= ~SelectionKey.OP_CONNECT; - k.interestOps(ops); - - unsafe.finishConnect(); - } - - // Process OP_WRITE first as we may be able to write some queued buffers and so free memory. - if ((readyOps & SelectionKey.OP_WRITE) != 0) { - // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write - ch.unsafe().forceFlush(); - } - - // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead - // to a spin loop - if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { - unsafe.read(); - } - } catch (CancelledKeyException ignored) { - unsafe.close(unsafe.voidPromise()); - } -} -``` -这里是**SelectionKey.OP_ACCEPT**,当然走的是`unsafe.read()` - - -io.netty.channel.nio.**AbstractNioMessageChannel** -```java -private final class NioMessageUnsafe extends AbstractNioUnsafe { - - private final List readBuf = new ArrayList(); - - @Override - public void read() { - assert eventLoop().inEventLoop(); - final ChannelConfig config = config(); - final ChannelPipeline pipeline = pipeline(); - final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); - allocHandle.reset(config); - - boolean closed = false; - Throwable exception = null; - try { - try { - do { - int localRead = doReadMessages(readBuf); - if (localRead == 0) { - break; - } - if (localRead < 0) { - closed = true; - break; - } - - allocHandle.incMessagesRead(localRead); - } while (allocHandle.continueReading()); - } catch (Throwable t) { - exception = t; - } - - int size = readBuf.size(); - for (int i = 0; i < size; i ++) { - readPending = false; - pipeline.fireChannelRead(readBuf.get(i)); - } - readBuf.clear(); - allocHandle.readComplete(); - pipeline.fireChannelReadComplete(); - } finally { - // ... - } - } -} -``` -这里面有很重要的两个方法,`doReadMessages(readBuf)`和`pipeline.fireChannelRead()` - - -io.netty.channel.socket.nio.**NioServerSocketChannel** -```java -@Override -protected int doReadMessages(List buf) throws Exception { - SocketChannel ch = SocketUtils.accept(javaChannel()); - try { - if (ch != null) { - buf.add(new NioSocketChannel(this, ch)); - return 1; - } - } catch (Throwable t) { - // ... - } - return 0; -} -``` - - -io.netty.util.internal.**SocketUtils** -```java -public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException { - try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public SocketChannel run() throws IOException { - return serverSocketChannel.accept(); - } - }); - } catch (PrivilegedActionException e) { - throw (IOException) e.getCause(); - } -} -``` -`serverSocketChannel`接受了Client端的连接后,将该`socketChannel`放到了`List`中。 - - -而后遍历该`List`,将每个`socketChannel`传入`pipeline.fireChannelRead()`中。 - -还记得当前`serverSocketChannel`的`ChannelPipeline`有哪些`ChannelHandler`吗? -```bash -head --> ServerBootstrapAcceptor --> tail -``` - - -接下来就需要重点看下`ServerBootstrapAcceptor`的逻辑 - - -io.netty.bootstrap.ServerBootstrap#**ServerBootstrapAcceptor** -```java -private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter { - - private final EventLoopGroup childGroup; - private final ChannelHandler childHandler; - private final Runnable enableAutoReadTask; - - ServerBootstrapAcceptor( - final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler, - Entry, Object>[] childOptions, Entry, Object>[] childAttrs) { - this.childGroup = childGroup; - this.childHandler = childHandler; - - enableAutoReadTask = new Runnable() { - @Override - public void run() { - channel.config().setAutoRead(true); - } - }; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - final Channel child = (Channel) msg; - - child.pipeline().addLast(childHandler); - - setChannelOptions(child, childOptions, logger); - setAttributes(child, childAttrs); - - childGroup.register(child).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - forceClose(child, future.cause()); - } - } - }); - } -} -``` -`ServerBootstrapAcceptor`将建立好连接的`socketChannel`注册到`workerGroup`中的某个`EventLoop`(或者说是`Selector`)上,而且将用户编写的`childHandler`加到了每个`socketChannel`的`ChannelPipeline`中。`ServerBootstrapAcceptor`相当于起了转发的作用,建立好连接后`Channel`实际的读写IO事件是由`workerGroup`中的`EventLoop`来处理。 - - -再回过头来,看Reactor模式的多Reactor版本(一主多从),不知道你是否能get到其中的含义? -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130172337275-1686188238.png) -注意:上面代码里的`childGroup`就是来自我们在写Server端`NettyServer`代码时定义的`workerGroup` -```java -EventLoopGroup workerGroup = new NioEventLoopGroup(); -``` -我觉得能坚持看到这个地方的朋友应该能明白,只是这里又啰嗦了一下。 - - -讲到这里,我觉得其实后面Client端的情况都不用讲了,已经很清晰了。不过为了文章的完整性,还是写下去比较好。 - - -Server端`accept`连接请求后,Client端此时同样也有了IO事件。同样还是走`processSelectedKey()`那个方法,不过执行的分支不一样。 -```java -int readyOps = k.readyOps(); -// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise -// the NIO JDK channel implementation may throw a NotYetConnectedException. -if ((readyOps & SelectionKey.OP_CONNECT) != 0) { - // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking - // See https://github.com/netty/netty/issues/924 - int ops = k.interestOps(); - ops &= ~SelectionKey.OP_CONNECT; - k.interestOps(ops); - - unsafe.finishConnect(); -} -``` -最终调用`doFinishConnect()`,如下: - - -io.netty.channel.socket.nio.**NioSocketChannel** -```java -@Override -protected void doFinishConnect() throws Exception { - if (!javaChannel().finishConnect()) { - throw new Error(); - } -} -``` -之后,Client端与Server端就可以通过`Channel`读写数据,通过`ChannelPipeline`中的`ChannelHandler`对数据`decode`、`compute`、`encode`。 -## 写在后面 - - -至此,本篇就大致讲清楚了Netty的Server端和Client端的整个启动并通信的过程以及如何对nio进行封装的。这里再贴一张在网络上流传较广的Netty工作原理图,相信此时再看这张图应该无比亲切吧。 -![](https://img2020.cnblogs.com/blog/1546632/202101/1546632-20210130172509631-223450187.png) - - -整个过程确实比较绕。但回过头再看,有一个清晰的思路,然后时刻记着与nio的代码做对比,多点耐心也还能坚持下去,另外遇到搞不明白的地方再配合`debug`,会轻松许多。最后,由于本人能力有限,文中如有错误的理解、不恰当的描述,欢迎指出! \ No newline at end of file diff --git a/ProblemResearch/data-from-mongodb-to-kafka.md b/ProblemResearch/data-from-mongodb-to-kafka.md deleted file mode 100644 index eea15d7..0000000 --- a/ProblemResearch/data-from-mongodb-to-kafka.md +++ /dev/null @@ -1,86 +0,0 @@ -# MongoDB -> kafka 高性能实时同步方案 - -## 写这篇博客的目的 -让更多的人了解 阿里开源的MongoShake可以很好满足mongodb到kafka高性能高可用实时同步需求(项目地址:`https://github.com/alibaba/MongoShake`,下载地址:`https://github.com/alibaba/MongoShake/releases`)。至此博客就结束了,你可以愉快地啃这个项目了。还是一起来看一下官方的描述: -> MongoShake is a universal data replication platform based on MongoDB's oplog. Redundant replication and active-active replication are two most important functions. 基于mongodb oplog的集群复制工具,可以满足迁移和同步的需求,进一步实现灾备和多活功能。 - -## 没有标题的标题 - -哈哈,有兴趣听我啰嗦的可以往下。最近,有个实时增量采集mongodb数据(`数据量在每天10亿条左右`)的需求,需要先调研一下解决方案。我分别百度、google了`mongodb kafka sync 同步 采集 实时`等 关键词,写这篇博客的时候排在最前面的当属`kafka-connect`(官方有实现`https://github.com/mongodb/mongo-kafka`,其实也有非官方的实现)那一套方案,我对kafka-connect相对熟悉一点(不熟悉的话估计编译部署都要花好一段时间),没测之前就感觉可能不满足我的采集性能需求,测下来果然也是不满足需求。后来,也看到了`https://github.com/rwynn/route81`,编译部署也较为麻烦,同样不满足采集性能需求。我搜索东西的时候一般情况下不会往下翻太多,没找到所需的,大多会尝试换关键词(包括中英文)搜搜,`这次可能也提醒我下次要多往下找找,说不定有些好东西未必排在最前面几个`。 - -之后在github上搜`in:readme mongodb kafka sync`,让我眼前一亮。 - -![github上搜索mongodb、kafka、sync关键词结果](https://img2018.cnblogs.com/blog/1546632/202002/1546632-20200218230148278-198252635.png) - -点进去快速读了一下readme,正是我想要的(后面自己实际测下来确实高性能、高可用,满足我的需求),官方也提供了MongoShake的[性能测试报告](https://github.com/alibaba/MongoShake/wiki/MongoShake-Performance-Document)。 - -这篇博客不讲(`也很大可能是笔者技术太渣,无法参透领会(●´ω`●)`)MongoShake的架构、原理、实现,如何高性能的,如何高可用的等等。就一个目的,希望其他朋友在搜索`mongodb kafka`时候,`MongoShake`的解决方案可以排在最前面。 - -## 初次使用MongoShake值得注意的地方 - -### 数据处理流程 -***v2.2.1之前的MongoShake版本处理数据的流程:*** - -MongoDB(数据源端,待同步的数据) -`-->`MongoShake(对应的是`collector.linux`进程,作用是采集) -`-->`Kafka(raw格式,未解析的带有header+body的数据) -`-->`receiver(对应的是`receiver.linux`进程,作用是解析,这样下游组件就能拿到比如解析好的一条一条的json格式的数据) -`-->`下游组件(拿到mongodb中的数据用于自己的业务处理) - -***v2.2.1之前MongoShake的版本解析入kafka,需要分别启collector.linux和receiver.linux进程,而且receiver.linux需要自己根据你的业务逻辑填充完整,然后编译出来,默认只是把解析出来的数据打个log而已*** - -`src/mongoshake/receiver/replayer.go`中的代码如图: - -![需要自己填充receiver逻辑的地方](https://img2018.cnblogs.com/blog/1546632/202002/1546632-20200219005031066-1197176266.png) - -详情见:[https://github.com/alibaba/MongoShake/wiki/FAQ#q-how-to-connect-to-different-tunnel-except-direct](https://github.com/alibaba/MongoShake/wiki/FAQ#q-how-to-connect-to-different-tunnel-except-direct) - -***v2.2.1版本MongoShake的`collector.conf`有一个配置项`tunnel.message`*** -```sh -# the message format in the tunnel, used when tunnel is kafka. -# "raw": batched raw data format which has good performance but encoded so that users -# should parse it by receiver. -# "json": single oplog format by json. -# "bson": single oplog format by bson. -# 通道数据的类型,只用于kafka和file通道类型。 -# raw是默认的类型,其采用聚合的模式进行写入和 -# 读取,但是由于携带了一些控制信息,所以需要专门用receiver进行解析。 -# json以json的格式写入kafka,便于用户直接读取。 -# bson以bson二进制的格式写入kafka。 -tunnel.message = json -``` -- 如果选择的`raw`格式,那么数据处理流程和上面之前的一致(MongoDB->MongoShake->Kafka->receiver->下游组件) -- 如果选择的是`json`、`bson`,处理流程为MongoDB->MongoShake->Kafka->下游组件 - -**v2.2.1版本设置为json处理的优点就是把以前需要由receiver对接的格式,改为直接对接,从而少了一个receiver,也不需要用户额外开发,降低开源用户的使用成本。** - -简单总结一下就是: -***raw格式能够最大程度的提高性能,但是需要用户有额外部署receiver的成本。json和bson格式能够降低用户部署成本,直接对接kafka即可消费,相对于raw来说,带来的性能损耗对于大部分用户是能够接受的。*** - -### 高可用部署方案 - -我用的是v2.2.1版本,高可用部署非常简单。`collector.conf`开启master的选举即可: -```sh -# high availability option. -# enable master election if set true. only one mongoshake can become master -# and do sync, the others will wait and at most one of them become master once -# previous master die. The master information stores in the `mongoshake` db in the source -# database by default. -# 如果开启主备mongoshake拉取同一个源端,此参数需要开启。 -master_quorum = true - -# checkpoint存储的地址,database表示存储到MongoDB中,api表示提供http的接口写入checkpoint。 -context.storage = database -``` -同时我checkpoint的存储地址默认用的是database,会默认存储在`mongoshake`这个db中。我们可以查询到checkpoint记录的一些信息。 -```sh -rs0:PRIMARY> use mongoshake -switched to db mongoshake -rs0:PRIMARY> show collections; -ckpt_default -ckpt_default_oplog -election -rs0:PRIMARY> db.election.find() -{ "_id" : ObjectId("5204af979955496907000001"), "pid" : 6545, "host" : "192.168.31.175", "heartbeat" : NumberLong(1582045562) } -``` -我在192.168.31.174,192.168.31.175,192.168.31.176上总共启了3个MongoShake实例,可以看到现在工作的是192.168.31.175机器上进程。自测过程,高速往mongodb写入数据,手动kill掉192.168.31.175上的collector进程,等192.168.31.174成为master之后,我又手动kill掉它,最终只保留192.168.31.176上的进程工作,最后统计数据发现,有重采数据现象,猜测有实例还没来得及checkpoint就被kill掉了。 \ No newline at end of file diff --git a/ProblemResearch/hbase-region-server-cannot-start.md b/ProblemResearch/hbase-region-server-cannot-start.md deleted file mode 100644 index 0913706..0000000 --- a/ProblemResearch/hbase-region-server-cannot-start.md +++ /dev/null @@ -1,26 +0,0 @@ -# 使用ClouderaManager管理的HBase的RegionServer无法启动排查 - -## 问题概述 -"新冠期间"远程办公,需要重新搭建一套ClouderaManager(CM)开发环境,一位测试同事发现HBase的RegionServer无法启动,在CM界面上启动总是失败,观察一下日志,也没有什么明显的报错。我就专门看了一下。 - -## 排查思路 -1. 因为有opentsdb在读写Hbase Region Server,我一开始怀疑RegionServer启动过程中在恢复一些数据,这个时候就有组件对它读写操作,可能压力较大起不来。后来停掉了opentsdb,依然如此,日志也没有明显报错,打着打着就断了,再看进程就没了。 - -2. 后来我在界面上又重启了一下,迅速 `jps -mlv`命令查看一下启动参数,这一看就明白了居然给的 `堆内存50MB`,难怪起不来,启动过程中应该就`OOM`了,很快,再执行一次`jps -mlv`命令 这个`HRegionServer`进程已经退出了。 - -3. 于是我在网上搜了一下,果然`ClouderaManager(CM)`给HBase默认堆内存50M,豁然开朗。 - -## 解决 - - -![修改HRegionServer堆内存配置](https://img2018.cnblogs.com/blog/1546632/202002/1546632-20200215132131196-898943847.png) - -根据实际情况修改一下HMaster、HRegionServer堆内存大小,在界面上重启,我这次用`jps -mlv`命令观察一下,配置生效了,然后看日志,正常启动中,至此,问题解决。 - -## 总结 - -有些时候 程序一启动就挂掉,而且没有什么明显报错日志,可能要观察一下程序的启动参数等。 -比如说内存给的太小,程序压根就不能正常启动(OOM异常退出); -或者内存给的太大,向操作系统申请内存失败直接被kill掉。 - - diff --git a/ProblemResearch/kafka-broker-already-registered.md b/ProblemResearch/kafka-broker-already-registered.md deleted file mode 100644 index b7acbde..0000000 --- a/ProblemResearch/kafka-broker-already-registered.md +++ /dev/null @@ -1,45 +0,0 @@ -# kafka启动报错"A broker is already registered on the path /brokers/ids/1"排查 - -## 问题 -kafka挂掉后,启动报错日志如下 -```sh -[2020-03-19 17:50:58,123] FATAL Fatal error during KafkaServerStartable startup. Prepare to shutdown (kafka.server.KafkaServerStartable) -java.lang.RuntimeException: A broker is already registered on the path /brokers/ids/1. This probably indicates that you either have configured a brokerid that is already in use, or else you have shutdown this broker and restarted it faster than the zookeeper timeout so it appears to be re-registering. - at kafka.utils.ZkUtils.registerBrokerInZk(ZkUtils.scala:408) - at kafka.utils.ZkUtils.registerBrokerInZk(ZkUtils.scala:394) - at kafka.server.KafkaHealthcheck.register(KafkaHealthcheck.scala:71) - at kafka.server.KafkaHealthcheck.startup(KafkaHealthcheck.scala:51) - at kafka.server.KafkaServer.startup(KafkaServer.scala:269) - at kafka.server.KafkaServerStartable.startup(KafkaServerStartable.scala:39) - at kafka.Kafka$.main(Kafka.scala:67) - at kafka.Kafka.main(Kafka.scala) -[2020-03-19 17:50:58,123] INFO [Kafka Server 1], shutting down (kafka.server.KafkaServer) -``` -## 分析 -从`This probably indicates that you either have configured a brokerid that is already in use`提示可知,zookeeper中可能已经注册了此broker id,正常情况下,你应该不会启动两个相同broker id的kafka server(除非你没注意弄错了使得两个kafka server用了相同的broker id) - -于是我用`$KAFKA`安装包下带有的`zookeeper` client 连接了zk server,看一下kafka broker的注册情况 - -```sh -$KAFKA/bin/zookeeper-shell.sh 192.168.0.1:2181 ls /brokers/ids -``` - -执行后显示 -```sh -Connecting to 192.168.0.1:2181 - -WATCHER:: - -WatchedEvent state:SyncConnected type:None path:null -[1, 2, 3] -``` -然而实际情况是,broker id为1的kafka server并没有启动起来。原因是这台机器之前因为卡死被`物理重启`,kafka broker没有正常下线,zk上还保留着它的broker id。 - -## 解决方案 -找到原因后,解决就很简单了,把注册在`zookeeper`上的这个broker id `delete`掉就行了 - -```sh -$KAFKA/bin/zookeeper-shell.sh 192.168.0.1:2181 delete /brokers/ids/1 -``` - -然后再启动观察日志就正常了。 diff --git a/Python/python-environment.md b/Python/python-environment.md deleted file mode 100644 index b0fff58..0000000 --- a/Python/python-environment.md +++ /dev/null @@ -1,213 +0,0 @@ -# Python开发环境搭建 - -好像任何一门编程语言都绕不过开发环境的搭建。比如说`Java`,初学者可能还没明白什么是`JDK`,但一般都会按照前辈们的步骤,先下载`JDK`,然后添加环境变量`JAVA_HOME`,再把`JAVA_HOME`的`bin`目录加入到环境变量`PATH`中,有过实际项目开发经验的朋友大多数紧接着就会安装依赖管理、项目构建工具,比如说国内常用的`Maven`等。`Golang`、`Scala`语言的开发环境搭建也都类似。至于用什么编辑器或者`IDE`来写代码,取决于语言特性或个人习惯,比如当前主流的`vim`、`VS Code`、`JetBrains`全家桶、`Eclipse`等等。 - - -使用`Python`语言的初学者建议直接安装`Anaconda`或者轻量级的`Miniconda`。 - -## 为什么需要Anaconda - -`Anaconda`官网:`https://www.anaconda.com/` - -> Anaconda 是一个用于科学计算的 Python 发行版,支持 Linux, Mac, Windows, 包含了众多流行的科学计算、数据分析的 Python 包。 - -介绍`Anaconda`之前必须要简单提一下`Conda`。官网:`https://docs.conda.io/en/latest/` - -> Conda is an open source package management system and environment management system that runs on Windows, macOS and Linux. Conda quickly installs, runs and updates packages and their dependencies. Conda easily creates, saves, loads and switches between environments on your local computer. It was created for Python programs, but it can package and distribute software for any language. - -`Conda`是`包管理器`。使用Python进行数据分析时,你会用到很多第三方的包,`Conda`可以很好地帮助你在计算机上安装和管理这些包,包括安装、卸载和更新包。 - -`Conda`也是`环境管理器`。比如你在A项目中用了`Python2`,而B项目需要使用`Python3`,同时安装两个`Python`版本可能会造成许多混乱和错误。这时候,`Conda`就可以帮助你为不同的项目建立不同的运行环境。再比如说很多项目使用的包版本不同,很难同时安装多个版本,此时,可以为不同版本建立不同环境,然后切换到对应版本的环境中工作。 - -`Anaconda`包括`Python`、`Conda`以及一大堆安装好的科学计算相关工具包,比如:`numpy`、`pandas`等等,开箱即用,非常方便。 - -`Miniconda`默认只包含`Python`和`Conda`。 - -换句话说,安装好了`Anaconda`,就相当于同时有了`Python`、`环境管理器`、`包管理器`以及一大堆开箱即用的`科学计算工具包`。 - -## 安装使用Anaconda - -### 安装Anaconda -安装非常简单,注意一下安装目录建议选择磁盘空间较大的地方。官网下载比较慢的话,可以选择清华大学镜像站下载(根据不同操作系统选择相应的包): -`https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/?C=M&O=D` - -安装成功后,命令行中敲`conda info`,会显示conda的版本和python的版本等详细信息;再敲`conda list`,会列出当前环境下所有安装的包。 - -### 换成国内镜像源 -`conda`和`pip`默认国外镜像源,所以每次安装模块`conda install xxx`或者`pip install xxx`的时候非常慢,换成国内的镜像源会显著加快模块安装速度。 - -- 修改`Conda`镜像源 - 详细操作见: - `https://mirror.tuna.tsinghua.edu.cn/help/anaconda/` - -- 修改`pip`镜像源 - `Linux`系统操作命令如下: -```sh -mkdir ~/.pip -cd ~/.pip -vim pip.conf -``` -添加中科大镜像源,内容如下: -```sh -[global] -index-url = https://pypi.mirrors.ustc.edu.cn/simple/ -``` - -不知道你有没有疑问,既然有了`Conda`包管理器,为什么`Anaconda`环境中,可能还需要用`pip`安装包呢? - -因为尽管在`Anaconda`下我们可以很方便的使用`conda install xxx`来安装我们需要的依赖,但是`Anaconda`本身只提供部分包,远没有`pip`提供的包多,有时`conda`无法安装我们需要的包,此时可能需要用`pip`将其装到`conda`环境里。 - -那么`Anaconda`环境下`pip`命令安装的包在哪里呢?会不会影响其他环境呢? - -首先要确保用的是本环境的`pip`,这样`pip install xxx`时,包才会安装到本环境中。`Linux`系统可以使用`which pip`来查看当前使用的`pip`是哪个环境的`pip`。 - -另外需要注意一下:安装特定版本的包,`conda`用`=`,`pip`用`==`。举例来说: -```python -conda install xxx=1.0.0 -pip install xxx==1.0.0 -``` - -### 使用Anaconda - -安装好了,默认是在`base`虚拟环境下,此时我们从`base`环境复制一份出来,在新环境里工作。 - -```sh -# 复制base环境, 创建test环境 -conda create --name test --clone base - -# 激活test环境 -conda activate test -``` -取消Conda默认激活`base`虚拟环境 -```sh -conda config --set auto_activate_base false -``` - - -再列出我本机的所有环境,如下,可见当前有2个环境,当前激活的是`test`环境: -```sh -(test) ➜ ~ conda info -e -# conda environments: -# -base /Volumes/300g/opt/anaconda3 -test * /Volumes/300g/opt/anaconda3/envs/test -``` - -`Anaconda`默认安装了`jupyter`,命令行输入: -```sh -jupyter notebook -``` -此时会自动弹出浏览器窗口打开`Jupyter Notebook`网页,默认为`http://localhost:8888`。下面会简单介绍一下`Jupyter Notebook`。 - -这里顺便贴一下`conda`一些常用命令: - -- 虚拟环境管理 - -```bash -# 创建环境,后面的python=3.6是指定python的版本 -conda create --name env_name python=3.6 - -# 创建包含某些包的环境(也可以加上版本信息) -conda create --name env_name python=3.7 numpy scrapy - -# 激活某个环境 -conda activate env_name - -# 关闭某个环境 -conda deactivate - -# 复制某个环境 -conda create --name new_env_name --clone old_env_name - -# 删除某个环境 -conda remove --name env_name --all - -# 生成需要分享环境的yml文件(需要在虚拟环境中执行) -conda env export > environment.yml - -# 别人在自己本地使用yml文件创建虚拟环境 -conda env create -f environment.yml -``` - -- 包管理 - -```bash -# 列出当前环境下所有安装的包 -conda list - -# 列举一个指定环境下的所有包 -conda list -n env_name - -# 查询库 -conda search scrapys - -# 安装库安装时可以指定版本例如:(scrapy=1.5.0) -conda install scrapy - -# 为指定环境安装某个包 -conda install --name target_env_name package_name - -# 更新安装的库 -conda update scrapy - -# 更新指定环境某个包 -conda update -n target_env_name package_name - -# 更新所有包 -conda update --all - -# 删除已经安装的库 -conda remove scrapy - -# 删除指定环境某个包 -conda remove -n target_env_name package_name -``` -更多命令请查看官方文档或者查询帮助命令: -```sh -conda --help - -conda install --help -``` - -## 使用Jupyter Notebook - -`Jupyter`源于2014年的`ipython`项目,逐渐发展为支持跨所有编程语言的交互式数据科学和科学计算。`Jupyter Notebook`,原名`IPython Notebook`,是`IPython`的加强网页版,一个开源Web应用程序,是一款程序员和科学工作者的编程/文档/笔记/展示软件。上一篇博客[一文上手Python3](https://www.cnblogs.com/bytesfly/p/python.html)就是用`Jupyter Notebook`写的。效果如下: -![](http://img2020.cnblogs.com/blog/1546632/202104/1546632-20210424235722527-129030812.png) - -`Jupyter Notebook`可以实时运行代码、渲染`Markdown`,将代码、文本说明和可视化整合在一起,适合数据分析领域的探索性工作,可迭代式地改进代码来改进解决方法。 - -`Jupyter Notebook`中一对`In Out`会话被视作一个代码单元,称为`cell`。如果`cell`行号前有`*`,表示代码正在运行中。 - -`Jupyter Notebook`支持两种模式: -- 编辑模式(`Enter`) - 命令模式下回车`Enter`或鼠标双击`cell`进入编辑模式 - -- 命令模式(`Esc`) - 按`Esc`退出编辑,进入命令模式 - -熟练在这两种模式下工作,再结合一些常用的快捷键,写代码以及文档的效率会大大提高。 - -在`Jupyter Notebook`中,可以使用`?`在另一个窗口中显示文档。 - -例如,`len?`将创建与`help(len)`几乎相同的内容,并在浏览器底部窗口中显示它。 - -![](https://img2020.cnblogs.com/blog/1546632/202110/1546632-20211014113111826-446402592.png) - -此外,如果使用两个问号,如`my_sum??`,将显示实现该函数的`Python`代码。 - -![](https://img2020.cnblogs.com/blog/1546632/202110/1546632-20211014113933680-341664686.png) - -### 安装`jupyter_contrib_nbextensions`库 - -注意:先关闭jupyter后台服务,然后执行下面的命令 - -```python -pip install jupyter_contrib_nbextensions - -jupyter contrib nbextension install --user --skip-running-check -``` -重启后,勾选需要的选项如下: -![](https://img2020.cnblogs.com/blog/1546632/202105/1546632-20210504155939115-948051826.png) - -此时就有了代码提示等功能,如下: -![](https://img2020.cnblogs.com/blog/1546632/202105/1546632-20210504160247554-1992201177.png) diff --git a/TODO/some-blogs.md b/TODO/some-blogs.md deleted file mode 100644 index b4b9073..0000000 --- a/TODO/some-blogs.md +++ /dev/null @@ -1,26 +0,0 @@ - -## AI - -[《动手学深度学习》(PyTorch版)](https://tangshusen.me/Dive-into-DL-PyTorch/) - - -## Java - -[https://www.graalvm.org/](https://www.graalvm.org/) - -[https://quarkus.io/](https://quarkus.io/) - -[史上最详细的JDK1.8 HashMap源码解析](https://joonwhee.blog.csdn.net/article/details/78996181?utm_medium=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.control) - -## 开源项目 - - -## 个人站点 - -[千寻主页](https://www.chihiro.org.cn/) - -[小四先生的笔记](https://www.zxiaosi.cn/) - -## 数据收集 - -[最全中华古诗词数据库](https://github.com/chinese-poetry/chinese-poetry) diff --git a/Tool/about-git.md b/Tool/about-git.md deleted file mode 100644 index 48d94b4..0000000 --- a/Tool/about-git.md +++ /dev/null @@ -1,231 +0,0 @@ -# Git使用汇总 - - -git就不用多说了,有些配置或者不常用的命令容易忘,这里随手做个记录。 - -## git常用命令 -```bash -# 只克隆最近1次commit -git clone --depth=1 repo_url.git - -# example: -# git clone -b release-1.9 --depth=1 git@github.com:apache/flink.git - -# 添加远程仓库地址 -git remote add itwild git@github.com:itwild/rewild.git - -# 查看远程仓库地址 -git remote -v - -# 将本地当前分支推送到远程itwild仓库的master分支 -git push itwild master - -# 强推(需要关闭对分支的保护),该操作需要谨慎 -git push -f itwild master - -# 修订上次的提交 -git commit --amend - -# 修订上次的提交时间 -git commit --amend --date="1 minute ago" - -# 指定commit时间 -git commit --date="10 day ago" -m "Your commit message" - -# 调整前几次的提交 -git rebase -i HEAD~2 - -# 从itwild远程仓库拉取最新的代码(仅仅是拉取代码,并没有参与本地分支merge或者rebase操作) -git fetch itwild - -# 展示本地分支的提交历史 -git log - -# 展示远程分支itwild/master的提交历史 -git log itwild/master - -# 可取代merge操作,避免产生不必要的commit,破坏提交树的美观 -# 找到两条分支的共同祖先,然后将共同祖先后不同的提交,回放一遍。 -git rebase itwild/master - -# 本地所有修改的,没有的提交的,都返回到原来的状态 -git checkout . -# 把所有没有提交的修改暂存到stash里面。可用git stash pop恢复 -git stash - -# 返回到某个commit,不保留修改 -git reset --hard commit_id - -# 返回到某个commit,保留修改 -git reset --soft commit_id -``` - -从工作目录中删除所有没有tracked过的文件 -```bash -git clean 参数 - -n Don't actually remove anything, just show what would be done - -f --force If the Git configuration variable clean.requireForce is not set to false, git clean will refuse to delete files or directories unless given -f, -n or -i. Git will refuse to delete directories with .git sub directory or file - unless a second -f is given. - -d Remove untracked directories in addition to untracked files -``` - -cherry-pick某个提交 -```bash -git cherry-pick [] ... - -常用options: - --quit 退出当前的chery-pick序列 - --continue 继续当前的chery-pick序列 - --abort 取消当前的chery-pick序列,恢复当前分支 - -n, --no-commit 不自动提交 - -e, --edit 编辑提交信息 -``` - -## git tag - -```bash -# https://github.com/apache/flink/tags - -# https://www.liaoxuefeng.com/wiki/896043488029600/902335479936480 - -# 本地打tag -git tag -a release-1.11.2 -m 'Apache Flink 1.11.2' - -# 删除一个本地标签 -git tag -d - -# 推送一个本地标签到远程仓库 -git push itwild - -# 推送全部未推送过的本地标签 -git push itwild --tags - -# 删除一个远程标签 -git push itwild :refs/tags/ -``` - -## git子模块(git submodule) -项目中可能会使依赖一些公共库或者其他team维护的其他项目,git submodule就可以很方便解决这样的场景。 -使用子模块后,不必负责子模块的维护,只需要在必要的时候同步更新子模块即可。 -```bash -# 递归克隆包含子模块的项目 -git clone --recursive - -# 添加子模块 -git submodule add [repo] - -git submodule init -git submodule update --init --recursive -git submodule update --remote --recursive - -git submodule foreach 'git pull' -git submodule foreach 'git checkout -b featureA' -git submodule foreach 'git diff' -``` - -## git配置多个SSH-Key -参考:https://my.oschina.net/stefanzhlg/blog/529403 - -日常工作中会遇到公司有个gitlab,还有些自己的一些项目放在github上。这样就导致我们要配置不同的ssh-key对应不同的环境。具体操作如下: - -1. 生成一个公司用的SSH-Key -```bash -ssh-keygen -t rsa -C "youremail@yourcompany.com" -f ~/.ssh/gitlab-rsa -``` -在~/.ssh/目录会生成gitlab-rsa和gitlab-rsa.pub私钥和公钥。 我们将gitlab-rsa.pub中的内容粘帖到公司gitlab服务器的SSH-key的配置中。 - -2. 生成一个github用的SSH-Key -```bash -ssh-keygen -t rsa -C "youremail@your.com" -f ~/.ssh/github-rsa -``` -在~/.ssh/目录会生成github-rsa和github-rsa.pub私钥和公钥。 我们将github-rsa.pub中的内容粘帖到github服务器的SSH-key的配置中。 - -3. 添加私钥 -```bash -ssh-add ~/.ssh/gitlab-rsa -ssh-add ~/.ssh/github-rsa -``` -如果执行ssh-add时提示"Could not open a connection to your authentication agent",可以现执行命令:`ssh-agent bash`,然后再运行ssh-add命令。 -```bash -# 可以通过 ssh-add -l 来确私钥列表 -ssh-add -l -# 可以通过 ssh-add -D 来清空私钥列表 -ssh-add -D -``` - -4. 修改配置文件 - -在 ~/.ssh 目录下新建一个config文件 -```bash -touch config -``` -添加内容: -```bash -# gitlab -Host gitlab.com - HostName gitlab.com - PreferredAuthentications publickey - IdentityFile ~/.ssh/gitlab-rsa -# github -Host github.com - HostName github.com - PreferredAuthentications publickey - IdentityFile ~/.ssh/github-rsa -``` - -5. 测试 -```bash -# test your company gitlab -ssh -T git@yourcompany.com -# test github -ssh -T git@github.com -``` - -## git对某个项目单独设置用户名/邮箱 -1. 进入项目.git文件夹,然后执行如下命令分别设置用户名和邮箱 -```bash -git config user.name "bytesfly" -git config user.email "bytesfly@example.com" -#使用vim作为编辑器 -git config --global core.editor "vim" -``` -2. 然后可以查看生成的config文件 -```bash -cat config -``` - -## git代理 - -取消代理: -```bash -git config --global --unset http.proxy -git config --global --unset https.proxy -``` - -设置代理: -```bash -git config --global http.proxy 'socks5://127.0.0.1:1080' -git config --global https.proxy 'socks5://127.0.0.1:1080' -``` - -仅代理`GitHub`: -```bash -git config --global http.https://github.com.proxy socks5://127.0.0.1:1080 - -# 取消代理 -git config --global --unset http.https://github.com.proxy -``` -补充: -如何自测本地代理是否有效(当然也可能端口有误): -```bash -curl -x socks5h://127.0.0.1:1080 www.google.com -``` -> In a proxy string, socks5h:// and socks4a:// mean that the hostname is resolved by the SOCKS server. socks5:// and socks4:// mean that the hostname is resolved locally - -也就是说: -- `socks5`适合本地能够解析目标主机域名(比如`github.com`)但是访问速度慢,来提高下载速度 -- `socks5h`用与本地不能解析目标主机域名(比如`google`),由代理服务器解析目标主机域名 - -```bash -export ALL_PROXY=socks5h://127.0.0.1:1080 -``` \ No newline at end of file diff --git a/Tool/awesome-sites.md b/Tool/awesome-sites.md deleted file mode 100644 index 8c3cd5e..0000000 --- a/Tool/awesome-sites.md +++ /dev/null @@ -1,229 +0,0 @@ -# 实用网站与工具 - -## 网站 - -### 翻译 - -DeepL翻译:[https://www.deepl.com/translator](https://www.deepl.com/translator) - -谷歌翻译:[https://translate.google.cn/](https://translate.google.cn/) - -### 技术博客 - -美团技术团队:[https://tech.meituan.com/](https://tech.meituan.com/) - -有赞技术团队:[https://tech.youzan.com/](https://tech.youzan.com/) - -淘系技术团队:[https://tech.taobao.org/](https://tech.taobao.org/) - -小米信息部技术团队:[https://xiaomi-info.github.io/](https://xiaomi-info.github.io/) - -NLP技术:[https://www.hanlp.com/](https://www.hanlp.com/) - -码农场-自然语言处理、机器学习算法:[https://www.hankcs.com/](https://www.hankcs.com/) - -SegmentFault论坛:[https://segmentfault.com/](https://segmentfault.com/) - -过往记忆大数据:[https://www.iteblog.com/](https://www.iteblog.com/) - -分享GitHub上有趣、入门级的开源项目:[https://hellogithub.com/](https://hellogithub.com/) - -周志明:[https://icyfenix.cn/](https://icyfenix.cn/) - -代码随想录: [https://programmercarl.com/](https://programmercarl.com/) - -皮皮鲁的科技星球(高性能计算、大数据和机器学习):[https://lulaoshi.info/](https://lulaoshi.info/) - -James D Bloom - Blog:[https://jamesdbloom.com/](https://jamesdbloom.com/) - -Ha0's Home:[https://t.hao0.me/](https://t.hao0.me/) - -盖若:[https://www.gairuo.com/](https://www.gairuo.com/) - -桔子code:[http://www.juzicode.com/](http://www.juzicode.com/) - -### Cloud Native - -[https://landscape.cncf.io/images/landscape.png](https://landscape.cncf.io/images/landscape.png) - -[https://landscape.cncf.io](https://landscape.cncf.io) - -### JVM、GC相关 - -Some interesting links concerning garbage collection:[https://github.com/chewiebug/GCViewer/wiki/Links](https://github.com/chewiebug/GCViewer/wiki/Links) - -list of links to gc log analysers.:[http://fasterj.com/tools/gcloganalysers.shtml](http://fasterj.com/tools/gcloganalysers.shtml) - -GCeasy:[https://gceasy.io/](https://gceasy.io/) - -GCViewer:[https://github.com/chewiebug/GCViewer](https://github.com/chewiebug/GCViewer) - -GCPlot - All-in-one JVM GC Logs Analyzer:[https://github.com/GCPlot/gcplot](https://github.com/GCPlot/gcplot) - -### 在线文档 - -一个神奇的文档网站生成器:[https://docsify.js.org/#/zh-cn/](https://docsify.js.org/#/zh-cn/) - -Spring官方文档:[https://spring.io/projects/spring-framework](https://spring.io/projects/spring-framework) - -Alibaba开源的Java诊断工具:[https://alibaba.github.io/arthas/](https://alibaba.github.io/arthas/) - -Hutool:[https://hutool.cn/docs/](https://hutool.cn/docs/) - -apachecn:[http://docs.apachecn.org/](http://docs.apachecn.org/) - -面试哥-各类在线文档:[http://www.mianshigee.com/tutorial/](http://www.mianshigee.com/tutorial/) - - -### 开源项目 - -apache开源项目:[https://apache.org/index.html#projects-list](https://apache.org/index.html#projects-list) - -apache孵化项目:[https://incubator.apache.org/](https://incubator.apache.org/) - -openjdk工具:[http://openjdk.java.net/projects/code-tools/jmh/](http://openjdk.java.net/projects/code-tools/jmh/) - -openjdk github:[https://github.com/openjdk](https://github.com/openjdk) - -flink-ecosystem:[https://flink-packages.org/](https://flink-packages.org/) - -### 开发工具 - -API 文档、调试、Mock、测试一体化协作平台:[https://www.apifox.cn](https://www.apifox.cn) - -新一代效率工具平台: [https://u.tools/](https://u.tools/) - - -### 软件下载 - -apache软件安装包:[http://archive.apache.org/dist/](http://archive.apache.org/dist/) - -apache软件安装包镜像:[http://www.apache.org/dyn/closer.cgi](http://www.apache.org/dyn/closer.cgi) - -kafka-manager安装包下载: -- [https://blog.wolfogre.com/posts/kafka-manager-download/](https://blog.wolfogre.com/posts/kafka-manager-download/) -- [https://github.com/yahoo/CMAK/releases](https://github.com/yahoo/CMAK/releases) - -Mac软件下载:[https://macwk.com/](https://macwk.com/)、[https://xclient.info/](https://xclient.info/)、[https://www.zhinin.com/](https://www.zhinin.com/) - -github加速下载:[https://gh.msx.workers.dev/](https://gh.msx.workers.dev/) - -github镜像:[https://hub.fastgit.org/](https://hub.fastgit.org/) - -PPT模板:[https://www.ypppt.com/](https://www.ypppt.com/) - -### 在线工具 - -截取并合并音乐: [https://audio-joiner.com/cn/](https://audio-joiner.com/cn/) - -词云工具:[https://wordart.com/](https://wordart.com/) - -毛笔字生成器: [https://www.zhenhaotv.com/](https://www.zhenhaotv.com/) - -艺术字体: [https://www.qt86.com/](https://www.qt86.com/) - -360查字体:[https://fonts.safe.360.cn/](https://fonts.safe.360.cn/) - -代码在线运行:[https://tool.lu/coderunner](https://tool.lu/coderunner) - -markdown表格工具:[https://tool.lu/tables](https://tool.lu/tables) - -图结构编辑工具:[https://csacademy.com/app/graph_editor/](https://csacademy.com/app/graph_editor/) - -解析几何作图:[https://csacademy.com/app/geometry_widget/](https://csacademy.com/app/geometry_widget/) - -文本比较工具(类似于git diff):[https://csacademy.com/app/diffing_tool/](https://csacademy.com/app/diffing_tool/) - -echarts图表展示工具:[https://echarts.apache.org/examples/zh/index.html](https://echarts.apache.org/examples/zh/index.html) - -蚂蚁金服数据可视化解决方案:[https://antv.vision](https://antv.vision) - -在线markdown作图:[https://markvis-editor.js.org/](https://markvis-editor.js.org/) - -牛客网:[https://www.nowcoder.com/](https://www.nowcoder.com/) - -在线刷题:[https://leetcode.com/](https://leetcode.com/)、[https://leetcode-cn.com/](https://leetcode-cn.com/) - -让微信公众号排版变Nice:[https://www.mdnice.com/](https://www.mdnice.com/) - -数据结构可视化工具:[https://www.cs.usfca.edu/~galles/visualization/Algorithms.html](https://www.cs.usfca.edu/~galles/visualization/Algorithms.html) - -processon在线作图:[https://www.processon.com/](https://www.processon.com/) - -其他作图:[https://app.diagrams.net/](https://app.diagrams.net/) - -百度脑图:[https://naotu.baidu.com/](https://naotu.baidu.com/) - -免费JSON接口:[http://www.bejson.com/knownjson/webInterface/](http://www.bejson.com/knownjson/webInterface/) - -制作banner:[http://patorjk.com/software/taag/](http://patorjk.com/software/taag/)、[https://www.bootschool.net/ascii](https://www.bootschool.net/ascii)、[http://www.network-science.de/ascii/](http://www.network-science.de/ascii/) - -根据图片转为文本:[https://www.degraeve.com/img2txt.php](https://www.degraeve.com/img2txt.php) - -LOGO设计-丢盖网:[http://www.diugai.com/](http://www.diugai.com/) - -消除图片背景:[https://www.remove.bg/zh](https://www.remove.bg/zh) - -浏览器脚本:[https://greasyfork.org/zh-CN/scripts](https://greasyfork.org/zh-CN/scripts) - -微信公众号辅助工具:[https://yiban.io/](https://yiban.io/) - -配色:[https://colordrop.io/](https://colordrop.io/)、[https://coolors.co/palettes/trending](https://coolors.co/palettes/trending) - -手绘示意图:[https://github.com/excalidraw/excalidraw](https://github.com/excalidraw/excalidraw)、[https://excalidraw.com/](https://excalidraw.com/) - -图片素材库:[https://pixabay.com/zh/](https://pixabay.com/zh/)、[https://www.pexels.com/zh-cn/](https://www.pexels.com/zh-cn/) - -图片封面在线设计:[https://kt.fkw.com/](https://kt.fkw.com/)、[https://www.chuangkit.com/](https://www.chuangkit.com/) - -### 实用网址 - -查看域名最快响应IP:[https://www.ipaddress.com/](https://www.ipaddress.com/) - -### 趋势 - -stackoverflow技术趋势: [https://insights.stackoverflow.com/trends](https://insights.stackoverflow.com/trends) - -百度指数:[http://index.baidu.com/v2/index.html](http://index.baidu.com/v2/index.html) - -谷歌趋势:[https://trends.google.com/](https://trends.google.com/) - -github热门:[https://github.com/trending](https://github.com/trending) - -## 工具 - -新一代多系统启动U盘解决方案: [https://www.ventoy.net/](https://www.ventoy.net/) - -### Mac - -制作纯文本流程图工具Graph::Easy: [http://bloodgate.com/perl/graph/manual/index.html](http://bloodgate.com/perl/graph/manual/index.html) -```bash -brew install graphviz -cpan -sudo cpan Graph:Easy -``` - -好用的剪贴板工具:[https://github.com/Clipy/Clipy/releases](https://github.com/Clipy/Clipy/releases) - -局域网屏幕分享: [https://deskreen.com/](https://deskreen.com/) - -好用的截图工具:[https://www.snipaste.com/](https://www.snipaste.com/) - -腾讯的截图、录屏工具:[https://jietu.qq.com/](https://jietu.qq.com/) - -### Linux - -### IDEA插件 - -- Maven Helper -- Lombok -- Json2Pojo with Lombok -- BashSupport -- MarkdownSupport -- CamelCase -- SonarLint -- Alibaba Java Coding Guidelines -- jclasslib Bytecode viewer -- 10302-asm-bytecode-viewer -- Translation - diff --git a/Tool/github-page-docsify.md b/Tool/github-page-docsify.md deleted file mode 100644 index ad1864f..0000000 --- a/Tool/github-page-docsify.md +++ /dev/null @@ -1,65 +0,0 @@ -# 使用GitHub Pages + docsify快速搭建一个站点 - -话不多说,先看效果: [https://bytesfly.github.io/blog](https://bytesfly.github.io/blog) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210918223015163-462918017.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210918222850884-1575341834.png) - -## 为什么需要一个站点 - - - -肯定有人会问,既然有类似 [博客园](https://www.cnblogs.com/) 这样优秀的平台来写博客,为什么还需要自己搭建站点呢? - -- 放在`GitHub`上托管,可以使用`Git`追踪博客内容的变更,就像维护代码一样,更加清晰明了,数据也不会丢失。 -- 大多优秀的开源项目,官方文档也很正式,如果用博客园来写貌似有点不合适,此时就需要一个独立的官方文档站点。 -- 如果你想免费搭建属于自己的个人站点,甚至用于一个公司、组织的官网,`GitHub Pages`也是一个不错的选择。 - -当然,对于我来说,我虽然会选择搭建一个属于自己的个人站点来记录成长的点滴,但同样还是会继续使用博客园。 因为,社区的力量很重要。 - -> 博客园的使命是帮助开发者用代码改变世界。 - -这里,再次感谢博客园团队不忘初心,专注于为开发者打造一个纯净的技术交流社区,推动并帮助开发者通过互联网分享知识,从而让更多开发者从中受益。 - - - -## 快速搭建 - - - -快速搭建非常简单,这里假定你已经有了`GitHub`账号,没有的话,注册一下。 - -- 第一步:`Fork`我的当前博客仓库,即 [https://github.com/bytesfly/blog](https://github.com/bytesfly/blog) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210918224423979-1304989038.png) - -- 第二步:在刚`Fork`的仓库设置(`Settings`)页面开启`GitHub Pages`功能 - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210918225610667-719903917.png) - -然后,你就可以打开`https://.github.io/blog`看看效果了。接下来,不用我说了吧,`clone`自己的`blog`仓库,在本地修改你的相关信息,添加你的博客文章,`push`到`GitHub`,刷新页面(浏览器可能有缓存),即可更新到最新的提交。 - - - -再看一下整个过程,是不是与日常新建项目写代码没啥区别?对,就这么简单,你只需要专注于写你的博客内容(`Markdown`),而且用了`Git`可以追踪所有的内容变更,这样你也不需要把写到一半的博客保存为文件传来传去,可以提交到`GitHub`,之后随时随地拉取最新的提交继续创作。 - - - -## 简单说明 - - - -`docsify`官方文档:[https://docsify.js.org/#/zh-cn/](https://docsify.js.org/#/zh-cn/) - -> `docsify`可以快速帮你生成文档网站。不同于 GitBook、Hexo 的地方是它不会生成静态的 .html 文件,所有转换工作都是在运行时。如果你想要开始使用它,只需要创建一个 index.html 就可以开始编写文档并直接部署在`GitHub Pages`。 - -所以,上面也提到,你只需要专注于写你的博客内容(`Markdown`),这对于只懂后端的程序员非常友好。当然,如果你了解前端的话,可以改`css`进一步美化,添加其他`js`插件让网站更加酷炫。 - - - -上面你看到的博客样式,是基于[`https://notebook.js.org`](https://notebook.js.org)修改的,在 [`关于本站`](https://bytesfly.github.io/blog/#/about/) 的致谢中也有明确说明。 - - - -一直以来可能有些朋友认为使用`GitHub Pages`搭建网站麻烦丑陋且不好维护,希望读完这篇能让你眼前一亮。 diff --git a/Tool/how-to-adjust-jd-gui-fontsize.md b/Tool/how-to-adjust-jd-gui-fontsize.md deleted file mode 100644 index 0b36232..0000000 --- a/Tool/how-to-adjust-jd-gui-fontsize.md +++ /dev/null @@ -1,40 +0,0 @@ -# MacOS如何调整JD-GUI反编译工具字体大小 - -how to change the fontsize of JD-GUI in MacOS? -MacOS如何调整JD-GUI反编译工具字体大小? - -## 问题描述 -JD-GUI是一款比较好用的反编译工具,不小心碰到什么东西把字体变得很小,代码都无法看清。在界面好像没找到调整字体大小的设置,双支、三指放大都不奏效。 -这里记录一下解决方案,让遇到同样问题的朋友少走弯路。 - -## 解决方法 - -Google搜索`jd gui font`排在第一位的链接就解决我的问题。`https://github.com/java-decompiler/jd-gui/issues/26` - -```bash -# 我的是在这个目录,或者用find去搜jd-gui-1.4.1.jar或者jd-gui.cfg -cd /Applications/JD-GUI.app/Contents/Resources/Java - -vim jd-gui.cfg - -# 找到12,我设置为12,然后关掉JD-GUI,重新打开JD-GUI字体大小就正常了 -``` -随手记录,方便你我他。 - -## 2021.07.13更新 - -评论中有朋友说在GUI界面上也可以设置字体,如下: -Help -> Preferences -> Appearance -> Font size - -我在`Deepin Linux`系统 下载最新版本试了下,确实有设置字体的选项。另外,观察了一下,也在当前用户(`home`)目录下的`.config`目录中看到了`jd-gui.cfg`文件,如下: -```bash -➜ ~ realpath .config/jd-gui.cfg -/home/bytesfly/.config/jd-gui.cfg -➜ ~ cat .config/jd-gui.cfg | grep font - 12 -➜ ~ - -``` - -顺便也把最新版本下载链接放在这: -[https://github.com/java-decompiler/jd-gui/releases](https://github.com/java-decompiler/jd-gui/releases) \ No newline at end of file diff --git a/Tool/image-to-latex.md b/Tool/image-to-latex.md deleted file mode 100644 index 7b1b1b4..0000000 --- a/Tool/image-to-latex.md +++ /dev/null @@ -1,183 +0,0 @@ -# 百闻不如一试——公式图片转Latex代码 - -写博客时,数学公式的编辑比较占用时间,在上一篇中详细介绍了如何在`Markdown`中编辑数学符号与公式。 - -[https://www.cnblogs.com/bytesfly/p/markdown-formula.html](https://www.cnblogs.com/bytesfly/p/markdown-formula.html) - - - -当然,有时候我们仅仅是想把现成的公式搬到`markdown`中来编辑,此时如果有工具能把公式截图直接解析成`Latex`代码就方便了。 - -刚好这几天看到好几个微信公众号都在推送`image-to-latex`这个开源项目: - -[https://github.com/kingyiusuen/image-to-latex](https://github.com/kingyiusuen/image-to-latex) - -> Convert images of LaTex math equations into LaTex code. - -![](https://blog-static.cnblogs.com/files/blogs/478024/image-to-latex.gif) - -该项目当前(2021年09月02日)star人数为631,Fork为81: - - - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902110737578-1880654998.png) - - - -最近正好也是在了解机器学习、深度学习相关的东西,于是打算上手感受一下转换效果。 - -## 百闻不如一试 - -其实`image-to-latex`这个项目的`README`写得算是比较清楚了,介绍了项目的来龙去脉、可以改进的地方、如何使用等等。 - -### 快速开始 - -下面我把自己第一次尝试的过程简单记录如下: - -- 克隆项目 - -```bash -git clone --depth=1 https://github.com/kingyiusuen/image-to-latex.git - -cd image-to-latex -``` - -多啰嗦一句: - -> --depth: 用来指定克隆的深度,1表示克隆最近的一次commit。这种方法克隆是为了减小项目体积的,加快克隆速度,对于那种庞大且活跃的开源项目非常有效。 - - - -- 准备Python环境 - -该项目依赖Python环境,由于我用的是`conda`来管理虚拟环境的,不是用`venv`,所以这里的步骤可能与`README`上的有一点点差异。 - -此时应该是在项目目录下,即`image-to-latex`目录,该目录下有`requirements.txt`文件。 - -```bash -# 创建新的python3.6环境 -conda create --name latex python=3.6 - -# 激活环境 -conda activate latex - -# 安装依赖 -pip install -r requirements.txt -``` - -关于Python环境的搭建,可以参考我之前的博客: - -[https://www.cnblogs.com/bytesfly/p/python-environment.html](https://www.cnblogs.com/bytesfly/p/python-environment.html) - - - -- 下载模型 - -> For example, you can use the following command to download my best run. - -到了这步本该是模型训练(`Model Training`),我这里仅想体验一下,可以直接下载别人已经训练好的模型。 - -```bash -python scripts/download_checkpoint.py kingyiusuen/image-to-latex/1w1abmg1 -``` - -此时shell显示如下: - -```bash -(latex) ➜ python scripts/download_checkpoint.py kingyiusuen/image-to-latex/1w1abmg1 -wandb: (1) Create a W&B account -wandb: (2) Use an existing W&B account -wandb: (3) Don't visualize my results -wandb: Enter your choice: 3 -wandb: You chose 'Don't visualize my results' -Downloading model checkpoint... -Model checkpoint downloaded to image-to-latex/artifacts/model.pt. -``` - -下载需要稍微等等,模型有将近2个G的大小。 - - - -- 启动服务 - -(1) 启动后端服务,执行命令`make api` - -> An API is created to make predictions using the trained model. - -看下项目的`Makefile`文件,其实`make api`就是调用了下面的启动命令: - -```bash -uvicorn api.app:app --host 0.0.0.0 --port 8000 --reload --reload-dir image-to-latex --reload-dir api -``` - -浏览器打开 [http://localhost:8000/docs](http://localhost:8000/docs) ,看到接口文档如下: - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902150321870-34535692.png) - -(2) 启动前端界面,执行命令`make streamlit` - -同样,看下项目的`Makefile`文件,其实`make streamlit`调用了下面的启动命令: - -```bash -streamlit run streamlit/app.py -``` - -浏览器打开 [http://localhost:8501/](http://localhost:8501/) ,就是上传图片的界面: - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902151456717-1363789109.png) - -至此,`image-to-latex`就成功启动了,下面就期待转换公式的效果了! - - - -### 上手体验 - - - -下面我作为一个小白用户,体验一下`image-to-latex`的转换效果。 - -我从之前的博客中截图了10个公式,使用下来,感觉当前的效果并非太理想。注意,个别解析出来仅是缺少了右`}`,这种也可以算解析出来了。如下: - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902172537004-1378895487.png) - - - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902172721087-391019105.png) - - - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902172823193-1612720369.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902172934022-1095338007.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902173029972-2139679155.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902173148438-513141252.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902173234936-1514057940.png) - - - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902173328303-311689009.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902173448440-1977459115.png) - -![](http://img2020.cnblogs.com/blog/1546632/202109/1546632-20210902173612716-251625472.png) - -测试来看,貌似对多行公式的解析不太好。当然了,有这样的免费工具来辅助我们把公式图片转成`Latex`代码已经让人挺惊喜了。相信以后随着更多的人参与算法的优化、模型的改善,解析的效果会更好。 - - - -## 写在后面 - -> I found a pretty established tool called Mathpix Snip that converts handwritten formulas into LaTex code. - -`image-to-latex`这个项目的`README`里也提到了`mathpix`这个更加成熟的工具。免费版每月能识别50次公式图片。详情见: - -[https://mathpix.com/](https://mathpix.com/) - -下载试了下,识别的效果确实不错。(注意:非广告,本人与`mathpix`无任何关系,仅仅试了下而已!!!) - - - -百闻不如一试,动手尝试之后才有发言权。后面有时间会看看`image-to-latex`的代码实现,学习学习。 diff --git a/Tool/kafka-commands.md b/Tool/kafka-commands.md deleted file mode 100644 index 38e1389..0000000 --- a/Tool/kafka-commands.md +++ /dev/null @@ -1,105 +0,0 @@ -# Kafka常用topic操作命令 - - -topic 工具 -[https://cwiki.apache.org/confluence/display/KAFKA/Replication+tools](https://cwiki.apache.org/confluence/display/KAFKA/Replication+tools) - -## offset相关 -```bash -# 最大offset -bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic test_topic --time -1 - -# 最小offset -bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic test_topic --time -2 - -# offset -bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic test_topic -``` -## topic相关 -```bash -# 列出当前kafka所有的topic -bin/kafka-topics.sh --zookeeper localhost:2181 --list - -# 创建topic -bin/kafka-topics.sh --create --zookeeper localhost:2181 --topic test_topic --replication-factor 1 --partitions 1 - -bin/kafka-topics.sh --create --zookeeper localhost:2181 --topic test_topic --replication-factor 3 --partitions 10 --config cleanup.policy=compact - -bin/kafka-topics.sh --create --zookeeper localhost:2181 --topic test_topic --partitions 1   --replication-factor 1 --config max.message.bytes=64000 --config flush.messages=1 - -# 查看某topic具体情况 -bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic test_topic - -# 修改topic(分区数、特殊配置如compact属性、数据保留时间等) -bin/kafka-topics.sh --zookeeper localhost:2181 --alter --partitions 3 --config cleanup.policy=compact --topic test_topic - -# 修改topic(也可以用这种) -bin/kafka-configs.sh --alter --zookeeper localhost:2181 --entity-name test_topic --entity-type topics --add-config cleanup.policy=compact - -bin/kafka-configs.sh --alter --zookeeper localhost:2181 --entity-name test_topic --entity-type topics --delete-config cleanup.policy -``` -## consumer-group相关 -```bash -# 查看某消费组(consumer_group)具体消费情况(活跃的消费者以及lag情况等等) -bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group test_group --describe - -# 列出当前所有的消费组 -bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list - -# 旧版 -bin/kafka-consumer-groups.sh --zookeeper 127.0.0.1:2181 --group test_group --describe -``` -## consumer相关 -```bash -# 消费数据(从latest消费) -bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test_topic - -# 消费数据(从头开始消费) -bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test_topic --from-beginning - -# 消费数据(最多消费多少条就自动退出消费) -bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test_topic --max-messages 1 - -# 消费数据(同时把key打印出来) -bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test_topic --property print.key=true - -# 旧版 -bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic test_topic -``` -## producer相关 -```bash -# 生产数据 -bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test_topic - -# 生产数据(写入带有key的message) -bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test_topic --property "parse.key=true" --property "key.separator=:" -``` -## producer-golang -```bash -# golang实现的kafka客户端 -https://github.com/Shopify/sarama/tree/master/tools - -# Minimum invocation -kafka-console-producer -topic=test -value=value -brokers=kafka1:9092 - -# It will pick up a KAFKA_PEERS environment variable -export KAFKA_PEERS=kafka1:9092,kafka2:9092,kafka3:9092 -kafka-console-producer -topic=test -value=value - -# It will read the value from stdin by using pipes -echo "hello world" | kafka-console-producer -topic=test - -# Specify a key: -echo "hello world" | kafka-console-producer -topic=test -key=key - -# Partitioning: by default, kafka-console-producer will partition as follows: -# - manual partitioning if a -partition is provided -# - hash partitioning by key if a -key is provided -# - random partioning otherwise. -# -# You can override this using the -partitioner argument: -echo "hello world" | kafka-console-producer -topic=test -key=key -partitioner=random - -# Display all command line options -kafka-console-producer -help -``` \ No newline at end of file diff --git a/Tool/kafka-write-speed.md b/Tool/kafka-write-speed.md deleted file mode 100644 index e9ac514..0000000 --- a/Tool/kafka-write-speed.md +++ /dev/null @@ -1,41 +0,0 @@ -# 使用脚本+kafka自带命令行工具 统计数据写入kafka速率 - -## 思路 - -每隔一段时间(比如说10秒)统计一次某`topic`的所有`partition`的最大`offset`值之和,这便是该`topic`的message总数。 -然后除以间隔时间就可以粗略但方便得出 某`topic`的数据增长速率(即相应程序写kafka的速率) - -[`Kafka常用topic操作命令汇总`](https://www.cnblogs.com/itwild/p/12287850.html) 中有统计最大offset命令 - -```bash -# 最大offset -bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic test_topic --time -1 -``` - -## shell实现 - -注意: -1. 第一次打印出来的speed请忽略(为了shell更加简单方便没有特殊处理第1次统计的情况) -2. 该脚本需要放入到kafka程序安装根目录,或者把bin/kafka-run-class.sh文件写成绝对路径 - -```bash -#!/bin/sh - -brokers="localhost:9092" -topic="test_topic" - -last=0 -now=0 -speed=0 - -while : -do - echo "-------------" - last=$now - now=$(bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list ${brokers} --topic ${topic} --time -1 | awk -F ":" '{sum+=$NF} END {print sum}') - let speed=(now-last)/10 - echo "now is $now, speed is $speed" - sleep 10 -done - -``` \ No newline at end of file diff --git a/Tool/markdown-formula.md b/Tool/markdown-formula.md deleted file mode 100644 index e881971..0000000 --- a/Tool/markdown-formula.md +++ /dev/null @@ -1,937 +0,0 @@ -# 一文学会在Markdown中编辑数学符号与公式 - -在用Markdown写博客时会涉及到数学符号与公式的编辑,下面进行汇总。随手记录,方便你我他。 - - - -- 行内公式:将公式插入到本行内 - -```bash -$0.98^{365} \approx 0.0006$ -``` - -我的365天:$0.98^{365} \approx 0.0006$ - - - -- 单独的公式块:将公式插入到新的一行内,并且居中 - -```bash -$$ -1.02^{365} \approx 1377.4 -$$ -``` -在座各位大佬的365天: -$$ -1.02^{365} \approx 1377.4 -$$ - -注意: - -1. 在博客园用Markdown写博客需要启用数学公式支持,如下: - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210819163846076-1128461557.png) - -2. 在博客园可以在公式上右键查看详情: - - ![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210822134624501-390959711.png) - -3. 如果使用Typora编写Markdown,解析行内公式需要手动设置一下, 文件 -> 偏好设置 -> Markdown -> Markdown扩展语法 -> 勾选 “内联公式”,重启软件,Typora才会解析行内公式。 - -![](http://img2020.cnblogs.com/blog/1546632/202108/1546632-20210819164501658-996062202.png) - -## 符号 - -### 上下标、运算符 - -| | 显示效果 | markdown公式语法 | -| :--------: | :----------------------------------------------------: | :----------------------------------------------------: | -| 上标 | $x^2、 x^y 、e^{365}$ | `x^2、 x^y 、e^{365}` | -| 下标 | $x_0、a_1、Y_a$ | `x_0、a_1、Y_a` | -| 分式 | $\frac{x}{y}、\frac{1}{x+1}$ | `\frac{x}{y}、\frac{1}{x+1}` | -| 乘 | $\times$ | `\times` | -| 除 | $\div$ | `\div` | -| 加减 | $\pm$ | `\pm` | -| 减加 | $\mp$ | `\mp` | -| 求和 | $\sum$ | `\sum` | -| 求和上下标 | $\sum_0^3 、\sum_0^{\infty} 、\sum_{-\infty}^{\infty}$ | `\sum_0^3 、\sum_0^{\infty} 、\sum_{-\infty}^{\infty}` | -| 求积 | $\prod$ | `\prod` | -| 微分 | $\partial$ | `\partial` | -| 积分 | $\int 、\displaystyle\int$ | `\int 、\displaystyle\int` | -| 不等于 | $\neq$ | `\neq` | -| 大于等于 | $\geq$ | `\geq` | -| 小于等于 | $\leq$ | `\leq` | -| 约等于 | $\approx$ | `\approx` | -| 不大于等于 | $x+y \ngeq z$ | `x+y \ngeq z` | -| 点乘 | $a \cdot b$ | `a \cdot b` | -| 星乘 | $a \ast b$ | `a \ast b` | -| 取整函数 | $\left \lfloor \frac{a}{b} \right \rfloor$ | `\left \lfloor \frac{a}{b} \right \rfloor` | -| 取顶函数 | $\left \lceil \frac{c}{d} \right \rceil$ | `\left \lceil \frac{c}{d} \right \rceil` | - -### 括号 - -| | 显示效果 | markdown公式语法 | -| :--------------: | :-----------------------------------------------: | :-----------------------------------------------: | -| 圆括号(小括号) | $\left( \frac{a}{b} \right)$ | `\left( \frac{a}{b} \right)` | -| 方括号(中括号) | $\left[ \frac{a}{b} \right]$或者$[ \frac{x}{y} ]$ | `\left[ \frac{a}{b} \right]`或者`[ \frac{x}{y} ]` | -| 花括号(大括号) | $\lbrace \frac{a}{b} \rbrace$ | `\lbrace \frac{a}{b} \rbrace` | -| 角括号 | $\left \langle \frac{a}{b} \right \rangle$ | `\left \langle \frac{a}{b} \right \rangle` | -| 混合括号 | $\left [ a,b \right )$ | `\left [ a,b \right )` | - - - -### 三角函数、指数、对数 - -| | 显示效果 | markdown公式语法 | -| :--: | :---------: | :--------------: | -| sin | $\sin(x)$ | `\sin(x)` | -| cos | $\cos(x)$ | `\cos(x)` | -| tan | $\tan(x)$ | `\tan(x)` | -| cot | $\cot(x)$ | `\cot(x)` | -| log | $\log_2 10$ | `\log_2 10` | -| lg | $\lg 100$ | `\lg 100` | -| ln | $\ln2$ | `\ln2` | - -### 数学符号 - -| | 显示效果 | markdown公式语法 | -| :---------------: | :--------------------------------------------: | :--------------------------------------------: | -| 无穷 | $\infty$ | `\infty` | -| 矢量 | $\vec{a}$ | `\vec{a}` | -| 一阶导数 | $\dot{x}$ | `\dot{x}` | -| 二阶导数 | $\ddot{x}$ | `\ddot{x}` | -| 算数平均值 | $\bar{a}$ | `\bar{a}` | -| 概率分布 | $\hat{a}$ | `\hat{a}` | -| 虚数i、j | $\imath、\jmath$ | `\imath、\jmath` | -| 省略号(一) | $1,2,3,\ldots,n$ | `1,2,3,\ldots,n` | -| 省略号(二) | $x_1 + x_2 + \cdots + x_n$ | `x_1 + x_2 + \cdots + x_n` | -| 省略号(三) | $\vdots$ | `\vdots` | -| 省略号(四) | $\ddots$ | `\ddots` | -| 斜线与反斜线 | $\left / \frac{a}{b} \right \backslash$ | `\left / \frac{a}{b} \right \backslash` | -| 上下箭头 | $\left \uparrow \frac{a}{b} \right \downarrow$ | `\left \uparrow \frac{a}{b} \right \downarrow` | -| $\angle$ | $\angle$ | `\angle` | -| $\prime$ | $\prime$ | `\prime` | -| $\rightarrow$ | $\rightarrow$ | `\rightarrow` | -| $\leftarrow$ | $\leftarrow$ | `\leftarrow` | -| $\Rightarrow$ | $\Rightarrow$ | `\Rightarrow` | -| $\Leftarrow$ | $\Leftarrow$ | `\Leftarrow` | -| $\Uparrow$ | $\Uparrow$ | `\Uparrow` | -| $\Downarrow$ | $\Downarrow$ | `\Downarrow` | -| $\longrightarrow$ | $\longrightarrow$ | `\longrightarrow` | -| $\longleftarrow$ | $\longleftarrow$ | `\longleftarrow` | -| $\Longrightarrow$ | $\Longrightarrow$ | `\Longrightarrow` | -| $\Longleftarrow$ | $\Longleftarrow$ | `\Longleftarrow` | -| $\nabla$ | $\nabla$ | `\nabla` | -| $\because$ | $\because$ | `\because` | -| $\therefore$ | $\therefore$ | `\therefore` | -| $\mid$ | $\mid$ | `\mid` | -| $\backslash$ | $\backslash$ | `\backslash` | -| $\forall$ | $\forall$ | `\forall` | -| $\exists$ | $\exists$ | `\exists` | -| $\backsim$ | $\backsim$ | `\backsim` | -| $\cong$ | $\cong$ | `\cong` | -| $\oint$ | $\oint$ | `\oint` | -| $\implies$ | $\implies$ | `\implies` | -| $\iff$ | $\iff$ | `\iff` | -| $\impliedby$ | $\impliedby$ | `\impliedby` | - -### 连线符号 - -| 显示效果 | markdown公式语法 | -| :----------------------------------------------: | :----------------------------------------------: | -| $\overleftarrow{a+b+c}$ | `\overleftarrow{a+b+c}` | -| $\overrightarrow{a+b+c}$ | `\overrightarrow{a+b+c}` | -| $\overleftrightarrow{a+b+c}$ | `\overleftrightarrow{a+b+c}` | -| $\underleftarrow{a+b+c}$ | `\underleftarrow{a+b+c}` | -| $\underrightarrow{a+b+c}$ | `\underrightarrow{a+b+c}` | -| $\underleftrightarrow{a+b+c}$ | `\underleftrightarrow{a+b+c}` | -| $\overline{a+b+c}$ | `\overline{a+b+c}` | -| $\underline{a+b+c}$ | `\underline{a+b+c}` | -| $\overbrace{a+b+c}^{Sample}$ | `\overbrace{a+b+c}^{Sample}` | -| $\underbrace{a+b+c}_{Sample}$ | `\underbrace{a+b+c}_{Sample}` | -| $\overbrace{a+\underbrace{b+c}_{1.0}}^{2.0}$ | `\overbrace{a+\underbrace{b+c}_{1.0}}^{2.0}` | -| $\underbrace{a\cdot a\cdots a}_{b\text{ times}}$ | `\underbrace{a\cdot a\cdots a}_{b\text{ times}}` | - - - -### 高级运算符 - -| | 显示效果 | markdown公式语法 | -| :----------: | :----------------------------------------------------------: | :----------------------------------------------------------: | -| 平均数运算 | $\overline{xyz}$ | `\overline{xyz}` | -| 开二次方运算 | $\sqrt {xy}$ | `\sqrt {xy}` | -| 开方运算 | $\sqrt[n]{x}$ | `\sqrt[n]{x}` | -| 极限运算(一) | $\lim^{x \to \infty}_{y \to 0}{\frac{x}{y}}$ | `\lim^{x \to \infty}_{y \to 0}{\frac{x}{y}}` | -| 极限运算(二) | $\displaystyle \lim^{x \to \infty}_{y \to 0}{\frac{x}{y}}$ | `\displaystyle \lim^{x \to \infty}_{y \to 0}{\frac{x}{y}}` | -| 求和运算(一) | $\sum^{x \to \infty}_{y \to 0}{\frac{x}{y}}$ | `\sum^{x \to \infty}_{y \to 0}{\frac{x}{y}}` | -| 求和运算(二) | $\displaystyle \sum^{x \to \infty}_{y \to 0}{\frac{x}{y}}$ | `\displaystyle \sum^{x \to \infty}_{y \to 0}{\frac{x}{y}}` | -| 积分运算(一) | $\int^{\infty}_{0}{xdx}$ | `\int^{\infty}_{0}{xdx}` | -| 积分运算(二) | $\displaystyle \int^{\infty}_{0}{xdx}$ | `\displaystyle \int^{\infty}_{0}{xdx}` | -| 微分运算 | $\frac{\partial x}{\partial y}、\frac{\partial^2x}{\partial y^2}$ | `\frac{\partial x}{\partial y}、\frac{\partial^2x}{\partial y^2}` | - -### 集合运算 - -| | 显示效果 | markdown公式语法 | -| :--------: | :----------------------------: | :----------------------------: | -| 属于 | $A \in B$ | `A \in B` | -| 不属于 | $A \notin B$ | `A \notin B` | -| 子集 | $x \subset y、y \supset x$ | `x \subset y、y \supset x` | -| 真子集 | $x \subseteq y、y \supseteq x$ | `x \subseteq y、y \supseteq x` | -| 并集 | $A \cup B$ | `A \cup B` | -| 交集 | $A \cap B$ | `A \cap B` | -| 差集 | $A \setminus B$ | `A \setminus B` | -| 同或 | $A \bigodot B$ | `A \bigodot B` | -| 同与 | $A \bigotimes B$ | `A \bigotimes B` | -| 异或 | $A \bigoplus B$ | `A \bigoplus B` | -| 实数集合 | $\mathbb{R}$ | `\mathbb{R}` | -| 自然数集合 | $\mathbb{Z}$ | `\mathbb{Z}` | - -### 希腊字母 - -| 大写字母 | markdown语法 | 小写字母 | markdown语法 | 中文注音 | -| :--------: | :----------: | :--------: | :----------: | :------: | -| $A$ | `A` | $\alpha$ | `\alpha` | 阿尔法 | -| $B$ | `B` | $\beta$ | `\beta` | 贝塔 | -| $\Gamma$ | `\Gamma` | $\gamma$ | `\gamma` | 伽马 | -| $\Delta$ | `\Delta` | $\delta$ | `\delta` | 德尔塔 | -| $E$ | `E` | $\epsilon$ | `\epsilon` | 伊普西龙 | -| $Z$ | `Z` | $\zeta$ | `\zeta` | 截塔 | -| $H$ | `H` | $\eta$ | `\eta` | 艾塔 | -| $\Theta$ | `\Theta` | $\theta$ | `\theta` | 西塔 | -| $I$ | `I` | $\iota$ | `\iota` | 约塔 | -| $K$ | `K` | $\kappa$ | `\kappa` | 卡帕 | -| $\Lambda$ | `\Lambda` | $\lambda$ | `\lambda` | 兰布达 | -| $M$ | `M` | $\mu$ | `\mu` | 缪 | -| $N$ | `N` | $\nu$ | `\nu` | 纽 | -| $\Xi$ | `\Xi` | $\xi$ | `\xi` | 克西 | -| $O$ | `O` | $\omicron$ | `\omicron` | 奥密克戎 | -| $\Pi$ | `\Pi` | $\pi$ | `\pi` | 派 | -| $P$ | `P` | $\rho$ | `\rho` | 肉 | -| $\Sigma$ | `\Sigma` | $\sigma$ | `\sigma` | 西格马 | -| $T$ | `T` | $\tau$ | `\tau` | 套 | -| $\Upsilon$ | `\Upsilon` | $\upsilon$ | `\upsilon` | 宇普西龙 | -| $\Phi$ | `\Phi` | $\phi$ | `\phi` | 佛爱 | -| $X$ | `X` | $\chi$ | `\chi` | 西 | -| $\Psi$ | `\Psi` | $\psi$ | `\psi` | 普西 | -| $\Omega$ | `\Omega` | $\omega$ | `\omega` | 欧米伽 | - -### 字体转换 - -若要对公式的某一部分字符进行字体转换,可以用 `{\font {需转换的部分字符}}` 命令,其中`\font`部分可以参照下表选择合适的字体。一般情况下,公式默认为意大利体。 - -| 字体 | 显示效果 | markdown语法 | -| :--------: | :-------------: | :-------------: | -| 罗马体 | $\rm D$ | `\rm D` | -| 花体 | $\cal D$ | `\cal D` | -| 意大利体 | $\it D$ | `\it D` | -| 黑板粗体 | $\Bbb D$ | `\Bbb D` | -| 粗体 | $\bf D$ | `\bf D` | -| 数学斜体 | $\mit D$ | `\mit D` | -| 等线体 | $\sf D$ | `\sf D` | -| 手写体 | $\scr D$ | `\scr D` | -| 打字机体 | $\tt D$ | `\tt D` | -| 旧德式字体 | $\frak D$ | `\frak D` | -| 黑体 | $\boldsymbol D$ | `\boldsymbol D` | - - - -## 公式 - - - -### 基本函数公式 - - - -- 行内公式:$\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt$ - -```bash -$\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt$ -``` - - - - -- 行间公式: - -$$ -\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt -$$ - -```bash -$$ -\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt -$$ -``` - - - - -- $y_k=\varphi(u_k+v_k)$ - -```bash -$y_k=\varphi(u_k+v_k)$ -``` - - - - -- $y(x)=x^3+2x^2+x+1$ - -```bash -$y(x)=x^3+2x^2+x+1$ -``` - - - - -- $x^{y}=(1+{\rm e}^x)^{-2xy}$ - -```bash -$x^{y}=(1+{\rm e}^x)^{-2xy}$ -``` - - - - -- $\displaystyle f(n)=\sum_{i=1}^{n}{n*(n+1)}$ - -```bash -$\displaystyle f(n)=\sum_{i=1}^{n}{n*(n+1)}$ -``` - -### 分段函数 - - - -- 分段函数: - -$$ -y=\begin{cases} -2x+1, & x \leq0\\ -x, & x>0 -\end{cases} -$$ - -```bash -$$ -y=\begin{cases} -2x+1, & x \leq0\\ -x, & x>0 -\end{cases} -$$ -``` - - - -- 方程组: - -$$ -\left \{ -\begin{array}{c} -a_1x+b_1y+c_1z=d_1 \\ -a_2x+b_2y+c_2z=d_2 \\ -a_3x+b_3y+c_3z=d_3 -\end{array} -\right. -$$ - -```bash -$$ -\left \{ -\begin{array}{c} -a_1x+b_1y+c_1z=d_1 \\ -a_2x+b_2y+c_2z=d_2 \\ -a_3x+b_3y+c_3z=d_3 -\end{array} -\right. -$$ -``` - - - -### 积分 - -- 积分书写: - -$$ -\int_{\theta_1(x)}^{\theta_2(x)}=l -$$ - -```bash -$$ -\int_{\theta_1(x)}^{\theta_2(x)}=l -$$ -``` - - - -- 二重积分: - -$$ -\iint dx dy=\sigma -$$ - -```bash -$$ -\iint dx dy=\sigma -$$ -``` - - - -- 三重积分: - -$$ -\iiint dx dydz=\nu -$$ - -```bash -$$ -\iiint dx dydz=\nu -$$ -``` - - - -### 微分和偏微分 - -- 一阶微分方程: - -$$ -\frac{dy}{dx}+P(x)y=Q(x) -$$ - -```bash -$$ -\frac{dy}{dx}+P(x)y=Q(x) -$$ -``` - - -$$ -\left. \frac{{\rm d}y}{{\rm d}x} \right|_{x=0}=3x+1=1 -$$ - -```bash -$$ -\left. \frac{{\rm d}y}{{\rm d}x} \right|_{x=0}=3x+1=1 -$$ -``` - - - -- 二阶微分方程: - -$$ -y''+py'+qy=f(x) -$$ - -```bash -$$ -y''+py'+qy=f(x) -$$ -``` - - - -$$ -\frac{d^2y}{dx^2}+p\frac{dy}{dx}+qy=f(x) -$$ - -```bash -$$ -\frac{d^2y}{dx^2}+p\frac{dy}{dx}+qy=f(x) -$$ -``` - - - -- 偏微分方程: - -$$ -\frac{\partial u}{\partial t}= h^2 \left( \frac{\partial^2 u}{\partial x^2} +\frac{\partial^2 u}{\partial y^2}+ \frac{\partial^2 u}{\partial z^2}\right) -$$ - -```bash -$$ -\frac{\partial u}{\partial t}= h^2 \left( \frac{\partial^2 u}{\partial x^2} +\frac{\partial^2 u}{\partial y^2}+ \frac{\partial^2 u}{\partial z^2}\right) -$$ -``` - - - -### 矩阵和行列式 - -起始标记 `\begin{matrix}` ,结束标记`\end{matrix}`,每一行末尾标记\\,行间元素之间以&分隔。在起始、结束标记处用下列词替换`matrix`。 - -- `pmatrix` :小括号边框 - -$$ -\begin{pmatrix} -1&2\\ -3&4\\ -\end{pmatrix} -$$ - -```bash -$$ -\begin{pmatrix} -1&2\\ -3&4\\ -\end{pmatrix} -$$ -``` - - - -- `bmatrix` :中括号边框 - -$$ -\begin{bmatrix} -1&2\\ -3&4\\ -\end{bmatrix} -$$ - -```bash -$$ -\begin{bmatrix} -1&2\\ -3&4\\ -\end{bmatrix} -$$ -``` - - - -- `Bmatrix` :大括号边框 - -$$ -\begin{Bmatrix} -1&2\\ -3&4\\ -\end{Bmatrix} -$$ - -```bash -$$ -\begin{Bmatrix} -1&2\\ -3&4\\ -\end{Bmatrix} -$$ -``` - - - -- `vmatrix` :单竖线边框 - -$$ -\begin{vmatrix} -1&2\\ -3&4\\ -\end{vmatrix} -$$ - -```bash -$$ -\begin{vmatrix} -1&2\\ -3&4\\ -\end{vmatrix} -$$ -``` - - - -- `Vmatrix` :双竖线边框 - -$$ -\begin{Vmatrix} -1&2\\ -3&4\\ -\end{Vmatrix} -$$ - -```bash -$$ -\begin{Vmatrix} -1&2\\ -3&4\\ -\end{Vmatrix} -$$ -``` - - - -- 无框矩阵: - -$$ -\begin{matrix} -1 & x & x^2 \\ -1 & y & y^2 \\ -1 & z & z^2 \\ -\end{matrix} -$$ - -```bash -$$ -\begin{matrix} - 1 & x & x^2 \\ - 1 & y & y^2 \\ - 1 & z & z^2 \\ -\end{matrix} -$$ -``` - - - -- 单位矩阵: - -$$ -\begin{bmatrix} -1&0&0\\ -0&1&0\\ -0&0&1\\ -\end{bmatrix} -$$ - -```bash -$$ -\begin{bmatrix} -1&0&0\\ -0&1&0\\ -0&0&1\\ -\end{bmatrix} -$$ -``` - - - -- $m \times n$矩阵: - -$$ -A=\begin{bmatrix} -{a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ -{a_{21}}&{a_{22}}&{\cdots}&{a_{2n}}\\ -{\vdots}&{\vdots}&{\ddots}&{\vdots}\\ -{a_{m1}}&{a_{m2}}&{\cdots}&{a_{mn}}\\ -\end{bmatrix} -$$ - -```bash -$$ -A=\begin{bmatrix} -{a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ -{a_{21}}&{a_{22}}&{\cdots}&{a_{2n}}\\ -{\vdots}&{\vdots}&{\ddots}&{\vdots}\\ -{a_{m1}}&{a_{m2}}&{\cdots}&{a_{mn}}\\ -\end{bmatrix} -$$ -``` - - - -- 行列式: - -$$ -D=\begin{vmatrix} -{a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ -{a_{21}}&{a_{22}}&{\cdots}&{a_{2n}}\\ -{\vdots}&{\vdots}&{\ddots}&{\vdots}\\ -{a_{m1}}&{a_{m2}}&{\cdots}&{a_{mn}}\\ -\end{vmatrix} -$$ - -```bash -$$ -D=\begin{vmatrix} -{a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ -{a_{21}}&{a_{22}}&{\cdots}&{a_{2n}}\\ -{\vdots}&{\vdots}&{\ddots}&{\vdots}\\ -{a_{m1}}&{a_{m2}}&{\cdots}&{a_{mn}}\\ -\end{vmatrix} -$$ -``` - - - -- 表格: - -$$ -\begin{array}{c|lll} -{}&{a}&{b}&{c}\\ -\hline -{R_1}&{c}&{b}&{a}\\ -{R_2}&{b}&{c}&{c}\\ -\end{array} -$$ - -```bash -$$ -\begin{array}{c|lll} -{}&{a}&{b}&{c}\\ -\hline -{R_1}&{c}&{b}&{a}\\ -{R_2}&{b}&{c}&{c}\\ -\end{array} -$$ -``` - - - -- 增广矩阵: - -$$ -\left[ \begin{array} {c c | c} -1 & 2 & 3 \\ -4 & 5 & 6 \\ -\end{array} \right] -$$ - -```bash -$$ -\left[ \begin{array} {c c | c} -1 & 2 & 3 \\ -4 & 5 & 6 \\ -\end{array} \right] -$$ -``` - - - -## 案例 - -- `^`表示上标,` _` 表示下标。如果上下标的内容多于一个字符,需要用`{}`将这些内容括成一个整体。上下标可以嵌套,也可以同时使用。 - -$$ -x^{y^z}=(1+{\rm e}^x)^{-2xy^w} -$$ - -```bash -$$ -x^{y^z}=(1+{\rm e}^x)^{-2xy^w} -$$ -``` - -其中`\rm`表示字体转换,上面有过具体说明。 - - - -- `()`、`[]`和`|`表示符号本身,使用 `\{` `\}` 来表示 {}。当要显示大号的括号或分隔符时,要用` \left` 和` \right` 命令。 - -$$ -f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right) -$$ - -```bash -$$ -f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right) -$$ -``` - - - -- 行标的使用:在公式末尾前使用`\tag{行标}`来实现行标。 - -$$ -f\left( -\left[ -\frac{ -1+\left\{x,y\right\} -}{ -\left( -\frac{x}{y}+\frac{y}{x} -\right) -\left(u+1\right) -}+a -\right]^{3/2} -\right) -\tag{公式1} -$$ - -```bash -$$ -f\left( - \left[ - \frac{ - 1+\left\{x,y\right\} - }{ - \left( - \frac{x}{y}+\frac{y}{x} - \right) - \left(u+1\right) - }+a - \right]^{3/2} -\right) -\tag{公式1} -$$ -``` - - - -- 有时要用 `\left.` 或` \right.` 进行匹配而不显示本身。 - -$$ -\left. \frac{{\rm d}u}{{\rm d}x} \right| _{x=0} -$$ - -```bash -$$ -\left. \frac{{\rm d}u}{{\rm d}x} \right| _{x=0} -$$ -``` - - - -- 添加注释文字` \text` - -$$ -f(n)= \begin{cases} -n/2, & \text {if $n$ is even} \\ -3n+1, & \text{if $n$ is odd} \\ -\end{cases} -$$ - -```bash -$$ -f(n)= \begin{cases} -n/2, & \text {if $n$ is even} \\ -3n+1, & \text{if $n$ is odd} \\ -\end{cases} -$$ -``` - - - -- 整齐且居中的方程式序列 - -$$ -\begin{align} -\sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\ -& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\ -& = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\ -& = \frac{73}{12}\sqrt{1-\frac{1}{73^2}} \\ -& \approx \frac{73}{12}\left(1-\frac{1}{2\cdot73^2}\right) \\ -\end{align} -$$ - -```bash -$$ -\begin{align} - \sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\ - & = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\ - & = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\ - & = \frac{73}{12}\sqrt{1-\frac{1}{73^2}} \\ - & \approx \frac{73}{12}\left(1-\frac{1}{2\cdot73^2}\right) \\ -\end{align} -$$ -``` - - - -- 在一个方程式序列的每一行中注明原因 - -$$ -\begin{align} -v + w & = 0 & \text{Given} \tag 1 \\ --w & = -w + 0 & \text{additive identity} \tag 2 \\ --w + 0 & = -w + (v + w) & \text{equations $(1)$ and $(2)$} \\ -\end{align} -$$ - -```bash -$$ -\begin{align} - v + w & = 0 & \text{Given} \tag 1 \\ - -w & = -w + 0 & \text{additive identity} \tag 2 \\ - -w + 0 & = -w + (v + w) & \text{equations $(1)$ and $(2)$} \\ -\end{align} -$$ -``` - - - -- 文字在左对齐显示 - -$$ -\left. -\begin{array}{l} -\text{if $n$ is even:} & n/2 \\ -\text{if $n$ is odd:} & 3n+1 \\ -\end{array} -\right\} -=f(n) -$$ - -```bash -$$ - \left. - \begin{array}{l} - \text{if $n$ is even:} & n/2 \\ - \text{if $n$ is odd:} & 3n+1 \\ - \end{array} - \right\} - =f(n) -$$ -``` - - - -- 连分式 - -$$ -x = a_0 + \cfrac{1^2}{a_1 + -\cfrac{2^2}{a_2 + -\cfrac{3^2}{a_3 + -\cfrac{4^4}{a_4 + -\cdots -} -} -} -} -$$ - -```bash -$$ -x = a_0 + \cfrac{1^2}{a_1 + - \cfrac{2^2}{a_2 + - \cfrac{3^2}{a_3 + - \cfrac{4^4}{a_4 + - \cdots - } - } - } - } -$$ -``` - - - -- 表格 - -通常,一个格式化后的表格比单纯的文字或排版后的文字更具有可读性。 -数组和表格均以 `\begin{array}` 开头,并在其后定义列数及每一列的文本对齐属性,`c l r `分别代表居中、左对齐及右对齐。若需要插入垂直分割线,在定义式中插入 `|` ,若要插入水平分割线,在下一行输入前插入` \hline` 。 -与矩阵相似,每行元素间均须要插入` &` ,每行元素以 \\ 结尾,最后以 `\ end{array}` 结束数组。 -$$ -\begin{array}{c|lcr} -n & \text{左对齐} & \text{居中对齐} & \text{右对齐} \\ -\hline -1 & 0.24 & 1 & 125 \\ -2 & -1 & 189 & -8 \\ -3 & -20 & 2000 & 1+10i \\ -\end{array} -$$ - -```bash -$$ -\begin{array}{c|lcr} - n & \text{左对齐} & \text{居中对齐} & \text{右对齐} \\ - \hline - 1 & 0.24 & 1 & 125 \\ - 2 & -1 & 189 & -8 \\ - 3 & -20 & 2000 & 1+10i \\ -\end{array} -$$ -``` diff --git a/Tool/work-on-deepin-linux.md b/Tool/work-on-deepin-linux.md deleted file mode 100644 index 40f8232..0000000 --- a/Tool/work-on-deepin-linux.md +++ /dev/null @@ -1,208 +0,0 @@ -# Linux工作环境搭建——deepin系统的使用 - -上大学的时候就在自己的笔记本上安装过深度操作系统(deepin),当时好像是15.x的版本。毕业后第一家公司是全Mac办公,因在学校期间有过完全Linux环境下的开发体验,上手Mac非常快、非常爽。前段时间换了工作,当前公司用的是台式机。于是,入职当天重装了deepin系统,也就有了此篇博客。随手记录,方便你我他。持续更新~~ - -![](https://img2020.cnblogs.com/blog/1546632/202105/1546632-20210516142452754-1442240469.png) - -deepin最新版本下载 -[https://www.deepin.org/zh/download/](https://www.deepin.org/zh/download/) - - -如何安装deepin -[https://www.deepin.org/zh/installation/](https://www.deepin.org/zh/installation/) - - -当前版本deepin20.2,开箱就内置了很多实用的软件。但作为软件开发人员,还需要安装一些开发中常用的工具与软件。 - -## 常用软件安装 -```bash -sudo apt-get install git -y - -sudo apt-get install curl -y - -sudo apt-get install zsh -y - -sudo apt-get install xsel -y - -sudo apt-get install htop -y -``` -Oh My Zsh安装: -[https://ohmyz.sh/](https://ohmyz.sh/) - -Chrome浏览器: -https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - - -Chrome浏览器常用插件 - -- Adblock Plus -- Tampermonkey -- JSON Formatter -- Sourcegraph -- Octotree -- GitCodeTree -- XPath Helper -- yuque-helper -- Google翻译 -- Proxy SwitchyOmega - - - -搜狗输入法: -[https://pinyin.sogou.com/linux/](https://pinyin.sogou.com/linux/) - -百度输入法: -[http://srf.baidu.com/site/guanwang_linux/index.html](http://srf.baidu.com/site/guanwang_linux/index.html) - -JDK: -[https://repo.huaweicloud.com/java/jdk/8u202-b08/](https://repo.huaweicloud.com/java/jdk/8u202-b08/) -[https://enos.itcollege.ee/~jpoial/allalaadimised/jdk8/](https://enos.itcollege.ee/~jpoial/allalaadimised/jdk8/) - -Python开发环境搭建: -[https://www.cnblogs.com/bytesfly/p/python-environment.html](https://www.cnblogs.com/bytesfly/p/python-environment.html) - -Java反编译图形化工具: -[http://java-decompiler.github.io/](http://java-decompiler.github.io/) - -JetBrains全家桶: -[https://www.jetbrains.com/zh-cn/products/](https://www.jetbrains.com/zh-cn/products/) - - -IntelliJ IDEA: -[https://www.jetbrains.com/zh-cn/idea/download/other.html](https://www.jetbrains.com/zh-cn/idea/download/other.html) - - -PyCharm: -[https://www.jetbrains.com/pycharm/download/other.html](https://www.jetbrains.com/pycharm/download/other.html) - - -DataGrip: -[https://www.jetbrains.com/zh-cn/datagrip/download/other.html](https://www.jetbrains.com/zh-cn/datagrip/download/other.html) - - -VSCode: -[https://code.visualstudio.com/Download](https://code.visualstudio.com/Download) - - -百度网盘客户端(官方已有Linux版): -[https://pan.baidu.com/download/](https://pan.baidu.com/download/) - -Free Download Manager(也可以从deepin的应用商店直接安装): -[https://www.freedownloadmanager.org/zh/](https://www.freedownloadmanager.org/zh/) - - -docker安装 -[https://wiki.deepin.org/wiki/Docker](https://wiki.deepin.org/wiki/Docker) -另外附上别人已经整理好的安装脚本(实测没毛病, 强烈推荐) -[https://gist.github.com/madkoding/3f9b02c431de5d748dfde6957b8b85ff](https://gist.github.com/madkoding/3f9b02c431de5d748dfde6957b8b85ff) - - - -命令导入OpenVPN文件: -[https://github.com/linuxdeepin/dde-control-center/issues/43](https://github.com/linuxdeepin/dde-control-center/issues/43) - -[https://bbs.deepin.org/post/205870](https://bbs.deepin.org/post/205870) -```bash -sudo nmcli connection import type openvpn file your-own-openvpn-profile-config-file.ovpn -``` - - -此外在deepin的应用商店可方便的安装很多常用软件,比如: -Typora(markdown编辑器)、微信、QQ、WPS、迅雷、Postman、Wireshark、Flameshot(好用的截图工具)、网易云音乐等等 - -Typora主题:[https://theme.typora.io/theme/Drake/](https://theme.typora.io/theme/Drake/) -字体:[https://www.jetbrains.com/zh-cn/lp/mono/](https://www.jetbrains.com/zh-cn/lp/mono/) - - -星火应用商店——致力于丰富Linux生态,取`星星之火,可以燎原`之意(有一些民间wine打包的应用): -[https://www.spark-app.store/](https://www.spark-app.store/) - - -其他: -[https://github.com/shadowsocksrr/electron-ssr](https://github.com/shadowsocksrr/electron-ssr) - -[https://github.com/Qv2ray/Qv2ray](https://github.com/Qv2ray/Qv2ray) - -[https://qv2ray.net/lang/zh/](https://qv2ray.net/lang/zh/) - -## Command汇总 - -### 实用 - -命令行操作剪贴板 -```bash -# 安装xsel -sudo apt-get install xsel - -# 拷贝到剪贴板 -cat file.txt | xsel -b - -# 从剪贴板粘贴 -xsel -b >> example.txt -``` - - -字体查看 -```sh -# 查看系统字体 -fc-list - -# 查看系统中已经安装的中文字体 -fc-list :lang=zh - -fc-list -q 'Noto Serif CJK SC' -fc-list -q 'WenQuanYi Micro Hei' -``` - -## 常见问题汇总 - - -下面是使用deepin过程中遇到的常见问题汇总。持续更新~~ - - -### 快捷键冲突 - - -参考:[https://www.jianshu.com/p/4bbae666abff](https://www.jianshu.com/p/4bbae666abff) - - -IDEA中有好几个常用的快捷键被deepin系统占用了,非常难受,我是不愿意修改IDEA默认快捷键的(通用的多好哇),所以尝试去修改deepin系统默认快捷键。 -```bash -# 查看哪些快捷键被占用了,记得用grep过滤 -gsettings list-recursively - -# 取消Ctrl+Alt+U -gsettings set com.deepin.dde.keybinding.system translation '[]' -``` -修改被系统占用的快捷键`Ctrl+Alt+B`,这样IDEA中就能happy地使用了。 -![](http://img2020.cnblogs.com/blog/1546632/202103/1546632-20210313093911720-306157024.png) - -### dpkg: 处理软件包 xxx (--configure)时出错 - -使用`apt-get`安装某软件包(比如`xxx`)时失败可能会导致安装其他软件都失败,报错信息大致如下: -> dpkg: 处理软件包 xxx (--configure)时出错: -已安装 xxx 软件包 post-installation 脚本 子进程返回错误状态 1 -在处理时有错误发生: -xxx -E: Sub-process /usr/bin/dpkg returned an error code (1) - -可以用如下方法尝试解决(比如发生错误的软件包是`xxx`): -```bash -# 新建临时目录用作备份 -mkdir /tmp/xxx - -# 查看xxx软件包信息文件 -ls -l /var/lib/dpkg/info/xxx.* - -# 把xxx软件包信息移到临时目录 -sudo mv /var/lib/dpkg/info/xxx.* /tmp/xxx - -# 下面这一步可以不要 -sudo apt autoremove xxx -``` -然后再用`apt-get`安装其他软件。 - -## 写在后面 - - -当前只是记录了少许痕迹,随着后续对deepin的深度使用,更多使用建议与问题汇总将记录于此。也欢迎朋友在评论区留言,分享你的常用软件与经验总结! diff --git a/about/Friends.md b/about/Friends.md deleted file mode 100755 index 61da259..0000000 --- a/about/Friends.md +++ /dev/null @@ -1,37 +0,0 @@ - -> 你有一个苹果,我有一个苹果,我们彼此交换,每人还是一个苹果;你有一种思想,我有一种思想,我们彼此交换,每人可拥有两种思想。 - - -## 🥂 友情链接 - -PS:排名不分先后,友情第一,链接第二 ~~ - - - - - - -## 📃 友链申请 - -与我 [ 联系 ](https://bytesfly.github.io/blog/#/about/?id=💌-联系) 或者 去 [博客园](https://www.cnblogs.com/bytesfly/p/awesome-blogs.html) 评论 或者 [github](https://github.com/bytesfly/blog/blob/master/about/Friends.md) 提`issues`or`pull requests`都行,格式大致如下: - -> * 昵称:字节飞扬 -> * 头像:https://img2020.cnblogs.com/blog/1546632/202109/1546632-20210916125244772-353101483.png -> * 主页:[https://www.cnblogs.com/bytesfly/](https://www.cnblogs.com/bytesfly/) -> * 说明:互联网是有记忆的,我想留下一些成长的脚印。 - diff --git a/about/README.md b/about/README.md deleted file mode 100755 index d42f9be..0000000 --- a/about/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# 🎉 关于本站 - -本站源码以及全部内容: [https://github.com/bytesfly/blog](https://github.com/bytesfly/blog) - - - - - -> `docsify`可以快速帮你生成文档网站。不同于 GitBook、Hexo 的地方是它不会生成静态的 .html 文件,所有转换工作都是在运行时。如果你想要开始使用它,只需要创建一个 index.html 就可以开始编写文档并直接部署在`GitHub Pages`。 - -快速上手: [使用GitHub Pages + docsify快速搭建一个站点](https://www.cnblogs.com/bytesfly/p/github-pages-docsify.html) - -部署可参考: [https://docsify.js.org/#/zh-cn/deploy](https://docsify.js.org/#/zh-cn/deploy) - -在已经有了诸如 [博客园](https://www.cnblogs.com/bytesfly) 之类的平台,还需要本站是因为放在`GitHub`上托管,可以使用`Git`追踪博客内容的变更,就像维护代码一样,更加清晰明了,数据也不会丢失。 - -## 📚 Blog - -> 他每晚上崖时,那道人往往和他并肩齐上,指点他如何运气使力。说也奇怪,那道人并未教他一手半脚武功,然而他日间练武之时,竟尔渐渐身轻足健。 -> -> 半年之后,本来劲力使不到的地方,现下一伸手就自然而然的用上了巧劲,原来拼了命也来不及做的招术,忽然做得又快又准。 -> -> 难道那道长教我的竟是内功? -> -> 他怎知过去两年之中,那三髻道人每晚在高崖之顶授他呼吸吐纳之术,虽然未教他半点武艺,但所授的却是上乘内功。 -> -> 原来郭靖心思单纯,极少杂念,修习内功易于精进,远胜满脑子各种念头此来彼去、难以驱除的聪明人,因此不到两年,居然已有小成。 - -**博客,其实更多是写给自己看的**。与其说成写博客,倒不如说是“**学习+思考+总结+提升**”这一整个过程,也是一种能极大提高兴趣和效率的学习方式。而对于读者的作用却是有限的,主要是给读者启发和引导,以及查漏补缺。 - -然后再补充个人一点心得:就软件开发而言,知识、经验、技能不经过二次加工、打磨、升华,形成自己的系统的知识体系,时间一长,难免会变得模糊。此外,闲下来或者无法静心学习工作的时候,刷刷同行的朋友最近在搞哪块,也是件蛮有意思的事。可能在读此篇文章的大多数朋友与我一样,都是同行中最普通不过的平凡人。但是我想说,**技术博客最重要的是你有没有用心去写**,这一点欺骗不了读者的眼睛。 - -始终记得走出校门刚入行时,技术老大就说过,“**一个优秀的程序员应当尽量少做重复性的劳动**”。当然,他说这句话时,是鼓励我们写代码时要多思考、多动脑筋、多花点时间设计,这样才能减少冗余代码,写出更优雅、质量更高的代码。同样我觉得写技术博客也能让程序员减少大量的重复性劳动,把自己摸索过的,`Coding`过的、思考过的、领悟到的记录下来,在工作中用到相关的内容时就能够快速翻阅。 - -互联网是有记忆的,我想留下一些成长的脚印。 - - -## 🐼 Me - -2018年本科毕业于某211大学软件工程专业,在上海工作两年半时间,回家乡合肥工作至今。 - -本人涉及的技术栈有点杂: -- [x] 后端 -- [x] 微服务 -- [x] 大数据 -- [x] 机器学习 - -接触过的编程语言: -- [x] Java -- [x] Python -- [x] Scala -- [x] Golang - -当然,目前用得最顺手的编程语言还是Java。 - - - -## 💌 联系 - -- **Email:** bytesfly@foxmail.com - -- **GitHub:** [https://github.com/bytesfly](https://github.com/bytesfly) - -- **博客园:** [https://www.cnblogs.com/bytesfly](https://www.cnblogs.com/bytesfly) - -- **微信公众号:** 字节飞扬(扫码关注我呀) - -
- -博客内容仅是个人的笔记与总结,有错误之处还请多多指正,欢迎来 [博客园](https://www.cnblogs.com/bytesfly) 讨论! - -## 🍋 致谢 - -- 网站使用 [docsify](https://docsify.js.org/#/zh-cn/) 生成 -- 网站样式基于 [https://notebook.js.org](https://notebook.js.org) 修改 -- 本页面使用 [Typora](https://www.typora.io/) 编辑器编写