在Web應用的開發中,不論是網站還是服務接口,我們可能會遇到來自客戶端的某個請求,而這請求的背后,隱藏着要執行的大量的繁重任務,如果我們在后台程序中,同步的進行處理,那么程序執行時間比較久,用戶體驗是糟糕的,甚至會導致502執行超時。針對這種情況,有很多成熟的解決方案【據我粗淺的認知,使用隊列是一個較好的方案】,但實現起來稍顯復雜和繁瑣。如果我們對要異步執行的任務沒有特別的要求【比如失敗重試或異步任務執行完畢后的事件回調】,那么,可以用一種非常輕松的方式來簡單實現:nohup 要執行的命令 > /dev/null 2>&1 &
。
如果,你看到這里,覺得這沒什么新奇的,那說明你水平挺好,至少比我要好【發自真心】。也希望你在離開之前,說一說有沒有啥更好的方法,分享一下。
應用場景
目前,我在兩種具體的場景下,實際使用了這種異步執行任務的方法。
1、早前,做一個網站,數據是另一個同事從第三方采集的,采集下來的數據,需要導入到我的數據庫中,於是,我在網站后台提供了一個功能,一個文件上傳導入的按鈕,當同事將他采集的數據,通過文件上傳的方式,保存在服務器上的時候,后台程序會讀取這個文件的內容,並基於里面的數據,進行一下必要的分析,最后將分析后的數據,通過SQL寫入到數據庫中。此過程執行的快與慢,取決於數據文件的大小,一個幾千行數據的文件,最后可能要執行一分多鍾。如果采用傳統的同步執行,那么從文件上傳 -> 數據分析 -> 寫入數據庫 這整個過程中,瀏覽器都在轉圈圈,若是時間再長點,執行就超時了,前功盡棄。所以,這里我采用了異步執行任務的方式,在數據文件成功上傳后,服務端直接響應回瀏覽器,顯示一個“數據導入成功,正在進行處理”的提示,整個前端的交互就到此為止,后面數據分析和寫入數據庫,就交給另一個單獨的進程去處理了。
2、就在前兩天,我剛用這種方式,寫了這樣的功能。我們做了一個APP【用APICloud做的一個不入流的APP】,當用戶在使用APP發布了一個內容后,我們需要調用百度AI的內容審核接口,對用戶發布的文字和圖片,進行自動審核,如果發現其中包含不良信息,則自動審核不過。而這個調用百度接口的過程,是略微有點耗時的,這主要取決於用戶發布的內容中所包含圖片的多少,圖多自然百度接口處理響應的慢。同樣的,如果用同步的方式,用戶發布內容 -> 調用百度接口 -> 等待接口返回數據 -> 判斷是否審核通過,太耗時了,這樣在用戶看來,就是內容發出去之后,等待了好幾秒,甚至十幾秒,最后才有反應,這用戶體驗就太差了。所以,可以做到當用戶發布了內容后,立刻提示“發布成功,正在審核”的字樣,而在幾秒鍾之后,用戶就看到他剛剛發布的內容審核通過,並出現在內容列表中的時候,是多么自然的一個過程。
實現思路
所以,有的時候,異步的處理一下任務,還是很有必要的。既然咱們開頭說到,在Web應用的開發中,那就跳不出Linux服務器,現如今除了.NET系的可能還會部署在Windows上【博客園貌似就不是】,其他的后端應用,基本都會部署在Linux上,而我們開頭提到的實現方式,就是在Linux下的命令實現。
首先,要實現程序的異步執行,大概有兩種方式:線程 和 進程【說大概,是因為聽說有的語言還支持協程。嗯?什么鬼? -_-!!!】。像Java這種,支持線程的編程語言,異步執行可以用線程實現,也可以用進程實現【Runtime.exec()】;而像PHP這種,在默認情況下,是沒有線程的,並且大家普遍也都不在PHP下使用線程,那么,這就只能通過其內部函數,調用外部進程,去實現異步任務的執行。
在PHP下,執行一個外部程序,並且要求這個外部程序是在后台運行,且不會讓你的宿主程序等待掛起【宿主也就是執行調用外部命令的PHP程序】,有一點要特別注意,這是在官方手冊的exec函數中特意提到的:
If a program is started with this function, in order for it to continue running in the background, the output of the program must be redirected to a file or another output stream. Failing to do so will cause PHP to hang until the execution of the program ends.
意思就是,為了讓外部程序在后台運行,這個外部程序的輸出【指標准輸出【像 Python中的 print,PHP中的 echo 和 Java中的 System.out.print】和標准錯誤】必須重定向到一個文件或另一個輸出流中。否則,宿主程序可能會掛起,等待外部程序執行完畢后,才會終止結束他的生命周期。
所以,文章開頭提到的命令中的 > /dev/null 2>&1
,就是用來重定向標准輸出和標准錯誤,將其寫入 /dev/null
文件的,以使得宿主程序在調用外部程序,讓其后台運行后,自己會立刻執行后續代碼,直到結束,可以很快的結束自己的生命周期,而此時,外部程序,還正在默默的努力運行中。
當我在寫這篇文章之前,還特意查了一下,在Java下用Runtime.exec()調用外部程序的實現方式,發現有篇文章提到了這樣一點:
意思也是要將外部程序的輸出重定向出來,這與PHP官方手冊中提到的注意事項,完全一致。
具體實現
下面,我們就來解釋一下 nohup 要執行的命令 > /dev/null 2>&1 &
這條命令的含義。
首先,是 要執行的命令
,比如我上文提到的,調用百度AI,進行內容審核,那么命令就像 php /www/wwwroot/app_service/artisan baidu:censor 文章ID
這樣,我這里用的PHP的Laravel框架,至於你用什么語言,什么框架,怎么寫這個 要執行的命令
,也要視你的情況而定。
其次,要讓一個程序,在后台運行,需要在命令后面加上 &
【也就是末尾的 &】,以告訴系統,我要執行的命令,是一個需要后台運行的程序。
然后,為了防止我們的宿主程序等待掛起,我們需要重定向外部程序的輸出,於是就加上了 > /dev/null 2>&1
,> /dev/null
是指將標准輸出重定向到 /dev/null
文件,而后面的 2>&1
是指,將標准錯誤也重定向到跟前面標准輸出一樣的位置。而 /dev/null
是一個不存在的文件,所以 > /dev/null 2>&1
的整體意思是,這個外部程序執行時,他產生的所有標准輸出和標准錯誤【就是報錯信息】,統統不要保存,我不要看到。當然,如果你在調用外部程序后,發現沒有按預期執行,可能是這個外部程序報錯了,你可以將輸出,重定向到一個真實的文件,以保存外部程序的輸出信息,便於你排錯。
最后,是 nohup
。當你通過指定 &
讓外部程序在后台運行后,如果此時你關閉、退出你的 terminal 終端【就是黑乎乎的命令行窗口】,那么此時你剛剛正在后台運行的外部程序,也會終止。為了避免這個問題,需要在開頭加上 nohup
,來告訴系統,關閉、退出終端時,別把我剛剛執行的外部程序的這個后台進程殺掉啊!!!
好了,具體實現要用到的命令,解釋清楚了,那在各個語言中,如何實現呢?這個,我相信各個語言中,都有調用外部程序的方式,你可以自己研究下。我用PHP多一點,最后就貼一下PHP的實現方法:
exec('nohup php /www/wwwroot/app_service/artisan baidu:censor 文章ID > /dev/null 2>&1 &');
【Laravel】
exec('nohup php /www/wwwroot/app_service/baidu_censor.php 文章ID > /dev/null 2>&1 &');