早春二月,研發(fā)倍忙,雜花生樹,群鷗竟飛。為什么?因為春季招聘,無論是應(yīng)屆生,還是職場老鳥,都在摩拳擦掌,秣馬厲兵,準(zhǔn)備在面試場上一較身手,既分高下,也決Offer,本次我們打響春招第一炮,躬身入局,讓2023年的第一個Offer來的比以往快那么一點點。
打開某垂直招聘平臺,尋找2023年的第一個獵物:

投遞簡歷之后,如約進(jìn)行面試。
筆試題
正規(guī)公司的面試一般都是筆試先行,筆試題的作用非常務(wù)實,就是直接篩掉一批人,提高面試效率,需要注意的是,在這個環(huán)節(jié)中,往往無法用搜索引擎進(jìn)行檢索,所以,你的大腦就是Python解釋器,你的筆將會代替程序的輸出:
# 實現(xiàn)字符串反轉(zhuǎn),以逗號作為切割符,切割的子串以單詞作為單元反轉(zhuǎn)
# 輸入:hello world, god bless you
# 輸出:world hello, you bless god
這道題網(wǎng)上沒有原題,但其實并不難,考點在于應(yīng)聘者對于Python基礎(chǔ)和復(fù)合數(shù)據(jù)類型內(nèi)置方法的熟悉程度,題目中所謂的字符串反轉(zhuǎn)并不是真正意義的字符串反轉(zhuǎn),而是以單詞為單元的反轉(zhuǎn),同時加入了逗號分割邏輯,所以只要對字符串內(nèi)置方法split,rstrip和列表內(nèi)置方法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,按照地區(qū)的分組聚合數(shù)據(jù)進(jìn)行排序:
id name location
-- ----- --------
1 Mark US
2 Mike US
3 Paul Australia
4 Pranshu India
5 Pranav India
6 John Canada
7 Rishab India
排序后結(jié)果:
id name location
-- ----- --------
4 Pranshu India
5 Pranav India
7 Rishab India
1 Mark US
2 Mike US
3 Paul Australia
6 John Canada
這道題也無法在網(wǎng)上查證,一般的分組聚合只是查一個數(shù),這個是按照數(shù)量進(jìn)行排序,并且其實并不展示數(shù)量,也可以理解為展示的為分組數(shù)據(jù)的明細(xì)排行榜:
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;
思路是先分組,隨后按照分組的聚合數(shù)據(jù)根據(jù)地區(qū)字段連表排序即可。
自我介紹
通過筆試題篩選后,進(jìn)入自我介紹環(huán)節(jié),一般介紹技術(shù)棧和簡單的項目經(jīng)歷即可,參考示例:
您好(下午好/上午好),我是19年畢業(yè)的,在RD(Research and Development engineer即研發(fā)工程師崗位)崗差不多有三年左右的工作經(jīng)驗,一開始在一家創(chuàng)業(yè)型公司起步,當(dāng)時主力開發(fā)語言是python,,使用mtv架構(gòu),在公司主要和業(yè)務(wù)打交道,開發(fā)和維護(hù)后臺的API,大概沉淀了兩年左右吧,我跳槽到了第二家公司,薪酬實現(xiàn)了double,在新的技術(shù)團(tuán)隊里,我接觸到了前后端分離項目,也學(xué)習(xí)了異步編程思想,主力框架是tornado,前端技術(shù)也有所涉獵,比如vue框架,了解了數(shù)據(jù)雙向綁定理念,同時也學(xué)習(xí)了在業(yè)務(wù)解耦和服務(wù)封裝層面比較流行的docker容器技術(shù),這項技術(shù)使我平時開發(fā)和測試工作都提高了效率,最近一年左右吧,我經(jīng)常使用的web框架是tornado,這個框架我個人非常喜歡,它的異步非阻塞特性讓我對異步編程思想的認(rèn)識更深入了。我也嘗試過remote這種工作形式,也鍛煉了我在團(tuán)隊中的溝通能力,其實三四年下來,做過的東西解決過的問題也挺多的,待過大團(tuán)隊也經(jīng)歷過小團(tuán)隊,給我的感覺就是互聯(lián)網(wǎng)企業(yè)隨著發(fā)展,技術(shù)和行業(yè)邊界其實是越來越模糊的,也就是說技術(shù)都是具有相通性的,我個人來講,優(yōu)勢就是技術(shù)涉獵比較廣,前后端都接觸過,踩得坑也比較多,在特定領(lǐng)域有一定的深入,比如異步編程這塊。另外我覺得搞開發(fā)的,學(xué)習(xí)能力,總結(jié)能力很重要,所以我一直保持著寫技術(shù)博客的習(xí)慣,這樣經(jīng)過沉淀,可以提高一個人的分析能力,也就是解決問題的能力,我的介紹完了,謝謝。
進(jìn)程、線程和協(xié)程的區(qū)別
進(jìn)程、線程和協(xié)程,從來就是Python面試中聚訟不休的一個話題,只要我們還在使用Python,就一定逃離不了三程問題:
進(jìn)程
首先明確一下進(jìn)程和線程的概念,進(jìn)程系統(tǒng)進(jìn)行資源分配的基本單位,一臺機器上可以有多個進(jìn)程,每個進(jìn)程執(zhí)行不同的程序,每個進(jìn)程相對獨立,擁有自己的內(nèi)存空間,隔離性和穩(wěn)定性比較高,進(jìn)程之間互不影響,但是資源共享相對麻煩,系統(tǒng)資源占用相對高,同時進(jìn)程可以利用cpu多核資源,適合cpu密集型任務(wù),比如一些統(tǒng)計計算任務(wù),比如計算廣告轉(zhuǎn)化率,uv、pv等等,或者一些視頻的壓縮解碼任務(wù),進(jìn)程還有一個使用場景,就是后期部署項目的時候,nginx反向代理后端服務(wù),往往需要開啟多個tornado服務(wù)來支持后臺的并發(fā),就是利用了多進(jìn)程的互不干擾,就算某個進(jìn)程僵死,也不會影響其他進(jìn)程,進(jìn)程使用的是mulitprossing庫 ,往往是先聲明進(jìn)程實例,里面可以傳入消費方法名稱和不定長參數(shù)args,然后將實例放入指定進(jìn)程數(shù)的容器中(list),通過循環(huán)或者列表推導(dǎo)式,使用start方法開啟進(jìn)程,join方法阻塞主進(jìn)程。
線程
線程是系統(tǒng)進(jìn)行資源調(diào)度的最小單位,它屬于進(jìn)程,每一個進(jìn)程中都會有一個線程,由于線程操作是單進(jìn)程的,所以線程之間可以共享內(nèi)存變量,互相通信非常方便,它的系統(tǒng)開銷比進(jìn)程小,它是線程之間由于共享內(nèi)存,會互相影響,如果一個線程僵死會影響其他線程,隔離性和穩(wěn)定性不如進(jìn)程,同時,線程并不安全,如果對同一個對象進(jìn)行操作,需要手動加鎖,另外從性能上講,多線程會觸發(fā)python的全局解釋器鎖,導(dǎo)致同一時間點只會有一個線程運行的交替運行模式,線程適用于io密集型任務(wù),所謂io密集型任務(wù)就是大量的硬盤讀寫操作或者網(wǎng)絡(luò)tcp通信的任務(wù),一般就是爬蟲和數(shù)據(jù)庫操作,文件操作非常頻繁的任務(wù),比如我負(fù)責(zé)開發(fā)的審核系統(tǒng),需要同時對mysql和redis有大量的讀寫操作,所以我使用多線程進(jìn)行消費。線程使用的是Threading庫 ,往往是先聲明線程實例,里面可以傳入消費方法名稱和不定長參數(shù)args,然后將實例放入指定線程數(shù)的容器中(list),通過循環(huán)或者列表推導(dǎo)式,使用start方法開啟線程,join方法阻塞主線程。
協(xié)程
協(xié)程是一種用戶態(tài)的輕量級線程,協(xié)程的調(diào)度完全由用戶控制,不像進(jìn)程和線程是系統(tǒng)態(tài),所以在不主動切換協(xié)程的情況下,操作全局變量的時候,可以無需加鎖(這里有坑,協(xié)程庫內(nèi)置也是有鎖的,但是看場景,如果使用場景內(nèi)沒有主動切換協(xié)程(await)寫操作就不需要加鎖,如果單協(xié)程執(zhí)行過程中,主動切換了協(xié)程,寫操作則需要加鎖 協(xié)程是否加鎖問題),只需要判斷資源狀態(tài)即可,效率非常高,同時協(xié)程是單線程的,即可以共享內(nèi)存,又不需要系統(tǒng)態(tài)的線程切換,同時也不會觸發(fā)gil全局解釋器鎖,所以它性能比線程要高。具體使用場景和線程一樣,適合io密集型任務(wù),所謂io密集型任務(wù)就是大量的硬盤讀寫操作或者網(wǎng)絡(luò)tcp通信的任務(wù),一般就是爬蟲和數(shù)據(jù)庫操作,文件操作非常頻繁的任務(wù),比如我負(fù)責(zé)開發(fā)的審核系統(tǒng),需要同時對mysql和redis有大量的讀寫操作,所以我后期將多線程改造成協(xié)程進(jìn)行消費。協(xié)程我使用的python原生協(xié)程庫asyncio庫,首先通過asyncio.ensure_future(doout(4))方法建立協(xié)程對象,然后根據(jù)當(dāng)天審核員數(shù)量指定開啟協(xié)程數(shù),和多線程以及多進(jìn)程的區(qū)別是,協(xié)程既可以直接傳實參,也可以傳不定長參數(shù),很方便,然后通過await asyncio.gather(*tasks)方法啟動協(xié)程,需要注意的是,主方法需要聲明成async方法,并且通過asyncio.run(main())來啟動。協(xié)程雖然是python異步編程的最佳方式,但是我認(rèn)為它也有缺點,那就是異步寫法導(dǎo)致代碼可讀性下降,同時對編程人員的綜合素質(zhì)要求高,并不是所有人都能理解協(xié)程的工作方式,以及python原生協(xié)程的異步寫法。
Python中的深拷貝和淺拷貝
僅次于三程問題的明星面試題,一般情況下,大家都會說淺拷貝修改復(fù)制對象會影響原對象,而深拷貝不會,但其實,淺拷貝會有三種細(xì)分的情況:
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.拷貝可變對象(一層結(jié)構(gòu)):產(chǎn)生新的對象,開辟新的內(nèi)存空間,改變互不影響。
>>> import copy
>>> a = [1, 2, 3]
>>> b = copy.copy(a)
>>> b
... [1, 2, 3]
# 查看兩者的內(nèi)存地址,不同,開辟了新的內(nèi)存空間
>>> 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.拷貝可變對象(多層結(jié)構(gòu)):產(chǎn)生新的對象,開辟新的內(nèi)存空間,不改變包含的子對象則互不影響、改變包含的子對象則互相影響。
>>> import copy
>>> a = [1, 2, [3, 4]]
>>> b = copy.copy(a)
>>> b
... [1, 2, [3, 4]]
# 查看兩者的內(nèi)存地址,不同,開辟了新的內(nèi)存空間
>>> id(b)
1833997596488
>>> id(a)
1833997596424
>>> a is b
... False
# 1.沒有對包含的子對象進(jìn)行修改,另一方關(guān)我卵事
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.對包含的子對象進(jìn)行修改,另一方也隨之改變
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]]
高并發(fā)如何進(jìn)行處理的
既然JD(Job Describe)中提到了高并發(fā),那么就一定會問高并發(fā)問題,一般情況下,涉及高并發(fā)場景的基本上都是外部系統(tǒng),此時需要簡單介紹一下系統(tǒng)的容量是多少,比如有注冊用戶數(shù)、日活、QPS等等。然后就是提供具體方案,一般的手段是加緩存,數(shù)據(jù)庫讀寫分離,數(shù)據(jù)庫 sharding 等等。高并發(fā)背景下,整個系統(tǒng)瓶頸一般都在數(shù)據(jù)庫。
除了上述的一些常規(guī)方案,業(yè)內(nèi)最常用的緩解高并發(fā)的手段是使用異步任務(wù)隊列:
為了解決生產(chǎn)者和消費者過度耦合的效率低下問題,我設(shè)計了一個緩沖區(qū),生產(chǎn)者不會直接和消費者產(chǎn)生關(guān)系,而是通過緩沖區(qū)解耦,這個緩沖區(qū)就是異步任務(wù)隊列,隊列容器我采用redis數(shù)據(jù)庫,因為redis性能優(yōu)勢比較明顯,同時內(nèi)置的list數(shù)據(jù)類型比較契合隊列這種數(shù)據(jù)結(jié)構(gòu),工具類內(nèi)置了,初始化方法,入隊方法,出隊方法,隊列長度,以及查重唯一方法。每當(dāng)商戶提交表單,此時并不會修改狀態(tài),而是將表單數(shù)據(jù)入庫,同時將商戶uid進(jìn)行入隊操作,遵循fifo原則,在消費者端使用異步的方式進(jìn)行消費,也就是出隊操作,每一個線程對應(yīng)一個審核員,通過消費方法進(jìn)行傳參,每次將出隊的商戶uid和線程傳入的審核員id進(jìn)行組合分配,出隊之后并發(fā)數(shù)已經(jīng)得到了控制,隨后在mysql端進(jìn)行update操作,達(dá)到異步分配審核的目的。
保持冪等性
如果面試中提到了異步任務(wù)隊列(消息隊列),那么冪等性操作幾乎一定會在后續(xù)的問題中提及,所謂冪等性,簡單來說就是對于同一個系統(tǒng),在同樣條件下,一次請求和重復(fù)多次請求對資源的影響是一致的,就稱該操作為冪等的。比如說如果有一個接口是冪等的,當(dāng)傳入相同條件時,其效果必須是相同的。在RabbitMQ中消費冪等就是指給消費者發(fā)送多條同樣的消息,消費者只會消費其中的一條。例如,在一次購物中提交訂單進(jìn)行支付時,當(dāng)網(wǎng)絡(luò)延遲等其他問題造成消費者重新支付,如果沒有冪等性的支持,那么會對同一訂單進(jìn)行兩次扣款,這是非常嚴(yán)重的,因此有了冪等性,當(dāng)對同一個訂單進(jìn)行多次支付時,可以確保只對同一個訂單扣款一次。
具體手段:
事實上,當(dāng)審核任務(wù)出隊之后,如果在消費端出現(xiàn)意外,這個意外包含但不限于出對后tornado宕機、mysql宕機等等,導(dǎo)致出隊任務(wù)沒有進(jìn)行流程化處理,所以我采用了ack驗證機制,也就是緩沖區(qū)隊列從單隊列升級為雙隊列,把rpop出隊改成redis內(nèi)置的rpoplpush的原子性操作,出隊后立即進(jìn)入確認(rèn)隊列,在消費端完成審核任務(wù)后,對ack隊列進(jìn)行確認(rèn)移除操作,如此,一次審批任務(wù)才算完結(jié),如果任務(wù)生命周期內(nèi),任務(wù)一直存在于確認(rèn)隊列沒有出隊,那么輪詢?nèi)蝿?wù)會將任務(wù)id移出確認(rèn)隊列,重新在緩沖區(qū)隊列進(jìn)行入隊操作,這樣就避免了,僵審任務(wù)的問題。
結(jié)語
技術(shù)面試雖然是一種信息不對等的較量,但是只要認(rèn)真研究JD(Job Describe),做好相關(guān)的知識儲備,基礎(chǔ)常識不要翻車(包含但不限于Python基礎(chǔ)/數(shù)據(jù)庫基礎(chǔ)),那么作為應(yīng)聘者拿一個Offer也不是想象中的那么難,本次面試的實戰(zhàn)錄音可以在B站(Youtube)搜索劉悅的技術(shù)博客查閱,歡迎諸君品鑒。