在最近的一次业务开发中,遇到一次“离奇”的远程接口响应时间问题。

问题可以抽象成下述情况:

有两个服务A, 服务B,因为业务需要,服务A需要调用服务B的接口并接受返回参数,代码逻辑大致是:

服务A代码:

1
2
3
4
5
6
/**
* 服务A,请求服务B的接口
*/
///// 其他业务
String result = HttpKit.post("http://xxxxx/api/method");
///// 其他业务

服务B代码:

1
2
3
4
5
6
7
8
/**
* 服务B,接受请求,执行并返回
*/
@RequestMapping("/api/method")
public Object method() {
// 执行业务代码
return "执行结果";
}

代码也很简单,就是一些基础的SQL查询与更新。

出现的离奇事情是什么呢?

最开始,服务A总是报错为:请求超时,接口19秒才返回。

嗯,首先猜测大概是服务B中的SQL执行慢吧,于是延长HTTP请求的超时时间设置,19秒返回,那我设置30秒总够了吧。

重新尝试,结果发现:请求超时,接口30秒返回结果。

嗯,大概是SQL不稳定吧,于是,再次延长HTTP请求时间到45秒。

再次尝试,结果发现:仍然超时,接口45秒返回结果。

大抵有一种逆转因果论的味道,我请求多长时间,你就跑多长时间,我也没打断点啊!

既然两者存在这么紧密的联系,肯定是存在某种尚未发现的关联,一步一步寻找吧。

发现,A服务,即服务发起端,远程调用请求位于一个数据库事务执行内部,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 服务A
*/
public void method() {


Transaction {
// 更新数据库
SqlExecute("update table_a set a = 1 WHERE id = 1")

///// 其他业务
String result = HttpKit.post("http://xxxxx/api/method");
///// 其他业务
}

}

而B服务,也需要更新同一个表的同一行:

1
2
3
4
5
6
7
8
9
10
/**
* 服务B,接受请求,执行并返回
*/
@RequestMapping("/api/method")
public Object method() {
// 执行业务代码
SqlExecute("update table_a set b = 1 WHERE id = 1")

return "执行结果";
}

如此,造成A服务走完整个事务之后,B服务的SQL才可以执行下去,从而造成A把表锁了,而B等着解锁后才能继续执行,而A又在等着B的执行结果才解锁,从而造成上述的局面。

总结

从此,长了一个经验,数据库事业也可能造成远程请求的超时问题。

课后作业部分

在讲述完Octave的基础操作之后,吴恩达老师给出了一个线性回归的编程作业。并给出了明确的指导文件,可以让我们一步一步的来按照提示实现。

单变量线性回归

本例中使用单变量线性回归预测餐车利润。

数据ex1data1.txt数据截图如下,两列数据分别表示:第一列表示对应城市的人口,第二列表示在该城市的一个餐车的利润。负数表示餐车亏损。

image-20220128100714689

Octave数据加载

1
2
3
4
data = load('ex1data1.txt');
X = data(:, 1);
y = data(:, 2);
m = length(y);

执行截图如下,可以发现通过load函数以及矩阵操作,将X与y值已分别导入到了工作空间。

image-20220128101637300

数据预览

我们首先通过绘图预览一下数据:

1
2
3
plot(X, y, 'rx', 'MarkerSize', 10);
ylabel('Profit in $10,000s');
xlabel('Population of City in 10,000s');

执行截图如下:

image-20220128102039012

梯度下降算法

代价函数J(θ)与假设函数hθ(x):

image-20220128103022245

迭代更新值:

image-20220128102936148

从而逐步使得θj接近使得代价函数去的最小值的最优解。

增加θ0

给数据X增加θ0列,设置学习率α为0.01,设定初始查找位置为θ0 = 0,θ1 = 0

1
2
3
4
X = [ones(m, 1), data(:,1)];
theta = zeros(2, 1);
iterations = 1500;
alpha = 0.01;

计算代价函数

在迭代过程中计算代价函数能够更好的监控梯度下降的过程。

1
2
3
4
function J = computeCost(X, y, theta)
m = length(y);
J = sum((X * theta - y).^2) / (2*m);
end

并通过gradientDescent函数逐步迭代,并且在每一步中保存代价函数的值:

1
2
3
4
5
6
7
8
9
10
11
12
function [theta, J_history] = gradientDescent(X, y, theta, alpha, num_iters)
m = length(y);
J_history = zeros(num_iters, 1);
theta_s=theta;
for iter = 1:num_iters
theta(1) = theta(1) - alpha * sum(X * theta_s - y) / m;
theta(2) = theta(2) - alpha * sum((X * theta_s - y) .* X(:,2)) / m;
theta_s=theta;
J_history(iter) = computeCost(X, y, theta);
end
J_history
end

继续执行ex1.m,执行结果截图:

image-20220128112629425

image-20220128113042370

绘制的完整代价函数3D图像与等高线图像如下所示。

image-20220128112843030

完整的ex1.m代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

data = load('ex1data1.txt');
X = data(:, 1); y = data(:, 2);
m = length(y); % number of training examples

% Plot Data
% Note: You have to complete the code in plotData.m
plotData(X, y);

fprintf('Program paused. Press enter to continue.\n');
pause;

%% =================== Part 3: Cost and Gradient descent ===================

X = [ones(m, 1), data(:,1)]; % Add a column of ones to x
theta = zeros(2, 1); % initialize fitting parameters

% Some gradient descent settings
iterations = 1500;
alpha = 0.01;

fprintf('\nTesting the cost function ...\n')
% compute and display initial cost
J = computeCost(X, y, theta);
fprintf('With theta = [0 ; 0]\nCost computed = %f\n', J);
fprintf('Expected cost value (approx) 32.07\n');

% further testing of the cost function
J = computeCost(X, y, [-1 ; 2]);
fprintf('\nWith theta = [-1 ; 2]\nCost computed = %f\n', J);
fprintf('Expected cost value (approx) 54.24\n');

fprintf('Program paused. Press enter to continue.\n');
pause;

fprintf('\nRunning Gradient Descent ...\n')
% run gradient descent
theta = gradientDescent(X, y, theta, alpha, iterations);

% print theta to screen
fprintf('Theta found by gradient descent:\n');
fprintf('%f\n', theta);
fprintf('Expected theta values (approx)\n');
fprintf(' -3.6303\n 1.1664\n\n');

% Plot the linear fit
hold on; % keep previous plot visible
plot(X(:,2), X*theta, '-')
legend('Training data', 'Linear regression')
hold off % don't overlay any more plots on this figure

% Predict values for population sizes of 35,000 and 70,000
predict1 = [1, 3.5] *theta;
fprintf('For population = 35,000, we predict a profit of %f\n',...
predict1*10000);
predict2 = [1, 7] * theta;
fprintf('For population = 70,000, we predict a profit of %f\n',...
predict2*10000);

fprintf('Program paused. Press enter to continue.\n');
pause;

%% ============= Part 4: Visualizing J(theta_0, theta_1) =============
fprintf('Visualizing J(theta_0, theta_1) ...\n')

% Grid over which we will calculate J
theta0_vals = linspace(-10, 10, 100);
theta1_vals = linspace(-1, 4, 100);

% initialize J_vals to a matrix of 0's
J_vals = zeros(length(theta0_vals), length(theta1_vals));

% Fill out J_vals
for i = 1:length(theta0_vals)
for j = 1:length(theta1_vals)
t = [theta0_vals(i); theta1_vals(j)];
J_vals(i,j) = computeCost(X, y, t);
end
end


% Because of the way meshgrids work in the surf command, we need to
% transpose J_vals before calling surf, or else the axes will be flipped
J_vals = J_vals';
% Surface plot
figure;
surf(theta0_vals, theta1_vals, J_vals)
xlabel('\theta_0'); ylabel('\theta_1');

% Contour plot
figure;
% Plot J_vals as 15 contours spaced logarithmically between 0.01 and 100
contour(theta0_vals, theta1_vals, J_vals, logspace(-2, 3, 20))
xlabel('\theta_0'); ylabel('\theta_1');
hold on;
plot(theta(1), theta(2), 'rx', 'MarkerSize', 10, 'LineWidth', 2);

为什么使用Octave

构建大规模机器学习项目,大多数使用octave/matlab构建算法原型,快速实现算法。

机器学习最常用语言:Octave(开源)/matlab/python、numpy/R。

Octave是开源免费的,而matlab是非常昂贵的。Python是高级编程语言,实现复杂度较高。

Octave的基础操作

指令 含义
pi π
disp() 显示
Sprint(‘…’) 转换成字符串
format long/short 修改浮点数默认显示格式,多小数/少小数
v=[1 2 3]与v=[1;2;3] 分号换行
v=1:0.1:2 从1开始,步长为0.1,一直增加到2
v=1:6 默认步长为1
ones(2,3) 2*3矩阵,所有元素都为1
zeros(2,3) 2*3矩阵,所有元素都为0
w=rand(1,3) 1*3矩阵,随机数,介于0~1之间
randn(1,3) 1*3矩阵,随机数服从高斯分布,均值为0,标准差或方差为1
-6+sqrt(10)*(randn(1,10000)) 均值为-6,随机变量的方差为10,标准差为img
eye(4) 单位矩阵
help 需要帮助的指令

Octave移动数据

指令 含义
sz=size(A) 返回矩阵的行数与列数,作为一个矩阵
length(V) 返回向量/矩阵的最大维度的大小
pwd 查看当前路径
dir, ls 显示当前路径下的文件
load filename.dat 加载数据
load(‘filaname.dat’) 加载数据
who 显示所有变量名
whos 显示所有变量名及详细信息
clear 变量名 删除某个变量
v=priceY(1:10) 将priceY的前10个元素赋值给v
save filename.mat v; 保存矩阵到文件,mat格式(压缩程度高)
save filename.txt v –ascii; 保存矩阵到文件,txt格式
A(2,:) 冒号表示某一行或某一列的全部元素
A([1,3],:) A第一行与第三行的全部元素
A=[A, [10;100;1000]] A增加一列
A=[A; [7,8]] A增加一行
A(:) 将A所有内容拼接为一个列向量
C=[A B]
C=[A; B]

octave 计算数据

A*B 矩阵乘法
A.*B 元素分别相乘
exp(A) e为底数,A中的元素为指数的幂运算
log(A) 返回自然对数,相当于ln(A)
abs(A) 求绝对值
A+1 A中的每个元素都+1
A’ 转置
[val, ind]=max(A) 数组中的最大元素,若A为矩阵,则返回每一列的最大值的行向量。
[row, col]=find(A<3) 返回索引
A=magic(3) 幻方矩阵(任意行、列,对角线和相当等)
sum(A) 求和
prod(A) 求乘积
floor(A) 向下取整
ceil(A) 向上取整
rand(rows, cols) 随机数
max(rand(3), rand(3)) 两个随机数矩阵,求最大值后构成的矩阵
max(A, [], 1) A的第一个维度(每一列)上的最大值(结果为行向量)
max(A, [], 2) A的第二个维度(每一行)上的最大值(结果为列向量)
flipud(A) 矩阵垂直反转
pinv(A) 逆矩阵

数据绘制

plot(t, y, ‘r’) 绘图
hold on 在旧图像的基础上绘图
axis([XMIN XMAX YMIN YMAX]) 设置二维图的x-y坐标范围axis([XMIN XMAX YMIN YMAX ZMIN ZMAX]) 设置三维图的x-y-z坐标范围 设置坐标轴范围
title(‘myplot’) 图的标题
legend(‘sin’, ’cos’) 图线标记image-20220127174236022
close 关闭图像窗口
figure(1) 标记图像窗口
subplot(1,2,1)image-20220127174303565 image-20220127174313917 前两个参数表示将图像窗口划分为1*2个格子,第三个参数表示使用第一个格子
clf 清楚一个图像
A=magic(5);imagesc(A);imagesc(A), colorbar, colormap gray;

1

多元梯度下降法技巧

正确的学习率α能够使代价函数J,每次迭代后J都下降。画出代价函数随迭代步数增加的变化曲线。通过看这种曲线来判断梯度下降算法是否收敛。这种图还能看到算法有没有正常工作。

因此,在进行梯度下降时,总是绘制代价函数随迭代的变化曲线,观察算法是否有效

image-20220118104930190

如果算法不正常工作,最简单的方法:使用更小的学习率(数学上已经证明,足够小的学习率能够使代价函数每次迭代都减小)(如果学习率太小,收敛速度会减慢)

image-20220118105017779

寻找学习率的方法:从小开始,逐步3倍增加,接近最大值得时候,取一个小一点儿的值。

image-20220118105032119

特征与多项式回归

通过定义新特征,你可能会得到一个更好的模型。(例如,直接给定的数据是房子的宽度,长度/深度,我们计算出新的特征值为面积)

image-20220118105113399

多项式回归与线性回归的一致性,以及特征缩放更加重要。

image-20220118105157311

可以自己选择所要拟合的函数:

image-20220118105231495

正规方程

正规方程直接求解最优θ(不需要做特征缩放)

image-20220118105323351

梯度下降需要迭代获取到最优值;正规方程能够直接计算出最优值。

image-20220118105342066

求解各个偏导数为0的方程组。

image-20220118105354794

image-20220118105402482

选择使用梯度下降还是正规方程?

特征值变量的数据决定选择梯度下降还是正规方程。(很难给出确切的临界数字,一般10000以上的特征变量就开始考虑使用梯度下降算法。)

正规方程的算法复杂度是O(n3),n为特征值数量。

image-20220118105517576

正规方程在矩阵不可逆情况下的解决办法

**XT*X**不可逆的原因:

  1. 存在多余特征(存在线性相关的特征)

  2. 特征太多: m<=n 表示样本数量小于特征数量(解决方法:删除多余特征或正规化 regularization)

    image-20220118105828937

多元线性回归

多元线性回归适用于多变量,多特征量的应用场景。

一些数学符号定义

n表示变量的数目;

m表示样本数目;

x(i)表示第i个训练样本:如x(2) = [1416, 3, 2, 40];

xj(i)表示第i个训练样本的第j个变量,如上述的x3(2)=2。

image-20220114152045434

多元线性回归问题

通过如下图的推导,将公式转化成向量的转置乘以向量(向量内积)。

image-20220114152316004

多元线性回归的代价函数与梯度下降算法

注意:在不断的迭代中,不断的更新每个θj (j=0, 1, …, n),需要同步更新。

image-20220114154710210

下图是公式表达从一元线性回归到多元线性回归的推导,仅是符号推导:

image-20220114154946176

特征值缩放

特征值缩放,即各个特征值都在一个相近的范围,这能够使得梯度下降算法更快地收敛,即很快的获的计算结果。

如下图,x1表示尺寸,取值是0-2000;x2表示卧室的数量,取值0-5,为了更快的获取结果,我们需要将其转换为一个相似的取值范围内,通常按照下图所示的方式来处理,即:特征值除以最大值。

image-20220114155650787

尽量使得各个变量的范围都处于-1~1之间,尽量使得各个变量的范围。

下面提供了一种特征值缩放的方法。

均值归一化

image-20220114164059496

如上图所示,其中μi表示变量的平均值,Si表示变量的范围或标准差,即(max - min)。归一化方法:

xi = (xi - μi) / Si

其他

这里提一句求导的一个复合函数求导公式,能帮助我们理解其中求导的过程。

image-20220114164532788

线性回归的一些注意事项

无论导数正负,都可以将变化趋势向最低点靠近

如果你已经在局部最低点了,θ1将不再改变,它将使你的点始终保持在局部最优点上。

image-20220113184823528

梯度下降能收敛到局部最优点,以一个固定的α。(随着越接近最优点,导数值越小)随着梯度下降法的运行,你移动的幅度会自动变得越来越小,直到最终移动幅度非常小,已经收敛到局部极小值。

image-20220113184859024

第一个机器学习算法

算法如下图,需要注意每次需要全部更新所有θ的值。

(如果每次只更新一个,或许也可以获取最优解,但是算法就不是该算法了)。

image-20220113185031506

“Batch” Gradient Descent,其中的“Batch”表示每一次迭代都使用了全部的训练数据。

(在后续学习中,“正规方程组方法”能够不用迭代的求出最优解;但是梯度下降适用于更大的数据集)

线性回归的代价函数图像总是这样的凸函数(convex function)(没有局部最优解,只有一个全局最优解),如下图:

image-20220113185014784

矩阵与向量的基础知识

矩阵:通常用大写字母表示矩阵。

向量:只有一列的矩阵,默认使用1-indexed下标方式,即下标从1开始;通常用小写字母表示向量。

image-20220113185440835

矩阵与向量乘法:

image-20220113185518441

通过矩阵向量相乘,就可以得出所有hθ(x)的预测值:

预测值 = 数据矩阵 * 参数向量

image-20220113185633032

矩阵的乘法:

C的第n列是矩阵A与矩阵B的第n列相乘的结果。

image-20220113185748976

image-20220113185755448

矩阵乘法不满足交换律 AB ≠ BA

矩阵乘法满足结合律:ABC = A*(BC) = (AB)*C

单位矩阵I:对角线为1,其他为0;

image-20220113185920537

不存在逆矩阵的矩阵,称为奇异矩阵,或者,退化矩阵。

吴恩达老师提到,对于算法而言,没有逆矩阵意味着什么?怎么解决?

至少可以这样理解:把哪些没有逆矩阵的矩阵,想象成非常近似为0

矩阵的转置

image-20220113185944000

续上篇

在上一篇文章《吴恩达机器学习课程——单变量线性回归》中,我们了解了线性回归的基本概念以及代价函数的数学表达式。今天继续来研究一下这个数学函数。

img

代价函数的图像

在线性回归中,我们假设其函数为:h(x) = ax + b,我们假设b = 0,则假设函数为:

h(x) = ax

代价函数为:

image-20220112162923346

对于固定的测试数据集合,其中的m, x(1…m),y(1…m)都是固定的,也就是说,J(a)是一个以a为自变量,J(a)值为因变量的函数,自然,我们就可以画出其函数图像。

若只有一个自变量a,则可以使用平面直角坐标系表示(a, J(a))的图像关系,通常情况下,其图像如下所示:

image-20220112163127238

上图左侧是h(x)的图像,右侧是对应每一个可能的取值a,即图中的θ1,J(θ1)的取值图像。

当不忽略b的取值时,代价函数为J(θ0,θ1),其图像很有可能是类似下图的3D曲面图与等高线图:

image-20220112164003170

梯度下降: Gradient descent

梯度下降:一种可以自动找到使代价函数J(θ0,θ1)最小的θ0,θ1的算法。

其思路是:

  1. start with some θ0,θ1, 以某些取值开始,通常令θ0 = 0,θ1 = 0 ;
  2. 不断的改变θ0,θ1的值,降低J(θ0,θ1)
  3. 直到找到一个最小值(可能是全局最小值,也可能是局部最小值)

第一次选择的起始点不同,得到的局部最小值可能不同。

其搜索路径如下图:

image-20220112164518256

梯度下降算法的定义:

image-20220112165727419

其中,

convergence: 收敛
“:=” 表示赋值;
α表示学习率(learning rate),用来控制梯度下降时我们迈出多大的步子,如果α很大,梯度下降就很迅速,我们会用大步子下山;如果α很小,那么我们会迈着很小的小碎步下山。

image-20220112172253401 表示其导数项。

对这个方程,你需要**同步更新(simultaneously update)(θ0,θ1)。吴恩达老师特意强调了,在更新θ0,θ1时,必须同步更新,以下更新方式是错误的:

image-20220112172108297

无论导数正负,都可以将变化趋势向最低点靠近:

image-20220112173100466

不同的α值的搜索效果如下图所示:如果太小会造成搜索过慢,如果太大则有可能不收敛失败。

image-20220112173150810

线性回归

在上一篇文章中,我们介绍了机器学习可以划分为监督学习(Supervised Learning)与无监督学习(Unsupervised Learning),监督学习即一致数据的正确答案:’right answer’ for each example in the data。

回归是指我们预测出一个具体的数值输出。

在线性回归中,有以下几个基本概念:

training set 训练数据集

​ m = 训练样本的数量

​ x = 输入变量/特征

​ y = 输出变量/(预测的)目标变量

​ (x, y) = 一个训练样本

​ (x^(i), y^(i)) = 特定的训练样本,第i个训练样本(i是上标)

​ h = 假设函数,h maps from x’s to y’s

监督学习算法工作一般流程:向学习算法提供训练数据集合,学习算法的任务是输出一个函数,通常以h表示,h表示假设函数,假设函数的作用是作用于新数据,从而得出预测的结果,如下图所示。

image-20220111175047682

当我们设计一个“学习算法”时,一个需要做的事是,决定怎么表示这个假设函数h。若预测的函数h可表示为如下形式:

h(x) = ax + b

则这就是一个一元线性回归,或单变量线性回归。

代价函数

代价函数解决的问题是,如何把最有可能的直线与我们的数据相拟合。

在上面的公式中,a, b被称为模型参数,我们要做的就是谈谈如何选择这两个参数值,要尽量选择参数值,使得在训练集中,给出训练集中的x值,我们能合理准确地预测y的值。用数学语言表达如下:

标准定义:在线性回归中,我们要解决的是一个最小化问题:

目标函数:mina, b = (h(x) - y)2

进一步,某个训练样本i:(img)表示第i个样本, m表示训练样本总数目。

目标函数:

img

进一步:整体目标函数为:

img

其中,img为假设函数。表述为找到img使得上述表达式的值最小

定义一个代价函数:(有时也称为平方误差函数,平方误差代价函数)

img

需要做的就是关于img对函数img求最小值。

也存在其他的代价函数,但平方误差函数式解决问题最常用的手段。

学习吴恩达老师的机器学习课程,记了些笔记,整理出来,与大家分享。

应用场景

机器学习的应用场景主要有4个:

数据挖掘:Data Mini

例如,挖掘Web点击数据、医院病历数据、生物学中的各项数据等。

完成无法编程实现的任务:Applications can’t program by hand

例如,自动无人机,无人驾驶,手写识别任务,自然语言处理(NLP),计算机视觉等。

个性化定制任务:Self-customizing programs

最常见是各个商城平台的商品推荐,例如淘宝、京东,亚马逊等。

了解人类

通过机器学习,通过AI来了解人类的学习机制,了解人类的大脑,逐步实现真正的人工智能。

定义:什么是机器学习

  1. Arthur Samuel (1959) :

机器学习是给予计算机无需明确的编程即可拥有学习的能力。

Field of study that gives computers the ability to learn without being explicitly programmed.

  1. Tom Mitchell (1998):

    学习问题是,对于某个任务T,其性能指标为P,通过从经验E学习,能够对执行任务T时,提高P值。

    拿电脑下棋来说,下棋为Task T,已经下过的棋盘为Experience E,下棋赢得概率为Performance P。

    Well-posed Learning Problem: A computer program is said to learn from experience E with respect to some task T and some performance measure P, if its performance on T, as measured by P, improves with experience E.

机器学习的分类

机器学习可划分为监督学习(Supervised Learning)与无监督学习(Unsupervised Learning)。

监督学习

监督学习,即teach computer how to do something,通俗的来说,就是给定一些数据的正确答案,然后通过学习,使得机器能够产生其他数据的正确答案。Give the algorithm a data set, in which the “right answers” were given, and the task of algorithm was to just produce more of these “right answers”.

比如房价预测,通过过往历年完整的房价数据来“学习”“训练”,使得能够一定程度上预测数值的输出。又比如通过对医院中的病历分析,判断新病人的肿瘤是良性还是恶性。

在监督学习中,每个样品都被明确标明为阳性样本或阴性样本,我们已经被告知了什么是所谓的正确答案。

无监督学习

无监督学习,即learn by itself。在无监督学习中,数据没有任何标签或都具有相同的标签,我们拿到一个数据集,但是不知道要拿它来做什么,也不知道每个数据点究竟是什么,只是被告知这里有一个数据集,你能在其中找到某种结构吗。

比如聚类算法,来分析社交网络,划分市场等等,没有明确的指标。又比如语音识别领域的鸡尾酒会问题,如何从多个混合一个声波信号中分离出多个独立正确的声波。

其他

吴恩达老师推荐使用Octave 开源免费的编程环境软件来学习与实现机器学习算法,当确认算法可以实现后,才用C或者Python等高级语言来实现。

Octave是一个类Matlab的开源程序。

写日志面临的问题

写日志在Web程序中是一个十分基础与常见的需求,其对性能的要求很高。主要需要处理以下问题:

  1. 多线程并发,需要保证顺序性。
  2. 高配IO操作,但IO操作相比其他指令耗时长,性能低。

即一方面需要面对程序端高配的日志写请求,一方面需要受限于系统磁盘相对缓慢写入文件,应该如何处理呢。

双缓冲区

因此,引入双缓冲区机制,一个缓冲区存储应用程序端发送的日志,按照时间顺序依次存储;另一个缓冲区负责向低层磁盘发送写文件请求。

写文件请求执行相对较慢,因此当写文件执行完毕后,通知管理程序,此时可以将另一个缓冲区内容写入磁盘了。

双缓冲区的奇妙之处就在于,两个缓冲区的交换,是通过交换指针来实现的,非常的高效。

部分实现代码如下(其他部分逻辑代码已省略)。

双缓冲区代码,不使用Java现有的线程安全类,后续通过加锁保证数据安全。

1
2
3
4
5
// 负责接收应用程序发来的日志
LinkedList<String> currentBuffer = new LinkedList<>();

// 负责将数据同步到磁盘
LinkedList<String> syncBuffer = new LinkedList<>();

第一个缓冲区,接收应用程序高速写日志请求

1
2
3
4
5
6
7
8
9
10
11

public void log(String content) {
// 加锁保证第一个缓冲区
synchronized(this) {
// 将log写入内存缓冲中,这里不会直接刷入磁盘文件
currentBuffer.add(content);
}

// 将缓冲区中的内容刷到磁盘
logSync();
}

第二缓冲区,向磁盘写日志,并在写入后交换缓冲区指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void logSync() {
synchronized(this) {
// 当前在刷内存缓冲到磁盘中去
if (isSyncRunning) {
// 判断是否第二个缓冲区还在刷
while (isSyncRunning) {
try {
// 释放锁,即允许第一个缓冲区继续接收日志缓存, 然后等待被唤醒
wait(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
// 此时没有人在写磁盘
}

// 交换缓冲区指针
setReadyToSync();

// 设置当前正在同步到磁盘的标志位
isSyncRunning = true;
}

// 刷磁盘,性能最低,不能加锁
logBuffer.flush();

synchronized(this) {
// 同步完磁盘之后,将标志位复位
isSyncRunning = false;
// 唤醒其他等待刷磁盘的线程
notifyAll();
}
}

交换缓冲区指针,功能变更

1
2
3
4
5
public void setReadyToSync() {
LinkedList<String> tmp = currentBuffer;
currentBuffer = syncBuffer;
syncBuffer = tmp;
}

奇妙之处

两个缓冲区各自处理,互不干扰

两个缓冲区很好的解决了应用程序的“快速、多线程”与IO操作的“缓慢,单线程”的矛盾。应该说,引入双缓冲区是一个显而易见的方式。

缓冲区交换

通过交换指针的方式实现两个缓冲区的功能互换,十分巧妙,令人称赞。

总结

你知道吗?电视机里也用着双缓冲机制呢