某天晚上在上班时间偷偷摸鱼的时候,公司的同事的和我探讨一个问题。
问题
同事大概描述了这样一个需求:
现在有一个需求,前端有一个按钮,点击以后会调用后端一个接口,这个接口会根据用户的筛选条件去hadoop
上跑任务,将图片的base64
转为img
然后打包成zip
,生成一个下载连接返回给前端,弹出下载框。
hadoop
上的这个任务耗时比较久,一般都是10s
以上,也就是说如果一直让前端等待,会出现请求超时的问题。
需求现阶段实现
以下是现阶段的实现状况:
用户点击下载按钮后,会把以下筛选条件传到后端,如图:
后端在收到请求后,在去hadoop
上跑任务前会向数据库插入这么一条数据:
id | name | status | url |
---|---|---|---|
1 | 1496980062652 | 0 |
name
- 以生成任务时的时间戳命名status
- 任务状态, 0
代表任务正常执行,1
代表任务执行完成url
- 打包后zip
的下载地址, status
为0
时,url
为空,status
为1
时,url
有对应下载地址
同事做到这里就卡住了。
解决方案
以生成任务时的时间戳命名真的好吗?
以生成任务时的时间戳命名使得生成的任务和任务的内容没有任何联系,举个例子:同样的筛选条件,用户每次去下载的时候都会在hadoop
上重新跑一个任务,生成一条新数据。实际上,同样的筛选条件打包出来的数据应该是一样的,没必要每次下载都去重跑一次任务,造成不必要的时间损耗。更直观来讲,也就是说2个用户使用相同的筛选条件,但是他们创建下载任务的时间不同,导致了他们都在hadoop
创建了一个任务和数据库插入了一条数据,实际上他们各自插入的数据除了虽然name
和url
不同,但是url
下载下来的文件中的内容是一样的。
我的解决方案是将所有筛选条件拼接在一起,然后encodeURIComponent
后作为唯一标志。
这样做也就是说同样筛选条件下,name
的标志具有唯一性,避免任务重跑。
如果用户在下载zip
的时候之前有人创建过相同条件的任务,那么则无许等待,直接就可以进行下载。
后端接口部分设计
再来设计后端接口,根据分析任务一共有三种状态,对应status
值如下
- 未建立任务————- -1
- 任务运行中————- 0
- 任务结束,生成
url
—- 1
接口伪代码如下
getTaskStatus
用于从数据库中获取任务状态,当数据库中没有与$name
相匹配的数据时则返回-1
,有则返回对应的status
。
接下来进行判断:
若$status
为-1
,表明该筛选条件的请求是第一次出现,establishTask
创建hadoop
任务的同时insertSQL
在数据库中插入一条name
为$name
,status
为0
的数据。(establishTask
在任务结束时会自动修改数据库中的status
为1
,同时插入url
)
若$status
为0
,表明该筛选条件对应的任务正在hapdoop
上运行。
若$status
为1
,表明该筛选条件对应的任务已经完成,通过getTaskUrl
从数据库中取得url
。
最后将data
返回给前端。
前端轮询机制
前端需要做的就是根据后端返回的结果来判定是否需要继续请求,也就是所谓的轮询
,根据setInterval
来进行实现。
伪代码如下
时间设置为2s
一次轮询,当response
中的status
为1
即获得下载地址,此时通过clearInterval
取消interval
,关闭轮询。
页面刷新带来的影响
如果hadoop
速度极慢,长时间没反应,用户可能会以为页面卡顿了,从而进行页面的刷新。
由于采取了上面同筛选条件下任务标识唯一的方法,即使刷新页面后,用户再点击下载相同条件的任务后也不会再去handoop上重新跑任务以及插入新的数据,如果任务还在running
则等待,任务已经结束则直接下载。
如果以最初的时间戳为name
,则会导致任务重跑,用户得重新开始等待。
关于为什么不引入socket.io?
交流中我曾询问过目前项目中是否还有其余与此相似的功能,但据了解暂时只有这一个需求,所以虽然socket.io
相比轮询来说更节约性能,但是没有必要为了一个功能而引入一个库,这样做的感觉是得不尝试。这样做的行为类似于你为了使用underscore
中的某个方法而引入整个underscore
。
关于轮询机制和websocket
的形象对比
轮询机制
websocket