躬身入局,乾貨分享,2023年春招後端技術崗(Python)面試實戰教學,Offer今始為君發

2023-02-20 12:00:55

早春二月,研發倍忙,雜花生樹,群鷗竟飛。為什麼?因為春季招聘,無論是應屆生,還是職場老鳥,都在摩拳擦掌,秣馬厲兵,準備在面試場上一較身手,既分高下,也決Offer,本次我們打響春招第一炮,躬身入局,讓2023年的第一個Offer來的比以往快那麼一點點。

開啟某垂直招聘平臺,尋找2023年的第一個獵物:

投遞簡歷之後,如約進行面試。

筆試題

正規公司的面試一般都是筆試先行,筆試題的作用非常務實,就是直接篩掉一批人,提高面試效率,需要注意的是,在這個環節中,往往無法用搜尋引擎進行檢索,所以,你的大腦就是Python直譯器,你的筆將會代替程式的輸出:

# 實現字串反轉,以逗號作為切割符,切割的子串以單詞作為單元反轉  
# 輸入:hello world, god bless you  
# 輸出:world hello, you bless god

這道題網上沒有原題,但其實並不難,考點在於應聘者對於Python基礎和複合資料型別內建方法的熟悉程度,題目中所謂的字串反轉並不是真正意義的字串反轉,而是以單詞為單元的反轉,同時加入了逗號分割邏輯,所以只要對字串內建方法split,rstrip和列表內建方法join以及reverse的用法足夠了解,就可以直接寫出解法:

def reseverWords(s:str) ->  str:  
    all_str =   ""  
    s   =    s.split(',')  
    for x   in  s:  
        lis=    x.split()  
        lis.reverse()  
        all_str +=  ' '.join(lis)+', '  
    all_str=all_str.rstrip(', ')  
    return  all_str  
print(reseverWords(str1))

第二題是SQL語句題目,請寫一條sql,按照地區的分組聚合資料進行排序:

id    name       location  
--    -----      --------  
1     Mark       US  
2     Mike       US  
3     Paul       Australia  
4     Pranshu    India  
5     Pranav     India  
6     John       Canada  
7     Rishab     India

排序後結果:

id    name       location  
--    -----      --------  
4     Pranshu    India  
5     Pranav     India  
7     Rishab     India  
1     Mark       US  
2     Mike       US  
3     Paul       Australia  
6     John       Canada

這道題也無法在網上查證,一般的分組聚合只是查一個數,這個是按照數量進行排序,並且其實並不展示數量,也可以理解為展示的為分組資料的明細排行榜:

SELECT x.*   
  FROM my_table x   
  JOIN (SELECT location, COUNT(*) total FROM my_table GROUP BY location) y  
    ON y.location = x.location  
 ORDER   
    BY total DESC  
     , id;

思路是先分組,隨後按照分組的聚合資料根據地區欄位連表排序即可。

自我介紹

通過筆試題篩選後,進入自我介紹環節,一般介紹技術棧和簡單的專案經歷即可,參考範例:

您好(下午好/上午好),我是19年畢業的,在RD(Research and Development engineer即研發工程師崗位)崗差不多有三年左右的工作經驗,一開始在一家創業型公司起步,當時主力開發語言是python,,使用mtv架構,在公司主要和業務打交道,開發和維護後臺的API,大概沉澱了兩年左右吧,我跳槽到了第二家公司,薪酬實現了double,在新的技術團隊裡,我接觸到了前後端分離專案,也學習了非同步程式設計思想,主力框架是tornado,前端技術也有所涉獵,比如vue框架,瞭解了資料雙向繫結理念,同時也學習了在業務解耦和服務封裝層面比較流行的docker容器技術,這項技術使我平時開發和測試工作都提高了效率,最近一年左右吧,我經常使用的web框架是tornado,這個框架我個人非常喜歡,它的非同步非阻塞特性讓我對非同步程式設計思想的認識更深入了。我也嘗試過remote這種工作形式,也鍛鍊了我在團隊中的溝通能力,其實三四年下來,做過的東西解決過的問題也挺多的,待過大團隊也經歷過小團隊,給我的感覺就是網際網路企業隨著發展,技術和行業邊界其實是越來越模糊的,也就是說技術都是具有相通性的,我個人來講,優勢就是技術涉獵比較廣,前後端都接觸過,踩得坑也比較多,在特定領域有一定的深入,比如非同步程式設計這塊。另外我覺得搞開發的,學習能力,總結能力很重要,所以我一直保持著寫技術部落格的習慣,這樣經過沉澱,可以提高一個人的分析能力,也就是解決問題的能力,我的介紹完了,謝謝。

程序、執行緒和協程的區別

程序、執行緒和協程,從來就是Python面試中聚訟不休的一個話題,只要我們還在使用Python,就一定逃離不了三程問題:

程序

首先明確一下程序和執行緒的概念,程序系統進行資源分配的基本單位,一臺機器上可以有多個程序,每個程序執行不同的程式,每個程序相對獨立,擁有自己的記憶體空間,隔離性和穩定性比較高,程序之間互不影響,但是資源共用相對麻煩,系統資源佔用相對高,同時程序可以利用cpu多核資源,適合cpu密集型任務,比如一些統計計算任務,比如計算廣告轉化率,uv、pv等等,或者一些視訊的壓縮解碼任務,程序還有一個使用場景,就是後期部署專案的時候,nginx反向代理後端服務,往往需要開啟多個tornado服務來支援後臺的並行,就是利用了多程序的互不干擾,就算某個程序僵死,也不會影響其他程序,程序使用的是mulitprossing庫 ,往往是先宣告程序範例,裡面可以傳入消費方法名稱和不定長引數args,然後將範例放入指定程序數的容器中(list),通過迴圈或者列表推導式,使用start方法開啟程序,join方法阻塞主程序。

執行緒

執行緒是系統進行資源排程的最小單位,它屬於程序,每一個程序中都會有一個執行緒,由於執行緒操作是單程序的,所以執行緒之間可以共用記憶體變數,互相通訊非常方便,它的系統開銷比程序小,它是執行緒之間由於共用記憶體,會互相影響,如果一個執行緒僵死會影響其他執行緒,隔離性和穩定性不如程序,同時,執行緒並不安全,如果對同一個物件進行操作,需要手動加鎖,另外從效能上講,多執行緒會觸發python的全域性直譯器鎖,導致同一時間點只會有一個執行緒執行的交替執行模式,執行緒適用於io密集型任務,所謂io密集型任務就是大量的硬碟讀寫操作或者網路tcp通訊的任務,一般就是爬蟲和資料庫操作,檔案操作非常頻繁的任務,比如我負責開發的稽核系統,需要同時對mysql和redis有大量的讀寫操作,所以我使用多執行緒進行消費。執行緒使用的是Threading庫 ,往往是先宣告執行緒範例,裡面可以傳入消費方法名稱和不定長引數args,然後將範例放入指定執行緒數的容器中(list),通過迴圈或者列表推導式,使用start方法開啟執行緒,join方法阻塞主執行緒。

協程

協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制,不像程序和執行緒是系統態,所以在不主動切換協程的情況下,操作全域性變數的時候,可以無需加鎖(這裡有坑,協程庫內建也是有鎖的,但是看場景,如果使用場景內沒有主動切換協程(await)寫操作就不需要加鎖,如果單協程執行過程中,主動切換了協程,寫操作則需要加鎖 協程是否加鎖問題),只需要判斷資源狀態即可,效率非常高,同時協程是單執行緒的,即可以共用記憶體,又不需要系統態的執行緒切換,同時也不會觸發gil全域性直譯器鎖,所以它效能比執行緒要高。具體使用場景和執行緒一樣,適合io密集型任務,所謂io密集型任務就是大量的硬碟讀寫操作或者網路tcp通訊的任務,一般就是爬蟲和資料庫操作,檔案操作非常頻繁的任務,比如我負責開發的稽核系統,需要同時對mysql和redis有大量的讀寫操作,所以我後期將多執行緒改造成協程進行消費。協程我使用的python原生協程庫asyncio庫,首先通過asyncio.ensure_future(doout(4))方法建立協程物件,然後根據當天稽核員數量指定開啟協程數,和多執行緒以及多程序的區別是,協程既可以直接傳實參,也可以傳不定長引數,很方便,然後通過await asyncio.gather(*tasks)方法啟動協程,需要注意的是,主方法需要宣告成async方法,並且通過asyncio.run(main())來啟動。協程雖然是python非同步程式設計的最佳方式,但是我認為它也有缺點,那就是非同步寫法導致程式碼可讀性下降,同時對程式設計人員的綜合素質要求高,並不是所有人都能理解協程的工作方式,以及python原生協程的非同步寫法。

Python中的深拷貝和淺拷貝

僅次於三程問題的明星面試題,一般情況下,大家都會說淺拷貝修改複製物件會影響原物件,而深拷貝不會,但其實,淺拷貝會有三種細分的情況:

1.拷貝不可變物件:只是增加一個指向原物件的參照,改變會互相影響。

>>> a = (1, 2, [3, 4])  
>>> b = copy.copy(a)  
>>> b  
... (1, 2, [3, 4])  
# 改變一方,另一方也改變  
>>> b[2].append(5)  
>>> a  
... (1, 2, [3, 4, 5])

2.拷貝可變物件(一層結構):產生新的物件,開闢新的記憶體空間,改變互不影響。

>>> import copy  
  
>>> a = [1, 2, 3]  
>>> b = copy.copy(a)  
>>> b  
... [1, 2, 3]  
# 檢視兩者的記憶體地址,不同,開闢了新的記憶體空間  
>>> id(b)  
... 1833997595272  
>>> id(a)  
... 1833997595080  
>>> a is b  
... False  
# 改變了一方,另一方不會改變  
a = [1, 2, 3]    b = [1, 2, 3]  
>>> b.append(4)  
>>> a  
... [1, 2, 3]  
>>> a.append(5)  
>>> b  
... [1, 2, 3, 4]

3.拷貝可變物件(多層結構):產生新的物件,開闢新的記憶體空間,不改變包含的子物件則互不影響、改變包含的子物件則互相影響。

>>> import copy  
  
>>> a = [1, 2, [3, 4]]  
>>> b = copy.copy(a)  
>>> b  
... [1, 2, [3, 4]]  
# 檢視兩者的記憶體地址,不同,開闢了新的記憶體空間  
>>> id(b)  
1833997596488  
>>> id(a)  
1833997596424  
>>> a is b  
... False  
# 1.沒有對包含的子物件進行修改,另一方關我卵事  
a = [1, 2, [3, 4]]    b = [1, 2, [3, 4]]  
>>> b.append(5)  
>>> a  
... [1, 2, [3, 4]]  
>>> a.append(6)  
>>> b  
... [1, 2, [3, 4], 5]  
# 2.對包含的子物件進行修改,另一方也隨之改變  
a = [1, 2, [3, 4]]    b = [1, 2, [3, 4]]  
>>> b[2].append(5)  
>>> a  
... [1, 2, [3, 4, 5]]  
>>> a[2].append(6)  
>>> b  
... [1, 2, [3, 4, 5, 6]]

高並行如何進行處理的

既然JD(Job Describe)中提到了高並行,那麼就一定會問高並行問題,一般情況下,涉及高並行場景的基本上都是外部系統,此時需要簡單介紹一下系統的容量是多少,比如有註冊使用者數、日活、QPS等等。然後就是提供具體方案,一般的手段是加快取,資料庫讀寫分離,資料庫 sharding 等等。高並行背景下,整個系統瓶頸一般都在資料庫。

除了上述的一些常規方案,業內最常用的緩解高並行的手段是使用非同步任務佇列:

為了解決生產者和消費者過度耦合的效率低下問題,我設計了一個緩衝區,生產者不會直接和消費者產生關係,而是通過緩衝區解耦,這個緩衝區就是非同步任務佇列,佇列容器我採用redis資料庫,因為redis效能優勢比較明顯,同時內建的list資料型別比較契合佇列這種資料結構,工具類內建了,初始化方法,入隊方法,出隊方法,佇列長度,以及查重唯一方法。每當商戶提交表單,此時並不會修改狀態,而是將表單資料入庫,同時將商戶uid進行入隊操作,遵循fifo原則,在消費者端使用非同步的方式進行消費,也就是出隊操作,每一個執行緒對應一個稽核員,通過消費方法進行傳參,每次將出隊的商戶uid和執行緒傳入的稽核員id進行組合分配,出隊之後並行數已經得到了控制,隨後在mysql端進行update操作,達到非同步分配稽核的目的。

保持冪等性

如果面試中提到了非同步任務佇列(訊息佇列),那麼冪等性操作幾乎一定會在後續的問題中提及,所謂冪等性,簡單來說就是對於同一個系統,在同樣條件下,一次請求和重複多次請求對資源的影響是一致的,就稱該操作為冪等的。比如說如果有一個介面是冪等的,當傳入相同條件時,其效果必須是相同的。在RabbitMQ中消費冪等就是指給消費者傳送多條同樣的訊息,消費者只會消費其中的一條。例如,在一次購物中提交訂單進行支付時,當網路延遲等其他問題造成消費者重新支付,如果沒有冪等性的支援,那麼會對同一訂單進行兩次扣款,這是非常嚴重的,因此有了冪等性,當對同一個訂單進行多次支付時,可以確保只對同一個訂單扣款一次。

具體手段:

事實上,當稽核任務出隊之後,如果在消費端出現意外,這個意外包含但不限於出對後tornado宕機、mysql宕機等等,導致出隊任務沒有進行流程化處理,所以我採用了ack驗證機制,也就是緩衝區佇列從單佇列升級為雙佇列,把rpop出隊改成redis內建的rpoplpush的原子性操作,出隊後立即進入確認佇列,在消費端完成稽核任務後,對ack佇列進行確認移除操作,如此,一次審批任務才算完結,如果任務生命週期內,任務一直存在於確認佇列沒有出隊,那麼輪詢任務會將任務id移出確認佇列,重新在緩衝區佇列進行入隊操作,這樣就避免了,僵審任務的問題。

結語

技術面試雖然是一種資訊不對等的較量,但是隻要認真研究JD(Job Describe),做好相關的知識儲備,基礎常識不要翻車(包含但不限於Python基礎/資料庫基礎),那麼作為應聘者拿一個Offer也不是想象中的那麼難,本次面試的實戰錄音可以在B站(Youtube)搜尋劉悅的技術部落格查閱,歡迎諸君品鑑。