问题说明
在进行业务开发时,有时会面临耗时请求。也就是前端发给后端的请求,后端需要一定时间进行处理,几分钟或十几分钟,远远超出了一般HTTP请求的timeout。如果不加处理,会带来不好的用户体验。
解决思路
- 面对耗时请求,后端开辟额外进程进行处理,立即返回已经开始处理的信息给前端。
- 前端轮询处理进度,更新页面UI,直到处理完成。(也可以改用WebSocket协议,让后端推送处理进度给前端)
实现案例
前端技术栈: Vue2 + ElementUI
后端技术栈:Python3 + Flask
Python 多进程及进程间通讯
- 耗时请求属于计算密集型任务,基于Python语言的特性,选择多进程而不是多线程。
- 进程间通讯有管道Pipe、队列Queue以及共享内存Manager,基于仅需要记录进度,主进程和子进程不需要过多的通讯,选择了共享内存。
from multiprocessing import Process
from multiprocessing.sharedctypes import Value
# 全局变量,其中:Value为共享内存变量
# 因为耗时请求和轮询请求属于不同的请求,所以不能用Flask的g
# 又因为共享内存变量无法Json化,所以不能用Flask的session
update_progress_info = {
# 全部数据
'total': 0,
# 以及处理的数据
'done': Value('i', 0),
# 状态:0 表示结束、未启动;1 表示进行中
'status': Value('i', 0)
}
# 后台子进程函数
def _update_all_person_tags(numbers, progress_info):
import time
for number in numbers:
progress_info['done'].value += 1
# 模拟密集计算
time.sleep(0.01)
# 处理完成,设置状态
progress_info['status'].value = 0
# 耗时请求处理函数
def update_all_person_tag():
if update_progress_info['status'].value:
return {'msg': '成功', 'code': 0, 'data': '更新程序已启动!'}
# 查询所有员工工号
mysql_cursor = db.connect_mysql()
sql = "select number from person"
mysql_cursor.execute(sql)
numbers = [number[0] for number in mysql_cursor.fetchall()]
# 修改全局变量
update_progress_info['total'] = len(numbers)
update_progress_info['done'].value = 0
update_progress_info['status'].value = 1
# 配置子进程
update_process = Process(target=_update_all_person_tags,
args=(numbers, update_progress_info))
# 配置其为守护进程,让主进程不必等待其结束
update_process.daemon = True
# 启动
update_process.start()
print(f"开启后台进程,其进程号为{update_process.pid}")
return {'msg': '成功', 'code': 0, 'data': '更新程序已启动!'}
JavaScript轮询
- 为了不阻塞页面,采用基于setTimeout的异步请求
- 只有在当前请求完成后,才嵌套调用下一次请求
askProgress() {
tagApi.askProgress().then(
(res) => {
// 每次接收后端返回的进度数据,更新UI
this.totalNum = res.data.total
this.updatedNum = res.data.done
this.updateStatus = res.data.status
// 判断是否更新完成
if (this.updateStatus == 0) {
// 提示更新完成
Message({
message: '更新已完成!',
type: 'success',
duration: 2 * 1000
})
} else {
// 未完成,则开启新一轮问询
this.timeoutId = setTimeout(() => {
clearTimeout(this.timeoutId)
this.askProgress()
}, 500)
}
},
(err) => {
console.log('err', err)
}
)
},
updatePersonTags() {
// 发请求通知后端开始更新
tagApi.updateAllPersonTag().then(
(res) => {
Message({
message: res.data,
type: 'success',
duration: 2 * 1000
})
// 开始轮询更新进度条
this.askProgress()
},
(err) => {
console.log('err', err)
}
)
}
效果
结合ElementUI的进度条组件,实现效果如下:
总结
结合多进程和轮询,完成了耗时请求处理进度的前端实时显示功能,存在以下可改进的地方:
- 耗时的处理进程目前是单进程,可使用多个进程同时对耗时任务进行处理,充分利用CPU的性能。
- 前端的轮询代码是嵌套调用,可采用Promise语言特性避免嵌套回调。