早春二月,研發倍忙,雜花生樹,群鷗竟飛。為什麼?因為春季招聘,無論是應屆生,還是職場老鳥,都在摩拳擦掌,秣馬厲兵,準備在面試場上一較身手,既分高下,也決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原生協程的非同步寫法。
僅次於三程問題的明星面試題,一般情況下,大家都會說淺拷貝修改複製物件會影響原物件,而深拷貝不會,但其實,淺拷貝會有三種細分的情況:
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)搜尋劉悅的技術部落格查閱,歡迎諸君品鑑。