背景
一鍵導(dǎo)出功能,一次可以導(dǎo)出大量數(shù)據(jù),最終導(dǎo)出形式為壓縮包。后端需要把生成壓縮包的進(jìn)度實(shí)時(shí)發(fā)給前端
痛點(diǎn)
1.如果用戶導(dǎo)出的數(shù)據(jù)量過(guò)大,后端生成壓縮包時(shí)間過(guò)長(zhǎng),超出http請(qǐng)求的時(shí)間限制,前端會(huì)斷開(kāi)連接
2.用戶需要等待較長(zhǎng)時(shí)間,體驗(yàn)較差
技術(shù)方案
1.前端生成uuid,使用uuid作為websocket的userId,開(kāi)啟websocket
2.后端使用ConcurrentHashMap保存userId與對(duì)應(yīng)的session,key為userId,value為session
3.在導(dǎo)出文件的http請(qǐng)求的參數(shù)中加入uuid,后端根據(jù)uuid找到session,并把文件導(dǎo)出進(jìn)度通過(guò)websocket實(shí)時(shí)發(fā)送到前端
問(wèn)題
以上方案,在單機(jī)系統(tǒng)沒(méi)有問(wèn)題,但如果是分布式系統(tǒng),會(huì)有問(wèn)題。
后端服務(wù)通過(guò)容器部署,假設(shè)是雙實(shí)例,前端websocket連接請(qǐng)求發(fā)到實(shí)例A,但是http請(qǐng)求發(fā)到了實(shí)例B,實(shí)例B上找不到userId對(duì)應(yīng)的session,因此發(fā)送消息給前端失敗。
系統(tǒng)已經(jīng)做了會(huì)話保持,但是是通過(guò)cookie(JSESSIONID)實(shí)現(xiàn)的,而不是nginx的iphash,因此只能控制http請(qǐng)求發(fā)到同一個(gè)實(shí)例,無(wú)法控制websocket連接請(qǐng)求和http請(qǐng)求發(fā)到同一個(gè)實(shí)例
解決方案一
整個(gè)導(dǎo)出功能使用websocket進(jìn)行通信,不使用http請(qǐng)求,所有請(qǐng)求參數(shù)通過(guò)websocket傳輸。
注意事項(xiàng)
websocket消息有長(zhǎng)度限制,需要修改org.apache.tomcat.websocket.textBufferSize
@Configuration
public class WebAppRootContext implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addListener(WebAppRootListener.class);
// websocket消息有長(zhǎng)度限制,默認(rèn)限制是8k個(gè)字符,這里改成1024k
servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize", String.valueOf(1024 * 1024));
}
}
解決方案二
使用nginx配置iphash實(shí)現(xiàn)會(huì)話保持
解決方案三
使用kafka解決websocket多實(shí)例問(wèn)題(實(shí)例指部署后端服務(wù)的容器實(shí)例)
1.每個(gè)實(shí)例維護(hù)一個(gè)ConcurrentHashMap,保存連接到該實(shí)例的userId和session
2.每個(gè)實(shí)例把要發(fā)給客戶端的消息(導(dǎo)出進(jìn)度),通過(guò)kafka廣播模式發(fā)給所有kafka消費(fèi)者
3.消費(fèi)者收到消息后,從消息中提取出userId,判斷該userId對(duì)應(yīng)的session是否存在于本實(shí)例,若存在,則把消息通過(guò)websocket發(fā)到前端,若不存在,直接舍棄消息(因?yàn)閟ession在其他實(shí)例)
使用nginx代理websocket的注意事項(xiàng)
1.客戶端與服務(wù)端之間有nginx時(shí),nginx是七層負(fù)載均衡,即在應(yīng)用層代理真實(shí)客戶端轉(zhuǎn)發(fā)http請(qǐng)求,并代理真實(shí)服務(wù)端把響應(yīng)結(jié)果返回給客戶端,nginx與真實(shí)服務(wù)端的讀超時(shí)時(shí)間限制默認(rèn)是60s,也就是超過(guò)60s會(huì)斷開(kāi)websocket長(zhǎng)連接,需要把該時(shí)間限制改大一點(diǎn)
#改成10分鐘
proxy_read_timeout 600s
2.需要配置把http協(xié)議升級(jí)為ws協(xié)議
Error during WebSocket handshake: Unexpected response code: 404
websocket進(jìn)度條準(zhǔn)確性控制
技術(shù)方案
在執(zhí)行具體業(yè)務(wù)邏輯之前,就計(jì)算好任務(wù)量,假設(shè)一次導(dǎo)出請(qǐng)求,包含ABCDE五個(gè)步驟,那就把任務(wù)量定為5,每執(zhí)行完一個(gè)步驟,就把進(jìn)度增加20%,全都執(zhí)行完畢后,進(jìn)度為100%。
問(wèn)題1
假如五個(gè)步驟的耗時(shí)不同怎么辦,比如ABCDE的耗時(shí)比例為6:1:1:1:1
解決方案
根據(jù)耗時(shí)比例,加權(quán)計(jì)算任務(wù)量,比如把任務(wù)量定為10,其中A占6個(gè)任務(wù)量,完成A后進(jìn)度增加60%
問(wèn)題2
假設(shè)A步驟耗時(shí)1分鐘,那么用戶看到進(jìn)度條1分鐘不動(dòng),然后突然升到60%,體驗(yàn)會(huì)非常差。
解決方案
需要把A步驟繼續(xù)拆分,比如,根據(jù)時(shí)間區(qū)間等入?yún)ⅲ琧ount查詢可以確定A步驟需要查1w條數(shù)據(jù),需要分頁(yè)查詢10次,那么可以把A步驟分成10個(gè)子任務(wù),每個(gè)任務(wù)執(zhí)行完成后,進(jìn)度增加60% / 10 = 6%
問(wèn)題3
問(wèn)題2的解決方案中,A步驟具體需要怎么拆分,很難在一開(kāi)始確定,需要執(zhí)行到具體的代碼段(比如count查詢)才能確定
解決方案(分治算法)
一開(kāi)始計(jì)算任務(wù)量的時(shí)候,還是分配6個(gè)任務(wù)量給A,至于A怎么分配這6個(gè)任務(wù)量,由A執(zhí)行到具體代碼段的時(shí)候再?zèng)Q定。比如執(zhí)行count查詢后發(fā)現(xiàn)需要分頁(yè)查10次,那么每一次分頁(yè)查詢后進(jìn)度增量為60% / 10 = 6%