轮询机制解决后端任务回调问题

  |  
 阅读次数

某天晚上在上班时间偷偷摸鱼的时候,公司的同事的和我探讨一个问题。

问题

同事大概描述了这样一个需求:

现在有一个需求,前端有一个按钮,点击以后会调用后端一个接口,这个接口会根据用户的筛选条件去hadoop上跑任务,将图片的base64转为img然后打包成zip,生成一个下载连接返回给前端,弹出下载框。

hadoop上的这个任务耗时比较久,一般都是10s以上,也就是说如果一直让前端等待,会出现请求超时的问题。

需求现阶段实现

以下是现阶段的实现状况:

用户点击下载按钮后,会把以下筛选条件传到后端,如图:
筛选条件

后端在收到请求后,在去hadoop上跑任务前会向数据库插入这么一条数据:

id name status url
1 1496980062652 0

name - 以生成任务时的时间戳命名
status - 任务状态, 0代表任务正常执行,1代表任务执行完成
url - 打包后zip的下载地址, status0时,url为空,status1时,url有对应下载地址

同事做到这里就卡住了。

解决方案

以生成任务时的时间戳命名真的好吗?

以生成任务时的时间戳命名使得生成的任务和任务的内容没有任何联系,举个例子:同样的筛选条件,用户每次去下载的时候都会在hadoop上重新跑一个任务,生成一条新数据。实际上,同样的筛选条件打包出来的数据应该是一样的,没必要每次下载都去重跑一次任务,造成不必要的时间损耗。更直观来讲,也就是说2个用户使用相同的筛选条件,但是他们创建下载任务的时间不同,导致了他们都在hadoop创建了一个任务和数据库插入了一条数据,实际上他们各自插入的数据除了虽然nameurl不同,但是url下载下来的文件中的内容是一样的。

我的解决方案是将所有筛选条件拼接在一起,然后encodeURIComponent后作为唯一标志。

1
2
let name = [startTime, endTime, sid, client, sign, cuid, number, result];
name = encodeURIComponent(name.join(''));

这样做也就是说同样筛选条件下,name的标志具有唯一性,避免任务重跑。
如果用户在下载zip的时候之前有人创建过相同条件的任务,那么则无许等待,直接就可以进行下载。

后端接口部分设计

再来设计后端接口,根据分析任务一共有三种状态,对应status值如下

  • 未建立任务————- -1
  • 任务运行中————- 0
  • 任务结束,生成url—- 1

接口伪代码如下

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
public function download () {
$startTime = $this->input->post('startTime');
$endeTime = $this->input->post('endeTime');
$sid = $this->input->post('sid');
$client = $this->input->post('client');
$sign = $this->input->post('sign');
$cuid = $this->input->post('cuid');
$number = $this->input->post('number');
$result = $this->input->post('result');
$name = $this->input->post('name');
$status = getTaskStatus($name);
$data = array(
'status' => $status,
'url' => ''
);
if ($status == -1) {
establishTask($startTime, $endTime, $sid, $cuid, $number, $result); // 去hadoop上跑任务
insertSQL($name, 0);
} else if ($status === 0) {
} else if ($status === 1) {
$data['url'] = getTaskUrl($name);
}
echo json_encode($data);
}

getTaskStatus用于从数据库中获取任务状态,当数据库中没有与$name相匹配的数据时则返回-1,有则返回对应的status

接下来进行判断:

$status-1,表明该筛选条件的请求是第一次出现,establishTask创建hadoop任务的同时insertSQL在数据库中插入一条name$name,status0的数据。(establishTask在任务结束时会自动修改数据库中的status1,同时插入url)

$status0,表明该筛选条件对应的任务正在hapdoop上运行。

$status1,表明该筛选条件对应的任务已经完成,通过getTaskUrl从数据库中取得url

最后将data返回给前端。

前端轮询机制

前端需要做的就是根据后端返回的结果来判定是否需要继续请求,也就是所谓的轮询,根据setInterval来进行实现。

伪代码如下

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
const download = () => {
let interval = setInterval(() => {
let {
startTime,
endTime,
sid,
client,
sign,
cuid,
number,
result
} = $scope;
let name = [startTime, endTime, sid, client, sign, cuid, number, result];
name = encodeURIComponent(name.join(''));
let data = {
startTime,
endTime,
sid,
client,
sign,
cuid,
number,
result,
name
};
$http({
method: 'POST',
url: '/downlaod',
data: data,
headers:{'Content-Type': 'application/x-www-form-urlencoded'},
transformRequest: function (data) {
  return $.param(data);
}
})
.then(res => {
if (res.status === 1) {
clearInterval(interval);
window.location.href = res.url;
}
}, error => {
console.log(error);
})
}, 2000);
}

时间设置为2s一次轮询,当response中的status1即获得下载地址,此时通过clearInterval取消interval,关闭轮询。

页面刷新带来的影响

如果hadoop 速度极慢,长时间没反应,用户可能会以为页面卡顿了,从而进行页面的刷新。

由于采取了上面同筛选条件下任务标识唯一的方法,即使刷新页面后,用户再点击下载相同条件的任务后也不会再去handoop上重新跑任务以及插入新的数据,如果任务还在running则等待,任务已经结束则直接下载。

如果以最初的时间戳为name,则会导致任务重跑,用户得重新开始等待。

关于为什么不引入socket.io?

交流中我曾询问过目前项目中是否还有其余与此相似的功能,但据了解暂时只有这一个需求,所以虽然socket.io相比轮询来说更节约性能,但是没有必要为了一个功能而引入一个库,这样做的感觉是得不尝试。这样做的行为类似于你为了使用underscore中的某个方法而引入整个underscore

关于轮询机制和websocket的形象对比

轮询机制

1
2
3
4
5
6
7
8
9
客户端:服务器,你有没有消息要给我啊?
服务器:有。
客户端:服务器,你有没有消息要给我啊?
服务器:没有。
——————————无限重复————————————
客户端:服务器,你有没有消息要给我啊?
服务器:没有。
客户端:服务器,你有没有消息要给我啊?
服务器:有

websocket

1
2
3
4
5
客户端:服务器,你有我的消息了记得call我。
服务器:OK!
——————————当有消息的时候————————
服务器:有你的消息了,客户端。
客户端:收到。