日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

过于自信,面试普通Java岗被面试官吊打了。。。

發(fā)布時(shí)間:2024/1/8 java 53 豆豆
生活随笔 收集整理的這篇文章主要介紹了 过于自信,面试普通Java岗被面试官吊打了。。。 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

是的,諸位沒有看錯(cuò),這篇文章的要講述的并不是我吊打面試官,而是一段我被面試官吊打的陳年往事,這段痛苦的記憶在我腦海中長久不衰,也是一個(gè)我內(nèi)心曾多次不愿面對(duì)的事實(shí),各位看官可以準(zhǔn)備好一小把瓜子,聽我將這則舊事緩緩道來~

一、“被吊打事件”的前因后果

在正式談及這次“被吊打事件”之前,首先來聊一聊此次事件的前因后果,事情的起因源自于我太過自信,剛畢業(yè)的那段時(shí)間,本故事的主人公,也就是我,經(jīng)過一些特殊手段,成功入職了一家從事教育軟件開發(fā)的小企業(yè)。當(dāng)然,你要問我什么樣的特殊手段能讓剛畢業(yè)的我,無需面試就進(jìn)入了一家軟件企業(yè),那就是大名鼎鼎的面試秘法——走后門

因?yàn)槲业囊晃挥H戚,在這家企業(yè)擔(dān)任級(jí)別不算低的“高管”,因此我靠常人不能及的手段成功入職,沒錯(cuò)!俺是一個(gè)妥妥的關(guān)系戶,也正式由于這個(gè)原因,所以入職后的工作任務(wù)并不算重,飯點(diǎn)前、下班前,沖在第一個(gè)的永遠(yuǎn)是我,畢竟實(shí)力擺在這,不囂張點(diǎn)簡(jiǎn)直對(duì)不住我的身份,哈哈哈~

總之在入職第一份工作的時(shí)光中,我大致算整個(gè)研發(fā)部門中最輕松的那個(gè),因?yàn)楣ぷ鬏p松,所以給了我不少摸魚學(xué)習(xí)時(shí)間,也正是通過這些時(shí)間,我在忙完工作之余的時(shí)間內(nèi),自身也額外學(xué)習(xí)了不少 Java 技術(shù)。

當(dāng)時(shí)我的技術(shù)大致是什么水平呢?這里我從招聘軟件上將我當(dāng)初的簡(jiǎn)歷信息摘過來了,如下:

2019 年的時(shí)候,我當(dāng)時(shí)的簡(jiǎn)歷是這樣的,但凡我當(dāng)初聽到過的主流技術(shù),基本上都去做了相關(guān)學(xué)習(xí),并且一半以上在開發(fā)中都用過,因?yàn)楫?dāng)時(shí)的研發(fā)模式屬于低代碼定制開發(fā),因此核心平臺(tái)的功能代碼中,涉及到的新技術(shù)也蠻多,所以當(dāng)初我有著迷之自信,自認(rèn)為技術(shù)達(dá)到了很牛逼的程度。

正是由于當(dāng)時(shí)這份簡(jiǎn)歷,給予了自己莫大的自信,再加上成也蕭何敗也蕭何,因?yàn)殛P(guān)系戶的原因,我入職額外輕松,但也正因?yàn)槭顷P(guān)系戶,所以極大程度上限制了自己的成長空間,也就是抹不開面子去提漲薪,因此最終我做出了一個(gè)決定:“大丈夫生于天地之間,豈能郁郁久居人下”!

沒錯(cuò),當(dāng)時(shí)的我毅然決然的“提桶跑路”了!提出辭職之后,在諸多的勸阻中,頭也不回的卷鋪蓋走人,沒有別的原因,完全歸咎于個(gè)人對(duì)自己技術(shù)的自信!當(dāng)時(shí)跑回了老家玩了一段時(shí)間后,想著男子漢大丈夫,是時(shí)候該有一番作為了!接著我去到了距離老家最近的省會(huì)城市,從此踏上了額外自信的面試之旅(上份工作不在老家的省份,這也是離職原因之一,玩心重,朋友都不在身邊~)

從這段回憶中,大家應(yīng)該能夠感受出我當(dāng)時(shí)的心態(tài),用一個(gè)詞去形容特別恰當(dāng),也就是“年少輕狂”,沉浸在自己的認(rèn)知中,換當(dāng)時(shí)的心理,如果非要找一個(gè)字來形容的話,那就是“我技術(shù)很屌”!哈哈哈,現(xiàn)在想起來感覺有些許幼稚,但當(dāng)初的我確實(shí)就是這個(gè)心態(tài),因自認(rèn)為的技術(shù)飛速提升,造就了當(dāng)時(shí)內(nèi)心十分膨脹的我。

二、“被吊打事件”的正戲開場(chǎng)

2019 年國慶后的某個(gè)下午,陽光明媚,抬頭望去,天空萬里無云,也正是在這個(gè)時(shí)間點(diǎn)上,一位身著白色T恤的帥小伙,正在賣力的蹬動(dòng)雙腿:

別看了,蹬車的不是這雙腿,而是我那長達(dá)一米四的大長腿~!

當(dāng)時(shí)流行騎共享單車,在我約好面試后,就按約定的時(shí)間騎車趕往面試現(xiàn)場(chǎng),經(jīng)過近半小時(shí)的不懈努力,我成功在約定時(shí)間前趕到了,首先接待我的是一位人事小姐姐,在走完面試前的一些流程后,隨即就喊來了一個(gè)技術(shù)老哥。

負(fù)責(zé)第一輪面試我的老哥,在簡(jiǎn)單看完我的簡(jiǎn)歷后表現(xiàn)的很有興趣,大概同我聊了有四五十分鐘的時(shí)間,當(dāng)然,這并不是本次的主題內(nèi)容,所以按下快進(jìn)鍵:

  • 先簡(jiǎn)單問了一些關(guān)于 Java 基礎(chǔ)的內(nèi)容,如面向?qū)ο蟆⒓稀⒍嗑€程、特性....

  • 接著問了最近做的兩個(gè)項(xiàng)目,整體的業(yè)務(wù)內(nèi)容、核心技術(shù)、個(gè)人負(fù)責(zé)的技術(shù)工作....

  • 然后又問了一些 JVM 相關(guān)的知識(shí),如內(nèi)存區(qū)域、垃圾回收、類加載機(jī)制、即時(shí)編譯.....

  • 接著又聊了一些常用開源框架,如 Spring 事務(wù)原理、MVC 工作流程、AOP 的應(yīng)用場(chǎng)景...

  • 然后又談了一些關(guān)于分布式系統(tǒng)的技術(shù),如分布式系統(tǒng)的一些解決方案、常用的中間件技術(shù)....

  • 最后探討了一下 MySQL 的知識(shí),也就是索引、事務(wù)、鎖的原理,以及 SQL 優(yōu)化、性能優(yōu)化....

第一輪技術(shù)面試中,在我的記憶中回答的還算不錯(cuò),包括當(dāng)時(shí)負(fù)責(zé)一面的技術(shù)老哥似乎也挺滿意,在聊完技術(shù)過了一會(huì)兒之后,他就帶著我的簡(jiǎn)歷去了人事部,緊接著又是最初的那位人事小姐姐,出來領(lǐng)著我往一個(gè)房間走去......

由于當(dāng)時(shí)房間內(nèi)還在談話,所以我在門口稍等了一小會(huì)兒,但沒過多久,我就被喊了進(jìn)去,進(jìn)門后映入我眼簾的,是一位梳著成功人士發(fā)型、穿著黑襯衫、打著小領(lǐng)帶、并且面容較為英俊的男人,從面相上看應(yīng)該在27、28歲左右,當(dāng)時(shí)給我的第一印象并不是油膩,而是一位很注重形象、并且風(fēng)采氣度不凡的老哥,他!就是本文的主角人物!

后來我從人事經(jīng)理口中得知,他就是這個(gè)分公司的技術(shù)總負(fù)責(zé)人,也可以被稱之為CTO、技術(shù)總監(jiān)啥的,當(dāng)然,在這里我印象最深刻的并非是他的長相和氣質(zhì),而是那張超大規(guī)模的真皮沙發(fā)!畢竟我進(jìn)門聽到的第一句,就是他朝我說:“別緊張,過來這里坐”!我當(dāng)時(shí)坐在真皮沙發(fā)上的第一感覺就是:真軟!

三、“被吊打事件”的來龍去脈

前奏:先是簡(jiǎn)單的交談,經(jīng)過一番寒暄之后,終于正戲上演了!

技術(shù)總監(jiān):看了一下你的簡(jiǎn)歷還不錯(cuò)呀,跟我聊聊你最近做過的這個(gè)項(xiàng)目吧。

我:叭啦叭啦叭.....一頓介紹。

技術(shù)總監(jiān):說說你在這個(gè)項(xiàng)目中,主要負(fù)責(zé)哪塊開發(fā)呢?

我:個(gè)人參與了該項(xiàng)目的不少核心功能開發(fā),如整個(gè)平臺(tái)的用戶模塊,管理、身份、權(quán)限等.....

技術(shù)總監(jiān):OK,那咱們聊一聊登錄注冊(cè)吧,這個(gè)是你負(fù)責(zé)的對(duì)嘛?

我:是的。

3.1、第一問:登錄、注冊(cè)的業(yè)務(wù)設(shè)計(jì)

技術(shù)總監(jiān):你先跟我說說,你們這個(gè)項(xiàng)目的注冊(cè)、登錄界面是怎么樣的呢?

注冊(cè)、登錄界面都大同小異,與一些主流網(wǎng)站類似,注冊(cè)界面會(huì)要求用戶輸入「手機(jī)號(hào)/郵箱、昵稱、密碼、二次確認(rèn)密碼、驗(yàn)證碼」等基本信息,等這些基礎(chǔ)信息填寫完成后,用戶就可以點(diǎn)擊按鈕注冊(cè)賬號(hào)了。

而對(duì)于登錄流程的設(shè)計(jì),相較來說就更為簡(jiǎn)單,只需要用戶輸入「郵箱/昵稱/手機(jī)號(hào)」中的任一信息,然后填寫密碼點(diǎn)擊登錄即可。

技術(shù)總監(jiān):那如果用戶登錄時(shí),忘記了密碼怎么辦呢?

對(duì)于這點(diǎn)是無需擔(dān)心的,因?yàn)樵诘卿涰撁嫔?#xff0c;提供了找回密碼的入口,前面注冊(cè)時(shí)必須要填寫郵箱或手機(jī)號(hào)的,用戶可以通過「手機(jī)驗(yàn)證碼或郵箱驗(yàn)證碼」的方式找回密碼。

技術(shù)總監(jiān):嗯呢,那用戶登錄一次之后,第二次登錄時(shí)還需要重新輸入密碼嗎?

這個(gè)要看具體情況來區(qū)分,因?yàn)樵诘卿浀臅r(shí)候,提供了一個(gè)「記住密碼」的選項(xiàng),如果用戶登錄時(shí)勾選了該選項(xiàng),這時(shí)在瀏覽器發(fā)出的「登錄請(qǐng)求」中,除開基本的用戶登錄信息外,還會(huì)額外傳遞一個(gè)標(biāo)識(shí)。

在后端判斷用戶輸入的「用戶名、密碼」正確后,如果請(qǐng)求中存在該標(biāo)識(shí),則會(huì)生成一個(gè) Cookie 信息,將「用戶名、密碼」保存在 Cookie 中返回,客戶端在收到該信息后,會(huì)自動(dòng)把 Cookie 存儲(chǔ)在瀏覽器的本地緩存中,所以當(dāng)用戶第二次登錄時(shí),避免了再次重復(fù)輸入「用戶名、密碼」的工作。

技術(shù)總監(jiān):那你認(rèn)為這種方式存在什么問題么?

有兩個(gè)隱患,一方面由于保存的「用戶名、密碼」存在瀏覽器的本地記錄中,所以如果在本地找到了對(duì)應(yīng)的 Cookie 記錄,用戶密碼是有被盜取的風(fēng)險(xiǎn)。

同時(shí),如果并非用戶本人操作電腦時(shí),其他人通過「開發(fā)者工具」把 input 元素的類型從 password 類型修改成 text 這種,依舊有可能造成用戶密碼泄露。

技術(shù)總監(jiān):可以的,邏輯思維能力還不錯(cuò)。

哈哈哈,沒有的,主要是對(duì)于這塊業(yè)務(wù)比較熟悉,而且登錄注冊(cè)的業(yè)務(wù)不算太復(fù)雜(這個(gè)時(shí)候我還沒有意識(shí)到問題的嚴(yán)重性)。

技術(shù)總監(jiān):再問問你登錄的設(shè)計(jì)哈。

技術(shù)總監(jiān):用戶注冊(cè)時(shí)填好了一部分信息,但因?yàn)橛惺伦唛_了,最后電腦沒電關(guān)機(jī),用戶重啟電腦后,再次打開注冊(cè)界面,需要重填信息嗎?

在我們當(dāng)時(shí)的項(xiàng)目中,如果出現(xiàn)這種情況,由于電腦已經(jīng)重啟了,因此用戶上次填寫的信息會(huì)丟失,需要用戶重新從頭填寫注冊(cè)信息。

技術(shù)總監(jiān):嗯呢,那你有沒有好的辦法解決這個(gè)問題呢?

「埋頭苦思:不對(duì)勁,這小子很不對(duì)勁,這種問題怎么也問?讓我想想該怎么回答。」 ? ?? ? ?在用戶填寫數(shù)據(jù)的時(shí)候,前端可以通過「光標(biāo)移出事件」來獲取用戶當(dāng)前填寫的數(shù)據(jù),接著將其保存在本地的 Cookie 中,如果用戶點(diǎn)擊了「注冊(cè)」按鈕后,則主動(dòng)去刪除 Cookie 中的信息,畢竟提交注冊(cè)后這些保存的信息就失去了作用。

但如果用戶寫到一半,電腦突然沒電關(guān)機(jī)了,重啟后再次打開注冊(cè)頁面,那這里又可以在「頁面加載事件」中,從本地 Cookie 中將原本保存的數(shù)據(jù)讀出來,然后賦值給對(duì)應(yīng)的文本框即可,從而避免用戶重復(fù)填寫數(shù)據(jù)。

技術(shù)總監(jiān):很棒呀,這個(gè)想法很不錯(cuò)!

技術(shù)總監(jiān):那假設(shè)有兩個(gè)用戶在同時(shí)注冊(cè),并且輸入的用戶名相同,同時(shí)提交注冊(cè)會(huì)出現(xiàn)什么情況呢?

首先這種情況出現(xiàn)的幾率比中彩票都小,同時(shí)就算出現(xiàn)了也沒關(guān)系,因?yàn)椴煌囟蔚木W(wǎng)速肯定有差距,所以兩個(gè)注冊(cè)請(qǐng)求到達(dá)服務(wù)器的時(shí)機(jī)也不同,同時(shí)在設(shè)計(jì)數(shù)據(jù)庫的用戶表時(shí),對(duì)用戶名加了唯一索引,所以兩個(gè)用戶同時(shí)注冊(cè)時(shí),就算輸入的用戶名相同,也只會(huì)有一個(gè)注冊(cè)成功,并不會(huì)出現(xiàn)用戶名重復(fù)的情況。

技術(shù)總監(jiān):你們項(xiàng)目除開通過注冊(cè)賬號(hào)登錄外,還有沒有什么其他方式呢?

還有第三方聯(lián)合登錄的實(shí)現(xiàn),主要就是QQ、微信這兩種社交賬號(hào)的聯(lián)合登錄,是通過騰訊本身提供的 API 來實(shí)現(xiàn)的,如果用戶選擇這種第三方登錄,會(huì)直接去調(diào)用騰訊的登錄 API。用戶掃碼登錄成功后,會(huì)觸發(fā)我們平臺(tái)登錄成功的回調(diào)接口,為其自動(dòng)在平臺(tái)注冊(cè)一個(gè)賬號(hào),最終實(shí)現(xiàn)第三方賬號(hào)的聯(lián)合登錄。

技術(shù)總監(jiān):好的,那咱們?cè)倭狞c(diǎn)其他的。

3.2、第二問:注冊(cè)時(shí)的敏感詞檢測(cè)

技術(shù)總監(jiān):你在做注冊(cè)業(yè)務(wù)的時(shí)候,有沒有考慮過,如若用戶填寫的「昵稱/用戶名」涵蓋敏感信息怎么辦呢?比如填寫的昵稱存在傳播色情、違反政策規(guī)定、存在侮辱性含義等情況。

「沉默下來思考了幾十秒,內(nèi)心OS:WC,我還真沒想過這塊問題」

對(duì)于這塊問題,當(dāng)時(shí)在開發(fā)時(shí)并未考慮完全,因?yàn)檫@個(gè)平臺(tái)屬于定制化開發(fā)的,所以用戶注冊(cè)量也不算太大,因此在設(shè)計(jì)時(shí)也沒往這塊多想。

技術(shù)總監(jiān):沒關(guān)系,那假設(shè)現(xiàn)在我讓你去解決這個(gè)問題,你會(huì)如何下手呢?

「當(dāng)時(shí)的我,因?yàn)樽龅亩际且恍┖?jiǎn)單的CRUD/增刪改查項(xiàng)目,所以被問的時(shí)候,腦袋有些斷線,心理想的是:明明我都說沒做過了,你偏偏還得往這塊問,這純屬是在存心刁難我胖虎啊!」

但沒辦法,畢竟人家都問了,所以當(dāng)時(shí)硬著頭皮隨便扯,當(dāng)時(shí)的回答大致是這樣的:首先我會(huì)在數(shù)據(jù)庫里設(shè)計(jì)一張表,或者在后端里面創(chuàng)建一個(gè)Map、Set這類的容器,專門用來存儲(chǔ)「違規(guī)敏感詞」,當(dāng)用戶注冊(cè)時(shí),在填寫好「昵稱」后,前端采用Ajax異步請(qǐng)求的方式,將用戶輸入的「昵稱」發(fā)給后端進(jìn)行敏感詞檢測(cè)。? ? ? ?后端收到前端發(fā)送的 Ajax 請(qǐng)求后,拿著用戶的昵稱去和「違規(guī)敏感詞」進(jìn)行匹配,如果用戶輸入的昵稱中包含敏感詞,那就讓前端顯示一下「昵稱違規(guī),請(qǐng)重新輸入」,反之則通過驗(yàn)證,允許用戶正常注冊(cè)。

技術(shù)總監(jiān):思路不錯(cuò)嘛,那如果用戶的昵稱有七個(gè)字,但其中有兩個(gè)字組成的詞語屬于敏感詞,請(qǐng)問如何檢測(cè)出來呢?

首先肯定需要先把用戶輸入的昵稱分開,然后再進(jìn)行敏感詞檢測(cè),但由于個(gè)人未處理過該問題,所以目前不清楚具體的做法(其實(shí)具體方案是可以借助 ElasticSearch 對(duì)用戶輸入的昵稱做分詞處理,然后再對(duì)分詞后的結(jié)果進(jìn)行敏感詞檢測(cè),或者可以通過 DFA 算法的方式進(jìn)行敏感詞檢測(cè))。

技術(shù)總監(jiān):沒關(guān)系,既然你沒具體做過,那咱們先跳過這個(gè)話題。

「我心中長呼一口氣,終于跳過這個(gè)該死的問題了,再問下去都遭不住了!但沒想到,我以為的結(jié)束卻僅僅只是開始!」

技術(shù)總監(jiān):如果有人通過機(jī)器手段,如爬蟲技術(shù)對(duì)平臺(tái)進(jìn)行賬號(hào)的批量注冊(cè)怎么辦?? ? ?這點(diǎn)不必?fù)?dān)心啊,因?yàn)榍懊嬲f過的,在注冊(cè)時(shí)用戶必須要填寫「手機(jī)號(hào)、或郵箱地址」,然后后端會(huì)先向?qū)?yīng)的手機(jī)號(hào)或郵箱發(fā)送「驗(yàn)證碼」,用戶必須要輸入正確的「驗(yàn)證碼」之后,才能繼續(xù)注冊(cè)的,而手機(jī)號(hào)也好、郵箱也罷,基本上同一個(gè)人不會(huì)有太多個(gè),所以通過「驗(yàn)證碼」的方式,能夠有效阻止機(jī)器批量注冊(cè)。

也包括這個(gè)平臺(tái)其實(shí)還支持第三方賬號(hào)注冊(cè),也就是通過QQ、微信的方式快捷注冊(cè),這種賬號(hào)和「手機(jī)號(hào)、郵箱」類似,都具備一定的稀缺性,同一個(gè)人不會(huì)有太多的賬號(hào),所以基于這類稀缺性賬號(hào)實(shí)現(xiàn)注冊(cè)功能,都能有效的避免機(jī)器批量注冊(cè)。技術(shù)總監(jiān):好的,那一個(gè)手機(jī)號(hào)或者郵箱允許注冊(cè)多個(gè)賬號(hào)么?

這個(gè)是不行的,因?yàn)樵诤蠖擞凶鑫ㄒ恍耘袛?#xff0c;一個(gè)「手機(jī)號(hào)、郵箱」注冊(cè)一次之后,就無法再利用它進(jìn)行二次注冊(cè)了。技術(shù)總監(jiān):嗯呢,好的。

3.3、第三問:爬蟲惡意調(diào)用短信接口做轟炸

技術(shù)總監(jiān):你有接觸過、或者聽說過短信轟炸嘛?

這個(gè)之前接觸過,比如當(dāng)你在網(wǎng)上和一個(gè)人起了爭(zhēng)執(zhí),并且對(duì)方通過一些手段得到了你的手機(jī)號(hào),他就可以拿著你的手機(jī)號(hào),放到一些轟炸平臺(tái)上去,然后這個(gè)平臺(tái)就會(huì)頻繁的給你發(fā)送一些垃圾短信,以此來實(shí)現(xiàn)轟炸、騷擾的效果。

技術(shù)總監(jiān):你說的很對(duì),所謂的短信轟炸就是這么回事,但我想問你個(gè)事啊。

技術(shù)總監(jiān):你前面說過:用戶在注冊(cè)時(shí)不是可以選擇手機(jī)號(hào)注冊(cè)么?

技術(shù)總監(jiān):假設(shè)有人通過逆向分析,調(diào)試出了你們「發(fā)送短信驗(yàn)證碼」的接口,接著用爬蟲技術(shù)批量調(diào)用該接口轟炸別人怎么辦?

「內(nèi)心OS:早知道說沒聽過短信轟炸了,我是真嘴欠啊,但后面仔細(xì)一想,好像不用擔(dān)心這個(gè)問題!」

咳咳,對(duì)于這個(gè)問題嘛,其實(shí)也不必?fù)?dān)心,因?yàn)楫?dāng)用戶點(diǎn)擊了「發(fā)送驗(yàn)證碼」的按鈕之后,首先會(huì)彈出來一個(gè)「滑塊驗(yàn)證碼」,只有當(dāng)用戶通過「滑塊驗(yàn)證」之后,才會(huì)頒發(fā)一個(gè)調(diào)用接口的「數(shù)字簽名」,如果不具備這個(gè)簽名,直接調(diào)用「發(fā)短信驗(yàn)證碼」的接口時(shí)就會(huì)返回「權(quán)限不足」的提示。

而作為一個(gè)正常人,通過「滑塊驗(yàn)證」自然不成問題,所以當(dāng)一個(gè)“人類用戶”注冊(cè)時(shí),肯定是先拿到簽名再調(diào)用「發(fā)短信」接口,如果出現(xiàn)未攜帶「數(shù)字簽名」的請(qǐng)求,自然無法通過調(diào)用前的校驗(yàn),因此通過這種滑塊驗(yàn)證碼的方式,就能有效防止爬蟲的暴力調(diào)用問題。

其實(shí)當(dāng)初身為一個(gè) CRUD 仔的我,在被問到這個(gè)問題之前,一直并不理解為什么要在發(fā)送短信之前,增加「滑塊驗(yàn)證碼」這步反人類操作,畢竟一個(gè)簡(jiǎn)單的滑塊,就連三歲小孩都能通過,因此當(dāng)初在開發(fā)程序時(shí),思來想去都不能理解這步操作!

技術(shù)總監(jiān):嗯呢,那如果對(duì)方通過 Selenium 這種自動(dòng)化技術(shù),通過了你們平臺(tái)的「滑塊驗(yàn)證」,又或者說對(duì)方又調(diào)試出了「數(shù)字簽名」的生成接口,從而得到了簽名,依舊可以正常調(diào)用「短信」接口怎么辦?

當(dāng)我聽到這個(gè)提問的時(shí)候,我很想回答一句:我!不!知!道!我只是一個(gè)天天摸魚的螺絲仔,這不是純屬刁難人么!但不懂兩個(gè)字決不能從我口中說出,因此當(dāng)時(shí)隨意間就扯了起來!

這個(gè)當(dāng)時(shí)沒有考慮到,畢竟前面跟您說過的,這個(gè)平臺(tái)屬于定制化程序,上線后面對(duì)的用戶量并不算大,因此也沒有考慮設(shè)計(jì)反爬蟲機(jī)制,但您所說的這個(gè)問題也很好解決,對(duì)于一些較為“珍貴”的接口資源,比如目前所說到的短信接口,因?yàn)槊織l發(fā)出的短信都需要付費(fèi),所以通常情況下都會(huì)做調(diào)用限制,比如限制十分鐘內(nèi)只允許調(diào)用三次這類的。

技術(shù)總監(jiān):拿你所說的十分鐘調(diào)用三次為例,如果一個(gè)人在第九分鐘調(diào)用了三次,接著又在第十一分鐘調(diào)用了三次,這樣做是不是打破了調(diào)用限制呢?你認(rèn)為是否有更好的方案代替呢?? ? ?當(dāng)時(shí)回答的是:聽您這么說,的確是存在一定的漏洞,從而讓調(diào)用限制被打破,但這塊沒有去詳細(xì)了解和接觸過,所以并不清楚是否有更好的方案解決此問題。

對(duì)于這個(gè)問題,當(dāng)時(shí)的確沒有接觸過,現(xiàn)在想來,他想聽到的答案應(yīng)該是“高并發(fā)情況下的限流方案”,而我回答的限流算法,屬于最基本的計(jì)數(shù)器限流方案,除此之外還有時(shí)間窗口限流、令牌桶限流、漏桶限流這三種方案,下面對(duì)這幾種常見的限流方案展開聊聊。

同時(shí),對(duì)于「發(fā)短信」這類“珍貴性”接口,也應(yīng)該做好接口的安全性設(shè)計(jì),比如做好接口的防篡改、防重放,以及通過數(shù)字簽名實(shí)現(xiàn)接口調(diào)用的高鑒權(quán)等措施。

3.3.1、計(jì)數(shù)器限流方案

計(jì)數(shù)器方案屬于限流算法中最簡(jiǎn)單、并且實(shí)現(xiàn)難度最低的算法,比如以前面的案例來說,規(guī)定了「短信」接口的調(diào)用頻率,不允許在十分鐘內(nèi)超出三次。

這時(shí)實(shí)現(xiàn)起來就很簡(jiǎn)單,在「短信」接口的類中,創(chuàng)建一個(gè)Map<String,AtomicInteger>類型的容器即可,其中Key存儲(chǔ)用戶ID,而Value則存儲(chǔ)一個(gè)原子計(jì)數(shù)器,每當(dāng)一個(gè)用戶調(diào)用一次短信接口后,就將容器中對(duì)應(yīng)的計(jì)數(shù)器加一,同時(shí)開啟一個(gè)定時(shí)任務(wù),每十分鐘對(duì)計(jì)數(shù)器做歸零重置。

當(dāng)然,上述這種做法在用戶量較大的情況下,顯然會(huì)對(duì)程序造成較大的性能損耗,假設(shè)有 100W 用戶,那就需要維護(hù) 100W 個(gè)計(jì)數(shù)器,這會(huì)使得內(nèi)存占用率直線飆升,同時(shí)還需要?jiǎng)?chuàng)建 100W個(gè) 定時(shí)器,來分別維護(hù)每個(gè)用戶的調(diào)用計(jì)數(shù)器。

更好一些的做法是借助中間件實(shí)現(xiàn),比如基于 Redis 緩存中間件來完成,將用戶 ID 設(shè)計(jì)成 Key,而 Value 則是計(jì)數(shù)器,并且創(chuàng)建每個(gè) Key 時(shí)將過期時(shí)間指定為10s,這樣就能充分利用資源,不會(huì)造成太大的資源與性能開銷,偽邏輯如下:

@Autowired private?StringRedisTemplate?redis;@RequestMapping("/sendSmsVerification") public?ResultVO?sendSmsVerification(String?sign,?String?userId){//?用?SMS_?拼接用戶ID作為KeyString?userIdSMS?=?"SMS_"?+?userId;//?先通過前面生成的Key去Redis中進(jìn)行查詢String?value?=?redis.opsForValue().get(userIdSMS);//?如果目前已經(jīng)達(dá)到了調(diào)用次數(shù)限制if?("3".equals(value))?{return?new?ResultVO(200,?"短信調(diào)用次數(shù)已達(dá)上限,請(qǐng)?jiān)谑昼姾笾卦?..");}//?如果該用戶的Key在Redis中不存在,說明是第一次調(diào)用短信接口if?("".equals(value))?{//?首次調(diào)用短信接口時(shí),則在Redis中創(chuàng)建一個(gè)計(jì)數(shù)器redis.opsForValue().set(lockKey,?1,?10,?TimeUnit.SECONDS);}?//?如果該用戶的Key在Redis中存在,說明并非第一次調(diào)用短信接口else?{//?此時(shí)則通過Redis的incr命令,把對(duì)應(yīng)的計(jì)數(shù)器加一redis.opsForValue().increment(key);}//?省略其他業(yè)務(wù)代碼...... }

這段限流代碼并不算特別復(fù)雜,整體下來無非還是前面說的那幾步:

  • ①先通過用戶 ID 拼接得到 Key,然后去 Redis 中進(jìn)行查詢。

  • ②如果查詢出的結(jié)果為 3,說明目前已達(dá)到了調(diào)用限制,則直接返回調(diào)用已達(dá)上限。

  • ③如果查詢出的結(jié)果為空,則說明用戶是第一次調(diào)用短信接口,此時(shí)則在 Redis 中創(chuàng)建計(jì)數(shù)器。

  • ④如果查詢出的 value 和上面兩條都不匹配,則對(duì) Redis 中的計(jì)數(shù)器加一。

這種計(jì)數(shù)器限流算法實(shí)現(xiàn)起來尤為簡(jiǎn)單,但前面也聊過它所存在的問題:臨界問題,如果在兩個(gè)時(shí)間單位的臨界處調(diào)用,比如在第9:59秒調(diào)用了三次,接著又在第10:01秒調(diào)用了三次,那依舊會(huì)發(fā)生“超出調(diào)用上限”的情況,畢竟以十分鐘作為單位,第9、10分鐘屬于一個(gè)時(shí)間單位內(nèi),這時(shí)就超出了調(diào)用上限,調(diào)用次數(shù)達(dá)到6次。

3.3.2、時(shí)間窗口限流方案

時(shí)間窗口限流方案被提出的主要目的,就是為了解決傳統(tǒng)的計(jì)數(shù)器方案存在的臨界問題,它的演變前身為TCP協(xié)議的滑動(dòng)窗口。

限流方案中的時(shí)間窗算法,主要可被分為固定窗口限流、滑動(dòng)窗口限流兩種方案,而前面聊到的計(jì)數(shù)器方案,實(shí)際上就是一種特殊的固定窗口限流方案,在前面的例子中,時(shí)間窗口大小為10min,速率限制為3次,這種方案存在明顯的臨界限制問題。

下面重點(diǎn)聊一聊滑動(dòng)時(shí)間窗口,這種方案是解決臨界問題而被提出的,但對(duì)于滑動(dòng)窗口的概念有些不好理解,所以先上一副邏輯圖,如下:

在上圖中,整個(gè)用虛紅線圈出來的代表一個(gè)時(shí)間窗口,以上述例子來說,一個(gè)窗口的大小為600s/10min,并且每個(gè)窗口被分為了三個(gè)單位,每個(gè)單位大小是200s,這也就意味著每過200s,窗口會(huì)向后滑動(dòng)一個(gè)單位,這個(gè)動(dòng)作也可以被稱之為向后滑動(dòng)一格,目前的窗口分布如下:

  • 第一格:0~200s

  • 第二格:201~400s

  • 第三格:401~600s

劃分出來的每個(gè)格子,都具備各自獨(dú)立的計(jì)數(shù)器,比如在第138s時(shí)發(fā)生了一次接口調(diào)用,此時(shí)第一格的計(jì)數(shù)器就會(huì)+1,還是以之前的例子來說:

第9:59秒調(diào)用了三次,接著又在第10:01秒調(diào)用了三次。

將這里的分鐘轉(zhuǎn)換為具體秒數(shù),也就是在第599s調(diào)用了三次,第601s調(diào)用了三次,此時(shí)來看,每當(dāng)時(shí)間過去200s,窗口就會(huì)向后滑動(dòng)一格,這也就意味著整個(gè)窗口會(huì)變成圖中的下面的樣子,此時(shí)的窗口分布為:

  • 第一格:201~400s

  • 第二格:401~600s

  • 第三格:601~800s

當(dāng)?shù)?99s調(diào)用了三次「短信」接口后,第二格的計(jì)數(shù)器會(huì)累加到3,此時(shí)再當(dāng)?shù)?01s嘗試調(diào)用「短信」接口時(shí),就會(huì)檢測(cè)出已達(dá)到調(diào)用上限,此時(shí)就會(huì)拒絕用戶的調(diào)用,以此來解決傳統(tǒng)計(jì)數(shù)器方案的臨界問題。

Why?Why?Why?有些小伙伴可能到這里就有些暈了,第601s是如何檢測(cè)出調(diào)用超額的吶?因?yàn)槟壳暗臅r(shí)間窗口范圍是201~800s,而將整個(gè)時(shí)間窗口內(nèi)的計(jì)數(shù)器求和,就會(huì)得到調(diào)用總次數(shù)為3,因而成功檢測(cè)出了第601s的調(diào)用上限。

當(dāng)出現(xiàn)調(diào)用達(dá)到上限時(shí),必須隨著時(shí)間推移、窗口不斷向后滑動(dòng),這樣整個(gè)窗口的計(jì)數(shù)器總和才會(huì)下降,因此用戶才能繼續(xù)調(diào)用,通過這種方式就能控制一個(gè)時(shí)間段的絕對(duì)限流。但滑動(dòng)窗口限流方案就不存在臨界問題嗎?答案是No,依舊存在,Why?來看下圖:

看上圖中給出的案例,因?yàn)槟壳暗臅r(shí)間窗口大小是600s,而199s~203s顯然處于同一個(gè)時(shí)間窗口范圍內(nèi),但隨著窗口向后滑動(dòng),這里依舊會(huì)出現(xiàn)臨界問題,也就是在一個(gè)窗口范圍內(nèi),同樣會(huì)出現(xiàn)打破調(diào)用次數(shù)上限的情況,那這種情況下又該如何解決呢?其實(shí)答案很簡(jiǎn)單,把一個(gè)窗口的格子單位調(diào)小即可。

比如直接將每一格的單位大小從200s調(diào)整為1s,此時(shí)每過一秒鐘,窗口就會(huì)向后滑動(dòng)一格,等到100s秒過后,窗口會(huì)向后滑動(dòng)100格,此時(shí)窗口的區(qū)間范圍是101~700s,這就將199~203s這個(gè)范圍包含了進(jìn)去,因此上述情況自然就不會(huì)出現(xiàn)!

經(jīng)過上述分析由此可以得出一條準(zhǔn)則:當(dāng)滑動(dòng)窗口的格子劃分的單位越小,整個(gè)窗口中的格子數(shù)量會(huì)越多,滑動(dòng)窗口的向后移動(dòng)就越平滑,限流的統(tǒng)計(jì)就會(huì)越精確。

3.3.3、令牌桶限流方案

前面簡(jiǎn)單聊完了時(shí)間窗口限流方案后,接著再來聊一聊大名鼎鼎的令牌桶限流方案,令牌桶算法是一種類似于“池化”思想的產(chǎn)物,算法的大體過程如下:

①初始化令牌桶并設(shè)置最大令牌數(shù),當(dāng)桶內(nèi)的令牌達(dá)到閾值時(shí),新添加的令牌會(huì)被拒絕或丟棄。

②根據(jù)限流大小,啟動(dòng)一條線程,并按照一定速率向令牌桶中不斷添加新的令牌。

③任何處于「限流范圍」內(nèi)的請(qǐng)求,都需要先獲取到一個(gè)可用令牌,然后才會(huì)被處理。

④當(dāng)一個(gè)請(qǐng)求獲取到可用令牌后,才會(huì)真正執(zhí)行業(yè)務(wù)邏輯,執(zhí)行完成后會(huì)將此令牌從桶內(nèi)移除。

⑤令牌桶除開有最大令牌數(shù)外,也會(huì)有最小令牌數(shù),當(dāng)桶內(nèi)令牌數(shù)小于最小閾值時(shí),處理完請(qǐng)求并不會(huì)移除令牌,而是會(huì)將令牌還給令牌桶。

對(duì)于令牌桶限流算法,理解起來并沒有前面的滑動(dòng)時(shí)間窗口復(fù)雜,但唯一要注意的是:當(dāng)桶內(nèi)的令牌被一個(gè)請(qǐng)求獲取后,此時(shí)并不會(huì)立馬從桶內(nèi)移除,該令牌會(huì)依舊停留在桶內(nèi),只不過該令牌的狀態(tài)會(huì)從可用狀態(tài)變?yōu)椴豢捎脿顟B(tài),也就是其他請(qǐng)求無法再獲取該令牌,真正移除令牌的工作,會(huì)在業(yè)務(wù)邏輯執(zhí)行完成之后才觸發(fā)。

3.3.4、漏桶限流方案

漏桶限流和令牌桶限流都屬于桶類型的算法,但漏桶算法更類似于MQ消息隊(duì)列,其算法的執(zhí)行示意圖如下:

想要理解漏桶算法,咱們先來看看日常生活中的漏斗,比如現(xiàn)在我要用漏斗來給摩托車加油:

倒油時(shí),我們可以用瓶子,也可以用桶子,也可以用加油槍.....,這也就意味著:漏斗上方的進(jìn)油速率并不固定,但不管上方的進(jìn)油速率如何,下方的漏斗出口,其速率確實(shí)固定的,無論上方進(jìn)油多快,都不能影響下方的出油速率。

理解了日常生活中的漏斗后,接著再來看看前面的漏桶限流算法,請(qǐng)求會(huì)從漏桶上方進(jìn)入,而服務(wù)端則只會(huì)按照固定速率去處理請(qǐng)求。此時(shí)思考一個(gè)問題:當(dāng)請(qǐng)求進(jìn)入的速率大于請(qǐng)求處理的速率,會(huì)發(fā)生什么情況呢?

此時(shí)依舊回到用漏斗給摩托車加油的例子中,如果漏斗上方的倒油速度比較快,而由于漏斗的結(jié)構(gòu)原因,下方的出口跟不上進(jìn)油速度,此時(shí)漏斗中的油量會(huì)直線上升,直到超出漏斗的最大容量時(shí),再進(jìn)入漏斗的汽油會(huì)溢出。

而限流中的漏桶算法同樣如此,請(qǐng)求進(jìn)入的速率大于請(qǐng)求處理的速率時(shí),多出來的請(qǐng)求會(huì)被放入桶中等待,當(dāng)桶內(nèi)阻塞等待的請(qǐng)求超過最大限制后,后續(xù)進(jìn)入的請(qǐng)求會(huì)被丟棄或拒絕。

從上述的講解中,諸位應(yīng)該能夠明顯感受到漏桶算法的特點(diǎn),即:寬進(jìn)嚴(yán)出,該算法中不會(huì)限制請(qǐng)求進(jìn)入的速率,但會(huì)限制請(qǐng)求處理的速率,一些對(duì)穩(wěn)定性要求較高的系統(tǒng),就可以采用該算法對(duì)系統(tǒng)進(jìn)行限流。當(dāng)然,如果熟悉MQ的小伙伴也能感受出:漏桶算法和MQ的削峰填谷有著異曲同工之妙,當(dāng)系統(tǒng)峰值流量較高時(shí),會(huì)將請(qǐng)求寫入到MQ中,然后再由具體的業(yè)務(wù)服務(wù),按照固定的速率拉取MQ中的消息進(jìn)行處理。

3.3.5、高并發(fā)限流算法小結(jié)

在前面共計(jì)提到了計(jì)數(shù)器、滑動(dòng)窗口、令牌桶、漏桶這四種常規(guī)的限流方案,但要記住:并不存在一種適用于任何場(chǎng)景的限流算法,根據(jù)業(yè)務(wù)的需求不同,系統(tǒng)的關(guān)注面不同,應(yīng)當(dāng)采用不同的限流方案,沒有所謂的最好!最后簡(jiǎn)單說一些成熟的限流實(shí)現(xiàn):

  • Guava 中的 RateLimiter 工具類:基于令牌桶實(shí)現(xiàn)的限流組件,并且對(duì)其進(jìn)行了預(yù)熱拓展。

  • Sentinel 中的勻速排隊(duì)限流策略:基于漏桶思想的限流策略,內(nèi)部采用隊(duì)列進(jìn)行實(shí)現(xiàn)。

  • Nginx 的 limit_req_zone 限流模塊:基于漏桶思想的限流模塊,實(shí)現(xiàn)網(wǎng)關(guān)層的限流控制。

  • ........

3.4、第四問:API接口的冪等性問題

技術(shù)總監(jiān):接下來我們?cè)倭牧钠渌矫娴目梢园?#xff1f;

技術(shù)總監(jiān):以目前的技術(shù)來說,任何用戶在使用網(wǎng)絡(luò)時(shí),難免會(huì)存在延遲是不是?

對(duì)的,這點(diǎn)我深有體會(huì),尤其是在過年回老家的時(shí)候,由于山區(qū)的網(wǎng)絡(luò)覆蓋并不全面,所以在訪問一個(gè)網(wǎng)站時(shí),加載的速度會(huì)特別的慢。

技術(shù)總監(jiān):嗯呢,既然你也說了這個(gè)問題,那我再問你一個(gè)問題。

技術(shù)總監(jiān):如果一個(gè)用戶在注冊(cè)時(shí),網(wǎng)絡(luò)比較卡頓,所以提交注冊(cè)后遲遲沒有反應(yīng),因此他又連續(xù)點(diǎn)擊了多次「注冊(cè)」按鈕,此時(shí)會(huì)發(fā)生什么情況呢?

「我沉思片刻回答道」:如果沒有做任何限制,理論上會(huì)向服務(wù)端發(fā)出多次請(qǐng)求,如果數(shù)據(jù)庫的表結(jié)構(gòu)設(shè)計(jì)不合理,那么還會(huì)出現(xiàn)同一用戶的注冊(cè)信息,在用戶表中被插入多次。

技術(shù)總監(jiān):說的不錯(cuò),那請(qǐng)問你們當(dāng)時(shí)是怎么處理呢?

我們當(dāng)時(shí)處理方案比較簡(jiǎn)單,首先在前端做了一定限制,也就是當(dāng)用戶首次點(diǎn)擊了「注冊(cè)」按鈕后,「注冊(cè)」按鈕就會(huì)變成灰色,也就是用戶再次點(diǎn)擊時(shí),并不會(huì)再次發(fā)送Post請(qǐng)求向后端提交表單數(shù)據(jù)。技術(shù)總監(jiān):那如果用戶看點(diǎn)擊注冊(cè)按鈕后遲遲沒反應(yīng),按F5刷新或?yàn)g覽器的后退鍵,接著再次點(diǎn)了「注冊(cè)」按鈕怎么辦?

「心里一顫,沒想過啊!硬著頭皮解釋道」:對(duì)于此問題,我在做登錄注冊(cè)時(shí)并未考慮周全,未對(duì)這個(gè)問題進(jìn)行思考。

但其實(shí)現(xiàn)在想來,解決的思想也比較簡(jiǎn)單,除開在原本將按鈕變灰的基礎(chǔ)上,再加上一個(gè)「重定向頁面」即可,比如信息提交后就跳轉(zhuǎn)下述這個(gè)界面:

這樣做的好處在于:重定向操作發(fā)生后,當(dāng)用戶再次刷新網(wǎng)頁,或者通過瀏覽器的回退鍵,回到原本的界面時(shí),之前表單中填寫的信息并不會(huì)保存。這樣做的好處在于:用戶想要再次點(diǎn)擊注冊(cè)按鈕,就只能再次重新輸入信息。

在用戶網(wǎng)絡(luò)比較卡頓的情況下,做了上述設(shè)計(jì)后,就只會(huì)出現(xiàn)兩種情況:

①用戶上次點(diǎn)擊「注冊(cè)」按鈕提交的Post請(qǐng)求發(fā)送失敗,服務(wù)端并未處理上次的注冊(cè)請(qǐng)求。

②用戶上次點(diǎn)擊「注冊(cè)」按鈕提交的Post請(qǐng)求發(fā)送成功,在用戶再次填寫信息的過程中,服務(wù)端將上次的注冊(cè)請(qǐng)求成功處理,用戶再次提交注冊(cè)時(shí),系統(tǒng)會(huì)直接提示去通過手機(jī)號(hào)登錄。

總之加入了這個(gè)「重定向頁面」后,都能保障在短時(shí)間內(nèi),用戶無法再次重復(fù)提交參數(shù)相同的注冊(cè)請(qǐng)求。

技術(shù)總監(jiān):那如果有人通過PostMan之類的工具,模擬注冊(cè)參數(shù)多次調(diào)用注冊(cè)接口呢?? ? ?這個(gè)實(shí)際上也不需要擔(dān)心的,因?yàn)樵跀?shù)據(jù)庫的表設(shè)計(jì)中,我們對(duì)「郵箱/昵稱/手機(jī)號(hào)」這些特殊字段也加了唯一索引,就算特殊情況下造成重復(fù)請(qǐng)求出現(xiàn),由于表結(jié)構(gòu)中有唯一性字段,所以對(duì)于相同注冊(cè)參數(shù)的請(qǐng)求,在用戶表中依舊只會(huì)成功插入一條數(shù)據(jù)。

技術(shù)總監(jiān):這種方案也可以,但你還有沒有什么其他更好的方案呢?

當(dāng)時(shí)項(xiàng)目是這么做的,所以并未再去對(duì)其他方案進(jìn)行研究。

技術(shù)總監(jiān):沒事,你等面試結(jié)束之后可以再研究一下。

3.4.1、接口冪等性設(shè)計(jì)的最佳實(shí)現(xiàn)

雖然當(dāng)時(shí)并未回答出更好的方案,但后續(xù)自己也去了解過「接口冪等性與防重設(shè)計(jì)」,這里做簡(jiǎn)單總結(jié)。

產(chǎn)生冪等問題的根本原因 ? ?總的來說,在軟件系統(tǒng)中出現(xiàn)冪等問題的原因無非四個(gè):

①用戶重復(fù)提交:一般是指用戶填寫好表單信息后,由于響應(yīng)較慢,從而多次點(diǎn)擊提交按鈕。

②非法調(diào)用:指第三方通過逆向手段調(diào)試到了接口地址,然后通過爬蟲或接口工具多次調(diào)用。

③失敗重試:指分布式項(xiàng)目中,被調(diào)用方出現(xiàn)超時(shí)或異常時(shí),觸發(fā)了調(diào)用方的重試補(bǔ)償機(jī)制。

④重復(fù)消息:通常指引入MQ的項(xiàng)目,對(duì)于同一個(gè)消息,生產(chǎn)者多次發(fā)送,或消費(fèi)者重復(fù)消費(fèi)。

會(huì)出現(xiàn)冪等問題的操作

作為開發(fā)者的我們都知道,任何一個(gè)軟件,不管業(yè)務(wù)多么復(fù)雜,其背后的本質(zhì)依舊是增刪改查,對(duì)于刪、查操作而言,天然具備冪等性,因此需要考慮冪等性設(shè)計(jì)的就只有增、改這兩種,Why?

因?yàn)椴樵儭h除操作,就算出現(xiàn)多次也并不影響整體數(shù)據(jù)的一致性,比如查詢“張三”的年齡,同一時(shí)間內(nèi)無論查多少次,得到的結(jié)果都是相同的。而刪操作同樣如此,如刪除姓名為“張三”的用戶數(shù)據(jù),就算同一時(shí)間內(nèi)出現(xiàn)了十個(gè)這樣的請(qǐng)求,最終結(jié)果都是“張三”這條數(shù)據(jù)不見了。

多個(gè)層面解決冪等問題的方案

前端:

①按鈕變灰/或變?yōu)長oad狀態(tài):防止用戶點(diǎn)擊多次按鈕,造成多個(gè)重復(fù)請(qǐng)求出現(xiàn)。

②重定向頁面:防止用戶通過刷新/回退的方式,造成多個(gè)重復(fù)請(qǐng)求出現(xiàn)。

后端:

①唯一Key方案:先根據(jù)業(yè)務(wù)參數(shù),從中選出或計(jì)算出一個(gè)全局唯一Key:

  • 唯一Key的計(jì)算方案:

    • 選用請(qǐng)求參數(shù)中的某個(gè)特殊值,如手機(jī)號(hào)、訂單號(hào)...作為Key。

    • 通過Hash函數(shù)來對(duì)所有參數(shù)進(jìn)行哈希計(jì)算,得到一個(gè)Key。

    • 非注冊(cè)的場(chǎng)景,可以使用當(dāng)前用戶ID+目標(biāo)方法名作為Key。

    • .....(這里只要能得到一個(gè)與業(yè)務(wù)相關(guān)的唯一Key即可)。

  • 得到唯一Key之后,通過set nx px命令向Redis插入數(shù)據(jù):

    • 成功:代表前面沒有重復(fù)的請(qǐng)求,當(dāng)前請(qǐng)求可以執(zhí)行。

    • 失敗:代表前面有相同請(qǐng)求已經(jīng)插入過了,當(dāng)前請(qǐng)求需要被丟棄。

②防重表方案:使用業(yè)務(wù)的唯一ID,如訂單號(hào)作為唯一索引,操作之前先插入防重表。

③狀態(tài)機(jī)方案:在表上多加一個(gè)狀態(tài)字段,對(duì)于update操作加上狀態(tài)判斷,如訂單表:

  • 將「待付款」改為「待發(fā)貨」:update ...,status = 2 where status = 1;

  • 這樣就算出現(xiàn)多個(gè)修改請(qǐng)求,因?yàn)榈谝粋€(gè)請(qǐng)求改成功后,狀態(tài)變?yōu)?,其他請(qǐng)求都會(huì)失敗。

④Token 方案:內(nèi)容較多,后面聊。

數(shù)據(jù)庫:

  • 樂觀鎖方案:額外設(shè)計(jì)一個(gè) version 版本字段,但這種方案只適用于 update 操作。

  • 唯一索引:對(duì)于數(shù)據(jù)的關(guān)鍵字段加上唯一索引,如手機(jī)號(hào),避免重復(fù)數(shù)據(jù)多次插入。

上面根據(jù)不同的層面,給出了多種冪等問題的解決方案,但有些方案只適用于特殊的場(chǎng)景,如狀態(tài)機(jī)、樂觀鎖、防重表等方案,如果要設(shè)計(jì)一套解決冪等問題的通用方案,選擇如下:

甲、前端重定向頁面防重 + 后端唯一Key去重 + 數(shù)據(jù)庫唯一索引兜底。

乙、前端按鈕變灰防重 + 后端Token去重 + 數(shù)據(jù)庫唯一索引兜底。

通過上述這兩套組合方案,任選其一都能夠打造出一套解決冪等問題的通用策略,但其中唯一沒展開講解的則是Token方案,這種方式到底是如何實(shí)現(xiàn)的呢?下面展開聊一聊,示意圖如下:

①當(dāng)用戶進(jìn)入一個(gè)表單時(shí),前端通過Ajax異步調(diào)用后端提供的Token獲取接口。

②后端生成一個(gè)全局唯一性的Token放入Redis中,可以是UUID、SnowflakeID....。

③后端將生成的Token返回給前端,前端先將其保存在一個(gè)變量或Cookie中。

④用戶填寫好表單數(shù)據(jù)后,在Post請(qǐng)求的頭部攜帶Token值,接著與表單數(shù)據(jù)一起發(fā)給后端。

⑤后端先獲取頭部的Token值,并嘗試去Redis中刪除該Token,即del [token_value]。

⑥后端根據(jù)刪除命令的執(zhí)行結(jié)果,進(jìn)行下一步判斷:

  • 如果成功刪除:表示目前請(qǐng)求是第一次調(diào)用接口,允許執(zhí)行具體的業(yè)務(wù)邏輯。

  • 如果刪除失敗:表示該Token之前已經(jīng)刪過了,當(dāng)前請(qǐng)求屬于重復(fù)請(qǐng)求,應(yīng)當(dāng)被丟棄。

上述即是前面所說的 Token 方案,整個(gè)過程會(huì)出現(xiàn)兩個(gè)請(qǐng)求,第一個(gè)請(qǐng)求是異步獲取 Token,第二個(gè)請(qǐng)求則是具體的業(yè)務(wù)請(qǐng)求,最后會(huì)基于業(yè)務(wù)請(qǐng)求上攜帶的 Token值,以此作為重復(fù)請(qǐng)求的判斷條件,從而避免同時(shí)處理多個(gè)重復(fù)的請(qǐng)求。

3.5、第五問:用戶賬號(hào)的合并問題

技術(shù)總監(jiān):你之前說過,你們項(xiàng)目注冊(cè)時(shí),可以選用「郵箱/手機(jī)號(hào)/第三方賬號(hào)」進(jìn)行登錄是吧?? ? ?對(duì)的,用戶可以通過這三種方式來注冊(cè)并登錄平臺(tái)。

技術(shù)總監(jiān):那一個(gè)用戶通過手機(jī)號(hào)注冊(cè)后,能否綁定第三方賬號(hào)呢?? ? ?這個(gè)是支持的,在用戶的個(gè)人中心里,用戶可以選擇綁定第三方賬號(hào),綁定第三方賬號(hào)后,后續(xù)用戶也可以直接通過第三方賬號(hào)登錄。

技術(shù)總監(jiān):那假設(shè)用戶先通過微信進(jìn)行第三方登錄,按你們平臺(tái)的規(guī)則,會(huì)自動(dòng)為其注冊(cè)一個(gè)賬號(hào)。

技術(shù)總監(jiān):接著該用戶又用手機(jī)號(hào)注冊(cè)了,此時(shí)同一個(gè)人在你們平臺(tái),是不是有了兩個(gè)賬號(hào)?

是的,通過微信登錄時(shí),如果之前這個(gè)微信沒有綁定過平臺(tái)賬號(hào),會(huì)為其自動(dòng)創(chuàng)建一個(gè)賬號(hào)。用戶通過手機(jī)號(hào)進(jìn)行注冊(cè),同樣又會(huì)生成一個(gè)賬號(hào)。

技術(shù)總監(jiān):嗯呢,那我想問一下,如果這個(gè)用戶有一次通過手機(jī)號(hào)登錄,接著想要綁定那個(gè)微信,這樣可以嗎?

我聽到這個(gè)問題,第一反應(yīng)是想回答:“可以”!但轉(zhuǎn)念一想發(fā)現(xiàn)了端倪,如果能綁定同一個(gè)微信,豈不是一個(gè)微信對(duì)應(yīng)兩個(gè)平臺(tái)賬號(hào)了?假設(shè)該用戶下次選擇通過微信掃碼登錄,掃碼成功之后,到底要登入哪個(gè)賬號(hào)呢?

「我理清思路回答道」:這是不可以的,因?yàn)檫@樣綁定之后,一個(gè)微信號(hào)會(huì)與兩個(gè)平臺(tái)賬號(hào)產(chǎn)生映射關(guān)系,下次用戶選擇用該微信號(hào)登錄時(shí),就會(huì)出現(xiàn)問題,無法確定要登入哪個(gè)賬號(hào)。

技術(shù)總監(jiān):既然你能想明白這個(gè)問題,那我想問問你有沒有什么好的解決方案呢?

「我聽到這個(gè)問題后,陷入了沉默.....」

3.5.1、站在現(xiàn)在的角度再次看待此問題

其實(shí)這個(gè)問題本身并不是技術(shù)問題,而是一個(gè)業(yè)務(wù)問題,所以想要解決此問題,就無法完全依靠程序自己完成,此時(shí)必須介入人工進(jìn)行處理,而這個(gè)問題在如今的各大平臺(tái)都有解決方案,大體歸為下述五類:

①選擇第三方登錄時(shí),需要用戶通過手機(jī)號(hào)先創(chuàng)建一個(gè)平臺(tái)賬號(hào)。

②合并多賬號(hào)的權(quán)利交給用戶自己。

③當(dāng)用戶嘗試綁定一個(gè)「已綁微信」時(shí),提示用戶找管理員申訴。

④允許同一個(gè)第三方賬號(hào)對(duì)應(yīng)多個(gè)平臺(tái)賬號(hào),掃碼登錄時(shí),選擇登錄哪個(gè)賬號(hào)的權(quán)利交給用戶。

⑤用戶想要綁定一個(gè)「已綁微信」時(shí),提示用戶先去解除該微信與其他賬號(hào)的綁定關(guān)系。

第一種做法在各大銀行的手機(jī)APP中比較常見,當(dāng)你選擇通過第三方賬號(hào)登錄手機(jī)銀行時(shí),如果是第一次登錄,微信登錄成功后會(huì)跳轉(zhuǎn)注冊(cè)界面,要求你先通過手機(jī)號(hào)創(chuàng)建一個(gè)賬號(hào),接著銀行APP會(huì)自動(dòng)將當(dāng)前「手機(jī)號(hào)、微信」產(chǎn)生綁定關(guān)系,后續(xù)可以兩者中的任一方式登錄。

第二種做法我在簡(jiǎn)書見過,當(dāng)多個(gè)賬號(hào)之間存在沖突時(shí),將合并賬號(hào)的權(quán)利交給用戶自己,當(dāng)用戶選擇保留某個(gè)賬號(hào)時(shí),其他賬號(hào)都會(huì)被銷毀,包括其他賬號(hào)在平臺(tái)上的所有數(shù)據(jù)也會(huì)徹底丟失。

第三種做法我在一些小的自建站見過,其實(shí)這是觸發(fā)了平臺(tái)的「未知操作」的補(bǔ)償機(jī)制,由于用戶在嘗試綁定一個(gè)「已綁微信」,這種操作在程序后臺(tái)無法識(shí)別,所以直接給出統(tǒng)一的提示,即:“請(qǐng)聯(lián)系管理員進(jìn)行申訴”,申訴后會(huì)由平臺(tái)管理員,介入修改后臺(tái)數(shù)據(jù)庫進(jìn)行處理。

第四種做法在游戲的用戶管理中比較常見,以廣為人知的「王者榮耀」舉例說明,在登錄界面可以選擇通過微信登錄游戲,而微信登錄成功之后,會(huì)出現(xiàn)下述這個(gè)界面:

在這類游戲中,玩家可以自行選擇分區(qū),同一個(gè)微信賬號(hào)支持在多個(gè)分區(qū)創(chuàng)建賬號(hào),這也就意味著一個(gè)第三方賬號(hào),可以與多個(gè)平臺(tái)賬號(hào)存在關(guān)聯(lián)關(guān)系,當(dāng)用戶下次通過該微信賬號(hào)登錄時(shí),用戶可以自行選擇具體的分區(qū)(具體要登錄的平臺(tái)賬號(hào))。

第五種做法屬于最常見的做法,明確規(guī)則一個(gè)第三方賬號(hào),只能與一個(gè)平臺(tái)賬號(hào)存在綁定關(guān)系,當(dāng)一個(gè)賬號(hào)嘗試綁定第三方賬號(hào)時(shí),如果檢測(cè)到對(duì)應(yīng)的第三方賬號(hào)存在其他的綁定關(guān)系,就直接提示用戶:“該第三方賬號(hào)已被其他賬號(hào)綁定,請(qǐng)手動(dòng)解除綁定后重試”!

3.6、第六問:登錄的奪命五連問

技術(shù)總監(jiān):用戶登錄成功之后,第二天再次打開網(wǎng)站需要重新登錄嗎?

如果用戶登錄成功之后,第二天再次打開網(wǎng)站無需再次登錄,但「免登錄」存在時(shí)效限制,一般情況下為7天,也就是距離用戶上次登錄的時(shí)間超出七天后,用戶再次訪問網(wǎng)站就需要再次登錄。

實(shí)現(xiàn)的大體原理:通過JWT實(shí)現(xiàn),用戶登錄成功之后,后端往Redis中存儲(chǔ)一個(gè)時(shí)效七天的refresh Token(Key=userID,value=refreshToken),接著會(huì)向前端頒發(fā)一個(gè)時(shí)效較短的access Token,前端會(huì)將其存儲(chǔ)瀏覽器本地,在后續(xù)每次客戶端訪問當(dāng)前網(wǎng)站時(shí),都會(huì)攜帶這個(gè)access Token完成鑒權(quán)。

頒發(fā)給前端的access Token時(shí)效為何比refresh Token要短呢?

有些業(yè)務(wù)對(duì)權(quán)限比較敏感,為了Token避免被盜用,access Token自然是有效期越短越安全。

時(shí)效較短的access Token過期了怎么辦?當(dāng)一個(gè)客戶端攜帶過期的access Token來請(qǐng)求時(shí),服務(wù)端可以通過該Token解析出時(shí)間戳和用戶信息,效驗(yàn)時(shí)間戳沒有問題后,接著通過用戶信息中的userID去查Redis,如果能夠查詢到對(duì)應(yīng)的refresh Token,此時(shí)就可以重新簽發(fā)一個(gè)access Token返回給前端,前端將之前的Token替換成新的后,再次請(qǐng)求服務(wù)端資源。

這個(gè)過程會(huì)不斷循環(huán),周而復(fù)始之,直至服務(wù)端Redis中的refresh Token過期為止(過期后需要用戶重新登錄)。

技術(shù)總監(jiān):用戶登錄成功之后,其他的子系統(tǒng)如何得知該用戶登錄了?

因?yàn)椴煌淖酉到y(tǒng)都有權(quán)限控制,一個(gè)用戶在主站登錄成功之后,服務(wù)端會(huì)向客戶端頒發(fā)Token,客戶端可以通過該Token在主站域名下“活躍”,但當(dāng)客戶端嘗試訪問其他不同域名的子系統(tǒng)時(shí),由于瀏覽器的本地?cái)?shù)據(jù)(緩存、Cookies等)是按域名區(qū)分存儲(chǔ)的,所以訪問其他子系統(tǒng)時(shí)并不會(huì)攜帶前面主站頒發(fā)的Token,最終客戶端的訪問會(huì)遭到拒絕。

現(xiàn)如今業(yè)務(wù)線愈加復(fù)雜,因此都會(huì)引入分布式概念拆分出不同的子系統(tǒng),并且不同的業(yè)務(wù)子系統(tǒng)會(huì)采用不同的域名部署,所以想要保證「用戶一次登錄,全線都能訪問」的功能,就需要實(shí)現(xiàn)單點(diǎn)登錄功能。在我們項(xiàng)目中,當(dāng)時(shí)通過OAuth2.0整合JWT實(shí)現(xiàn)了SSO認(rèn)證服務(wù),從而最終實(shí)現(xiàn)了單點(diǎn)登錄的功能。

簡(jiǎn)單概述OAuth2.0 + JWT + SSO實(shí)現(xiàn)單點(diǎn)登錄的原理,如下圖:

前提:

①當(dāng)用戶在訪問任意子系統(tǒng)沒有攜帶Token(Ticket)時(shí),都會(huì)被重定向到獨(dú)立部署的SSO認(rèn)證中心。

②如果對(duì)應(yīng)的用戶在SSO服務(wù)中找不到登錄憑證,最終會(huì)跳轉(zhuǎn)登錄頁面,要求用戶進(jìn)行登錄操作。

一次完整的單點(diǎn)登錄過程:

①用戶未攜帶Ticket訪問A系統(tǒng)的某個(gè)頁面,被重定向到SSO服務(wù)。

②用戶未攜帶登錄憑證訪問SSO認(rèn)證中心,被重定向到登錄頁面。

③用戶完成登錄操作,在SSO域的Cookie中植入各種憑證,并再攜帶Code重定向到A系統(tǒng)的回調(diào)接口。

④用戶攜帶Code訪問A系統(tǒng),A向SSO請(qǐng)求驗(yàn)證Code,有效則為A域頒發(fā)Ticket,并重定向到原網(wǎng)頁。

⑤用戶攜帶Ticket訪問A系統(tǒng)的原網(wǎng)頁,A向SSO請(qǐng)求校驗(yàn)Ticket,有效則執(zhí)行具體的業(yè)務(wù)邏輯。

⑥用戶訪問B系統(tǒng)的某個(gè)頁面(此時(shí)無法攜帶A域的Ticket),被重定向到SSO服務(wù)。

⑦用戶攜帶SSO-Cookie訪問SSO,該用戶的登錄憑證校驗(yàn)成功,攜帶Code重定向到B系統(tǒng)的回調(diào)。

⑧用戶攜帶Code訪問B系統(tǒng),B向SSO請(qǐng)求驗(yàn)證Code,有效則為B域頒發(fā)Ticket,并重定向到原網(wǎng)頁。

⑨用戶攜帶Ticket訪問B系統(tǒng)的原網(wǎng)頁,B向SSO請(qǐng)求校驗(yàn)Ticket,有效則執(zhí)行具體的業(yè)務(wù)邏輯。

為什么可以通過Code換Ticket呢?利用OAuth2.0的四種授權(quán)方式之一:授權(quán)碼來實(shí)現(xiàn)。為什么要用Code換Ticket呢?Code是一次性的,降低Ticket被盜用的風(fēng)險(xiǎn)。

技術(shù)總監(jiān):用戶復(fù)制一個(gè)登錄后才能訪問的鏈接,然后粘貼到另一個(gè)頁面上會(huì)怎樣?

這要分情況,如果用戶復(fù)制鏈接之后,粘貼在同一個(gè)瀏覽器的其他頁面,此時(shí)該用戶是可以正常訪問的。但如果用戶復(fù)制鏈接粘貼到其他瀏覽器上,在其他瀏覽器未登錄過的情況下,本次訪問都會(huì)遭到拒絕。這是因?yàn)楹蠖硕紝?duì)用戶做了權(quán)限控制,如果未登錄賬號(hào)的客戶端,在我們平臺(tái)屬于游客級(jí)別,而登錄了賬號(hào)的客戶端,則屬于正常用戶的級(jí)別,不同的用戶級(jí)別對(duì)應(yīng)不同角色,不同角色則又對(duì)應(yīng)不同權(quán)限,以此來實(shí)現(xiàn)權(quán)限的精準(zhǔn)控制。

這里背后的實(shí)現(xiàn)原理就不過多啰嗦了,當(dāng)時(shí)的項(xiàng)目是采用Shrio權(quán)限框架實(shí)現(xiàn)的,所有的權(quán)限、角色、用戶的映射關(guān)系,都存儲(chǔ)在數(shù)據(jù)庫的五張權(quán)限表之中(有興趣的可以自行去了解)。

技術(shù)總監(jiān):用戶點(diǎn)擊登錄之后把當(dāng)前頁面關(guān)了會(huì)發(fā)生什么?

「思索片刻后不自信道」:額....,應(yīng)該會(huì)登錄成功吧。

技術(shù)總監(jiān):確定會(huì)登錄成功么?

「陷入沉默.....」(內(nèi)心:我擦,這純屬刁難人啊,那個(gè)吃飽沒事干的人,會(huì)點(diǎn)了登錄就關(guān)網(wǎng)頁!?!!)

站在現(xiàn)在的角度思考:

結(jié)論:是否會(huì)登錄成功要分實(shí)際情況來決定,看用戶關(guān)閉的是當(dāng)前網(wǎng)頁,還是當(dāng)前的瀏覽器。

用戶關(guān)閉了當(dāng)前網(wǎng)頁,結(jié)果是會(huì)登錄成功。用戶關(guān)閉了當(dāng)前瀏覽器,結(jié)果是不一定登錄成功。

原理分析:關(guān)閉當(dāng)前網(wǎng)頁:因?yàn)橛脩酎c(diǎn)擊登錄按鈕之后,登錄(賬號(hào)、密碼)的請(qǐng)求已經(jīng)發(fā)往了服務(wù)器,所以服務(wù)端處理完登錄請(qǐng)求后,最終會(huì)返回一個(gè)Token或登錄憑證,此時(shí)由于瀏覽器進(jìn)程還在,這也就意味著瀏覽器自帶的網(wǎng)絡(luò)進(jìn)程并未消失,所以登錄效驗(yàn)成功之后的操作,如在Cookie中植入Token、各類憑證等操作依舊能正常完成,所以理論上會(huì)登錄成功。

關(guān)閉當(dāng)前瀏覽器:這種情況下,用戶點(diǎn)擊登錄按鈕后,依舊會(huì)向服務(wù)器發(fā)出登錄請(qǐng)求,但由于瀏覽器已經(jīng)被關(guān)閉了,所以相應(yīng)的網(wǎng)絡(luò)進(jìn)程也會(huì)消失,最終就會(huì)出現(xiàn)一種特殊現(xiàn)象:「當(dāng)服務(wù)端處理完登錄請(qǐng)求后,向客戶端返回響應(yīng)結(jié)果時(shí),由于客戶端的網(wǎng)絡(luò)進(jìn)程已經(jīng)銷毀,所以瀏覽器無法接收響應(yīng)結(jié)果,也就自然無法在Cookie中植入各種登錄憑證,最終結(jié)果就不一定登錄成功」。

疑惑解答:

為什么關(guān)閉瀏覽器之后無法接收服務(wù)端的響應(yīng)結(jié)果?

因?yàn)镠TTP/HTTPS協(xié)議的底層是TCP協(xié)議,TCP是一種雙向通信的網(wǎng)絡(luò)協(xié)議,當(dāng)通信的一端出現(xiàn)故障時(shí),兩端之間的網(wǎng)絡(luò)數(shù)據(jù)就無法正常傳輸,期間TCP的發(fā)送方會(huì)多次重新發(fā)送數(shù)據(jù)包,但由于接收端的網(wǎng)絡(luò)進(jìn)程已銷毀,所以無法收到響應(yīng)結(jié)果。

為什么關(guān)閉瀏覽器的結(jié)果是不一定登錄成功?因?yàn)榇嬖诓环€(wěn)定因素,畢竟大多數(shù)進(jìn)程在退出時(shí),采取的措施都是優(yōu)雅關(guān)閉,也就是會(huì)先處理完目前正在執(zhí)行的任務(wù)后,才會(huì)真正將所有后臺(tái)進(jìn)程退出(也就是大家關(guān)閉一個(gè)程序之后,電腦管家都會(huì)提醒你XX軟件還有殘留進(jìn)程可清理的原因)。

如果關(guān)閉瀏覽器之后,網(wǎng)絡(luò)進(jìn)程沒有立馬銷毀,在這期間可能會(huì)正常收到服務(wù)端的響應(yīng)結(jié)果,最終就會(huì)登錄成功。

但如果服務(wù)端的響應(yīng)時(shí)間比較慢,或者用戶安裝了電腦管家之類的程序,在進(jìn)程退出后也許會(huì)自動(dòng)清理殘留進(jìn)程,這種情況下就會(huì)徹底銷毀網(wǎng)絡(luò)進(jìn)程,此時(shí)結(jié)果就是登錄失敗。

技術(shù)總監(jiān):用戶點(diǎn)擊登錄之后把網(wǎng)線拔了,你認(rèn)為結(jié)果是怎么樣的?

「當(dāng)時(shí)的心情:.....................................」

「當(dāng)時(shí)的內(nèi)心:我去,你*&#...~-/!,前面的點(diǎn)登錄按鈕后關(guān)頁面就夠離譜了,你現(xiàn)在又整一個(gè)拔網(wǎng)線...,你怎么不問我用戶點(diǎn)擊登錄之后,地球就爆炸了會(huì)怎樣呢???」

「我的回答」:不知道!(當(dāng)時(shí)到這里心態(tài)都被問出一點(diǎn)問題了)

以現(xiàn)在的知識(shí)儲(chǔ)備進(jìn)行理性思考:

結(jié)論:具體要看用戶拔網(wǎng)線的時(shí)機(jī),結(jié)果依舊可能是登錄成功或登錄失敗。

如果用戶在響應(yīng)結(jié)果回來之后拔了網(wǎng)線,結(jié)果是登錄成功。但如若響應(yīng)回來之前拔了網(wǎng)線,結(jié)果是失敗。

原理分析:

這個(gè)問題其實(shí)和上一個(gè)問題類似,但實(shí)際情況又存在很大差異,因?yàn)椴还苁裁磿r(shí)候拔網(wǎng)線,本質(zhì)上瀏覽器的網(wǎng)絡(luò)進(jìn)程都不會(huì)消失,問題在于網(wǎng)絡(luò)傳輸鏈路出了問題。

對(duì)于接收到響應(yīng)結(jié)果之后才拔網(wǎng)線的情況,理解起來也比較容易,畢竟響應(yīng)結(jié)果都拿到了,剩下的工作自然也能進(jìn)行,最終結(jié)果就必然是登錄成功。但此時(shí)重點(diǎn)要說明的另一種情況,也就是:為什么在響應(yīng)結(jié)果回來之前,拔掉網(wǎng)線的結(jié)果是登錄失敗?

想要明白這個(gè)問題,本質(zhì)上與計(jì)算機(jī)網(wǎng)絡(luò)的基礎(chǔ)脫不了干系,眾所周知的一點(diǎn),現(xiàn)如今的互聯(lián)網(wǎng)是由一個(gè)個(gè)局域網(wǎng)組成的,由于IP屬于珍貴性資源,所以并不是每臺(tái)網(wǎng)絡(luò)設(shè)備都具備公網(wǎng)IP,而恰恰遠(yuǎn)距離的網(wǎng)絡(luò)通信需要公網(wǎng)IP,此時(shí)又該怎么辦呢?那也就是多臺(tái)網(wǎng)絡(luò)設(shè)備共享一個(gè)公網(wǎng)IP,這些共享一個(gè)公網(wǎng)IP的多臺(tái)機(jī)器,會(huì)組成一個(gè)小的局域網(wǎng)(如果理解比較困難,可以這樣理解:插同一個(gè)路由器網(wǎng)線、連同一個(gè)路由器WiFi的設(shè)備,都可以看成是一個(gè)局域網(wǎng)內(nèi)的設(shè)備)。

有了上述知識(shí)的簡(jiǎn)單儲(chǔ)備后,接著再回到問題本身進(jìn)行探討,當(dāng)用戶的瀏覽器發(fā)出登錄請(qǐng)求,并且服務(wù)端將用戶的登錄請(qǐng)求處理完成后,經(jīng)一系列處理會(huì)產(chǎn)生一個(gè)數(shù)據(jù)報(bào)文,該報(bào)文的目標(biāo)地址就是發(fā)出登錄請(qǐng)求的那臺(tái)機(jī)器(實(shí)際上是那臺(tái)機(jī)器所在的局域網(wǎng)的公網(wǎng)IP),接著響應(yīng)報(bào)文會(huì)先來到機(jī)器所在的局域網(wǎng),但此刻問題來了!

響應(yīng)報(bào)文已經(jīng)抵達(dá)了局域網(wǎng),不過此刻用戶的電腦網(wǎng)線被拔,也就是對(duì)應(yīng)設(shè)備會(huì)退出這個(gè)局域網(wǎng),那么局域網(wǎng)的路由器在“派送數(shù)據(jù)報(bào)文”時(shí),就無法找到具體的派送目標(biāo),但此時(shí)用戶電腦上的瀏覽器網(wǎng)絡(luò)進(jìn)程依舊存在,只是由于傳輸鏈路出現(xiàn)故障,所以無法接收到響應(yīng)結(jié)果,最終導(dǎo)致登錄失敗。

這種情況就相當(dāng)于買快遞,原本你寫的是收貨地址A,當(dāng)快遞送到A小區(qū)的菜鳥驛站時(shí),結(jié)果你搬家搬去了B小區(qū),這時(shí)A小區(qū)的驛站派送員,就無法根據(jù)收貨地址將快遞送貨上門。

當(dāng)然,還有一種特殊情況,也就是用戶把網(wǎng)線拔了之后,又立馬插上去了,這時(shí)理論上還是會(huì)登錄成功的,因?yàn)镠TTP底層的TCP協(xié)議,是一種可靠性傳輸協(xié)議,在傳輸失敗的情況下會(huì)有重發(fā)機(jī)制。

3.7、第七問:令人窒息的多IP并發(fā)操作

技術(shù)總監(jiān):一個(gè)賬號(hào)在多臺(tái)電腦上同時(shí)點(diǎn)擊登錄按鈕,最后會(huì)出現(xiàn)什么情況呢?

「吸收前面的教訓(xùn),聽到這個(gè)問題的我,第一反應(yīng)就是這里面絕對(duì)有詐!」

「經(jīng)過一番思考后,回答道」:應(yīng)該都會(huì)登錄成功。

技術(shù)總監(jiān):哦?也就是你們的項(xiàng)目中,并未限制多IP登錄,或者做同端互斥對(duì)嗎?

「我仔細(xì)想了想,好像確實(shí)沒做,于是回答道」:在我們的項(xiàng)目中確實(shí)沒做這些。

技術(shù)總監(jiān):那假設(shè)一個(gè)賬號(hào)在兩個(gè)IP上登錄了,同時(shí)修改昵稱會(huì)發(fā)生什么變化?

有一個(gè)IP上修改的昵稱,會(huì)被另外一個(gè)IP上的昵稱替代掉。因?yàn)榫退銉蓚€(gè)IP同時(shí)修改、同時(shí)提交,最終到數(shù)據(jù)庫執(zhí)行update語句時(shí),都會(huì)被串行化,因?yàn)閮蓚€(gè)事務(wù)并發(fā)修改同一行數(shù)據(jù)時(shí),需要先獲取行鎖資源,這也就意味著這兩個(gè)修改操作最終都有前后之分,前一個(gè)IP修改的昵稱總會(huì)被后一個(gè)IP修改的昵稱覆蓋掉。

技術(shù)總監(jiān):嗯呢,那在不限制多IP登錄的情況下,你有什么好的辦法結(jié)果這個(gè)問題嗎?

「仔細(xì)推導(dǎo)一番后,回答道」:可以加入一個(gè)中間狀態(tài),也就是在用戶表中多設(shè)計(jì)一個(gè)狀態(tài)字段,0代表正常狀態(tài),1代表審核狀態(tài),當(dāng)用戶的信息發(fā)生變化后,對(duì)應(yīng)的用戶記錄都會(huì)被改成「審核中狀態(tài)」,而執(zhí)行語句時(shí)只允許修改正常狀態(tài)的用戶記錄,偽SQL如下:

--?之前的SQL語句 update?zz_user?set?user_name?=?"888",?...?where?user_id?=?888;--?優(yōu)化后的SQL語句 update?zz_user? set?user_name?=?"888",?status?=?1,?...? where?user_id?=?888?and?status?=?0;

通過這樣的手段,在第一個(gè)IP修改成功之后,第二個(gè)IP就無法滿足SQL語句的執(zhí)行條件,最終就無法真正修改用戶數(shù)據(jù)。

技術(shù)總監(jiān):很不錯(cuò)的思路,的確能夠解決我所提出的問題。

技術(shù)總監(jiān):如果現(xiàn)在有一個(gè)簽到領(lǐng)積分的功能,兩個(gè)不同IP的同一賬號(hào)同時(shí)簽到,會(huì)不會(huì)領(lǐng)到雙倍積分?

如果沒有做任何限制措施,這種情況下應(yīng)該會(huì)領(lǐng)到雙倍積分,但前提是兩個(gè)IP是以絕對(duì)手段進(jìn)行同時(shí)操作的才行,也就是服務(wù)端中同一時(shí)間內(nèi),兩條線程并行處理兩個(gè)IP的簽到請(qǐng)求。

技術(shù)總監(jiān):嗯呢,那如果你項(xiàng)目中有訂單功能,一個(gè)IP刪除訂單,一個(gè)IP結(jié)算訂單,兩個(gè)操作同一時(shí)刻內(nèi)進(jìn)行,結(jié)果是什么呢?

會(huì)出現(xiàn)問題,導(dǎo)致一個(gè)賬號(hào)上的訂單數(shù)據(jù)錯(cuò)亂。

技術(shù)總監(jiān):那你認(rèn)為該怎么解決此問題呢?

當(dāng)時(shí)的我聽到這個(gè)問題,心里的第一想法:得加鎖,但又轉(zhuǎn)念一想,似乎發(fā)現(xiàn)了不對(duì)勁,因?yàn)榧渔i只能讓并行操作串行化,但最終兩個(gè)業(yè)務(wù)操作總會(huì)執(zhí)行的,這里加鎖之后只會(huì)出現(xiàn)兩種情況:

①刪除訂單的請(qǐng)求先獲取鎖,先刪掉了訂單,結(jié)算訂單的請(qǐng)求無法執(zhí)行結(jié)算業(yè)務(wù)(因?yàn)橛唵味紱]了)。

②結(jié)算訂單的請(qǐng)求先拿到鎖,用戶付錢結(jié)算了訂單之后,刪除訂單的請(qǐng)求獲取到了鎖,然后把用戶已經(jīng)付錢的訂單刪了(這顯然更不合理,用戶估計(jì)能舉起四十米的大刀...)。

「由于當(dāng)時(shí)的我沒做過并發(fā)處理,就只懂一些簡(jiǎn)單的多線程理論,于是又陷入了沉默.....」

站在如今的角度出發(fā),再次看待此問題,解決方案為:狀態(tài)機(jī)!啥意思呢?其實(shí)和之前「并發(fā)修改昵稱」的方案差不多,單獨(dú)的靠加鎖無法解決此問題,問題并不在這上面,這同樣是個(gè)業(yè)務(wù)邏輯的問題,應(yīng)該在訂單表上面也設(shè)計(jì)一個(gè)status狀態(tài)字段。

訂單表的狀態(tài)字段,可選狀態(tài)如下:

  • 0:待結(jié)算(待支付)。

  • 1:待發(fā)貨。

  • 2:待收貨。

  • 3:已簽收。

  • .....

  • 9:已銷毀。

有了上述這個(gè)狀態(tài)機(jī)字段后,再回過頭來看「刪除訂單、結(jié)算訂單」這兩個(gè)業(yè)務(wù)操作,本質(zhì)上都是執(zhí)行update操作,刪除是將狀態(tài)改為9,結(jié)算是將狀態(tài)改為1,所以SQL語句只需要新增一個(gè)條件即可:-- 刪除訂單(只允許刪除待結(jié)算、已簽收的訂單)

update?zz_order?set?status?=?9,...?where?status?=?0?or?status?=?3; --?結(jié)算訂單(只允許結(jié)算待支付的訂單) update?zz_order?set?status?=?1,...?where?status?=?0;

也就是直接通過狀態(tài)字段限制其他并發(fā)操作,無論「刪除訂單、結(jié)算訂單」誰先執(zhí)行,另一個(gè)操作都無法繼續(xù)執(zhí)行。有人也許會(huì)疑惑:有了狀態(tài)機(jī)之后,就不需要加鎖了嗎?

其實(shí)這種情況下,加不加鎖就無所謂了,因?yàn)镸ySQL-InnoDB本身有行鎖機(jī)制,多個(gè)事務(wù)并發(fā)修改同一條數(shù)據(jù),都會(huì)被串行化執(zhí)行,因此在后端加鎖,只是將請(qǐng)求串行化的工作提前罷了,這反而會(huì)影響整體的性能。

「其實(shí)到這里還并未結(jié)束,后來這位面試官還與我聊了許多,但由于時(shí)間較為久遠(yuǎn),就只能回憶起一些印象比較深刻的提問了~」

四、這段難忘經(jīng)歷給我?guī)淼母形?/h2>

可能看到這里,大家很感興趣的一點(diǎn)是:后來的我怎么樣了?其實(shí)這次面試之后,當(dāng)時(shí)的我不氣餒是不可能的,甚至那時(shí)的我被打擊的有些嚴(yán)重,自以為不可一世的我,結(jié)果死在了“最簡(jiǎn)單的登錄注冊(cè)”上....

結(jié)束了這次面試后,我并未再繼續(xù)投遞簡(jiǎn)歷,但人總得吃飯是不?于是乎,我又使出了另一種赫赫有名的面試秘法 —— 朋友內(nèi)推,在第二天以滿意的薪酬,成功入職了另一家公司~

當(dāng)然,其實(shí)后來這家外企的人事也在后面一周的周一,給我打來了入職邀約的電話,但由于我已經(jīng)入職了朋友公司,所以用「臨時(shí)有事,不方便過去入職」的理由拒絕了(我原本以為自己肯定涼了,畢竟三四天都沒有給我通知,但后面轉(zhuǎn)念一想,畢竟這是外企的分公司,可能是入職審批流程比較長)。

不過令人出乎意料的是:在當(dāng)天的下午,該外企的人事總監(jiān)又打來了一個(gè)電話邀請(qǐng)我入職,說他們技術(shù)總監(jiān)比較看好我,感覺我很具備培養(yǎng)價(jià)值....,而且這回的入職邀約中,可能以為我上次拒絕是薪資不滿意,還額外在我報(bào)價(jià)的基礎(chǔ)上加了1.5K的工資(這對(duì)當(dāng)時(shí)的我來說,雖然不算特別多,但每個(gè)月多出1.5K也是一筆不菲的收入),不過最終還是因?yàn)槎喾矫娴脑蚓芙^了,哈哈(其實(shí)早兩天打給我說不定真的會(huì)過去~)。

雖然這次面試帶給我的打擊很大,但從中我的收獲也不小,其實(shí)總的來說也算咎由自取,畢竟當(dāng)時(shí)的我的確很驕傲,而這位 CTO 則用我當(dāng)時(shí)認(rèn)為“最簡(jiǎn)單的業(yè)務(wù)”,將我虐的體無完膚,從這段經(jīng)歷中我想明白了一個(gè)道理:謙虛戒驕才是真正的大佬應(yīng)有的美德。

當(dāng)然,說了這么多的過程,最后也來聊聊這段經(jīng)歷帶給我的感悟,捫心自問,其實(shí)這位面試官也是人生中的一位“貴人”,從他身上我看到了很多之前并不知曉的道理。

4.1、千萬不要抱怨自己只是個(gè)CRUD的螺絲仔

在現(xiàn)在的開發(fā)環(huán)境中,很多人都會(huì)抱怨工作:“天天都是負(fù)責(zé)業(yè)務(wù)的增刪改查,這種日子什么時(shí)候是個(gè)頭啊,不想一直再做CRUD的螺絲仔了”!擁有這種心態(tài)的人不在少數(shù),誰的心里都有個(gè)夢(mèng),起初的我也并不例外,「架構(gòu)師、CTO、技術(shù)總監(jiān)、技術(shù)專家.....」,面對(duì)這一個(gè)個(gè)高大上的職位,曾經(jīng)的我也憧憬過,時(shí)常幻想著什么時(shí)候我也能成為這樣的人啊,這頭銜說出去就倍有面子.....

但等到了這些職位的時(shí)候,你會(huì)發(fā)現(xiàn)每天的工作還是和業(yè)務(wù)打交道,泡泡茶暢談未來技術(shù)?用技術(shù)在項(xiàng)目中指點(diǎn)江山?沉淪在技術(shù)中做架構(gòu)選型?其實(shí)這些都不存在,每天其實(shí)依舊在圍繞著業(yè)務(wù)兜兜轉(zhuǎn)轉(zhuǎn),「高職技術(shù)人」和「普通開發(fā)者」之間,唯一區(qū)別就是把敲簡(jiǎn)單的業(yè)務(wù)代碼這項(xiàng)工作,換成了其他更為艱難的任務(wù)。

當(dāng)然,話再說回主題,既然目前無法在項(xiàng)目中用到各種新技術(shù),目前的CRUD無法給自己帶來技術(shù)成長,那我們要做的就是:在有限的空間內(nèi)做到“無限”的發(fā)展,其實(shí)就算最普通的業(yè)務(wù)也能玩出不同花樣,業(yè)務(wù)的增刪改查想要做好也并不容易,比如怎樣才能讓代碼更整潔、能否讓程序拓展性更好?如何才能讓代碼跑的更快.....,動(dòng)才能改變,抱怨再多也改變不了自身。

4.2、牢記謙虛戒驕,人外有人天外有天

這個(gè)道理應(yīng)該是本次經(jīng)歷中,帶給我感悟最深的一條,作為技術(shù)人覺得自己牛可以,但千萬不要驕傲,也不要在面試中、同事交談中、群聊討論中.....表露出來,因?yàn)橛肋h(yuǎn)會(huì)有人比你更厲害,不要為了虛榮心去刻意“攀比”,否則最終倒霉的還是自己,舉個(gè)很常見的案例:

如果當(dāng)過面試官的小伙伴應(yīng)該都遇到過一種情況,也就是候選者在面試時(shí)有些刻意裝逼,這樣的候選者在面試時(shí),往往都會(huì)遭到面試官的無情打壓,最簡(jiǎn)單的做法就是連環(huán)炮問法,從基礎(chǔ)入門問源碼原理,從API調(diào)用問到操作系統(tǒng)實(shí)現(xiàn).....直到最后被問到啞口無言。

擁有自信是好事,但千萬不要自信過頭,牢記謙虛戒驕,因?yàn)槿送庥腥颂焱庥刑臁1热缥?#xff0c;原以為自己的技術(shù)已達(dá)巔峰造極,但經(jīng)過這次面試后,發(fā)現(xiàn)自己理解的一些東西都是浮于表面的假象,看似駕輕就熟,實(shí)際上只是上層特性的搬磚工,學(xué)習(xí)和學(xué)會(huì),壓根是兩碼事!

4.3、再牛的技術(shù)也永遠(yuǎn)是為業(yè)務(wù)所服務(wù)

在IT開發(fā)行業(yè),其實(shí)有不少人抱著做純技術(shù)開發(fā)的念想,至少我遇到過的不在少數(shù),不想去重復(fù)做單純的業(yè)務(wù)開發(fā),但也請(qǐng)牢記:技術(shù)驅(qū)動(dòng)業(yè)務(wù),但技術(shù)也永遠(yuǎn)是為業(yè)務(wù)提供服務(wù)。

當(dāng)然,想做純技術(shù)開發(fā)也并非不行,但國內(nèi)這樣的人很少,或者說國內(nèi)這樣的崗位比較少,除開少數(shù)中間件開發(fā)、開源技術(shù)研發(fā)、基礎(chǔ)平臺(tái)開發(fā)等工作外,大多數(shù)崗位都需要和業(yè)務(wù)打交道,所以在學(xué)習(xí)新技術(shù)時(shí)也萬萬不要忘了業(yè)務(wù),等你吃透某一行業(yè)的業(yè)務(wù)時(shí),也許給你帶來的好處會(huì)勝過技術(shù)的收益。

總結(jié)

以上是生活随笔為你收集整理的过于自信,面试普通Java岗被面试官吊打了。。。的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。