场景应用题目常见面试真题详解
文章目錄
- 1. 場景應(yīng)用
- 1.1 微信紅包相關(guān)問題
- 1.2 秒殺系統(tǒng)相關(guān)問題
- 1.3 掃碼登錄流程
- 1.4 如何實現(xiàn)單點登錄?
- 1.5 如何設(shè)計一個本地緩存?
1. 場景應(yīng)用
1.1 微信紅包相關(guān)問題
參考答案
概況:2014年微信紅包使用數(shù)據(jù)庫硬抗整個流量,2015年使用cache抗流量。
微信的金額什么時候算?
微信紅包的金額是拆的時候?qū)崟r算出來,不是預(yù)先分配的,采用的是純內(nèi)存計算,不需要預(yù)算空間存儲。采取實時計算金額的考慮,是因為實時效率很高,而預(yù)算需要占存儲,預(yù)算空間效率低。
為什么明明搶到紅包,點開后發(fā)現(xiàn)沒有?
2014年的紅包一點開就知道金額,分兩次操作,先搶到金額,然后再轉(zhuǎn)賬。2015年的紅包的拆和搶是分離的,需要點兩次,因此會出現(xiàn)搶到紅包了,但點開后告知紅包已經(jīng)被領(lǐng)完的狀況。進入到第一個頁面不代表搶到,只表示當時紅包還有。
紅包里的金額怎么算?為什么出現(xiàn)各個紅包金額相差很大?
隨機,額度在0.01和剩余平均值*2之間。
例如:發(fā)100塊錢,總共10個紅包,那么平均值是10塊錢一個,那么發(fā)出來的紅包的額度在0.01元~20元之間波動。當前面3個紅包總共被領(lǐng)了40塊錢時,剩下60塊錢,總共7個紅包,那么這7個紅包的額度在:0.01~(60/7*2)=17.14之間。
注意:這里的算法是每被搶一個后,剩下的會再次執(zhí)行上面的這樣的算法。這樣算下去,會超過最開始的全部金額,因此到了最后面如果不夠這么算,那么會采取如下算法:保證剩余用戶能拿到最低1分錢即可。如果前面的人手氣不好,那么后面的余額越多,紅包額度也就越多,因此實際概率一樣的。
紅包的設(shè)計
微信從財付通拉取金額數(shù)據(jù)過來,生成個數(shù)/紅包類型/金額放到redis集群里,app端將紅包ID的請求放入請求隊列中,如果發(fā)現(xiàn)超過紅包的個數(shù),直接返回。根據(jù)紅包的邏輯處理成功得到令牌請求,則由財付通進行一致性調(diào)用,通過像比特幣一樣,兩邊保存交易記錄,交易后交給第三方服務(wù)審計,如果交易過程中出現(xiàn)不一致就強制回歸。
紅包如何計算被搶完?
cache會抵抗無效請求,將無效的請求過濾掉,實際進入到后臺的量不大。cache記錄紅包個數(shù),原子操作進行個數(shù)遞減,到0表示被搶光。財付通按照20萬筆每秒入賬準備,但實際還不到8萬每秒。
通如何保持8w每秒的寫入?
多主sharding,水平擴展機器。
據(jù)容量多少?
一個紅包只占一條記錄,有效期只有幾天,因此不需要太多空間。
查詢紅包分配,壓力大不?
搶到紅包的人數(shù)和紅包都在一條cache記錄上,沒有太大的查詢壓力。
一個紅包一個隊列?
沒有隊列,一個紅包一條數(shù)據(jù),數(shù)據(jù)上有一個計數(shù)器字段。
有沒有從數(shù)據(jù)上證明每個紅包的概率是不是均等?
不是絕對均等,就是一個簡單的拍腦袋算法。
拍腦袋算法,會不會出現(xiàn)兩個最佳?
會出現(xiàn)金額一樣的,但是手氣最佳只有一個,先搶到的那個最佳。
每領(lǐng)一個紅包就更新數(shù)據(jù)么?
每搶到一個紅包,就cas更新剩余金額和紅包個數(shù)。
紅包如何入庫入賬?
數(shù)據(jù)庫會累加已經(jīng)領(lǐng)取的個數(shù)與金額,插入一條領(lǐng)取記錄,入賬則是后臺異步操作。
入帳出錯怎么辦?比如紅包個數(shù)沒了,但余額還有?
最后會有一個take all操作,另外還有一個對賬來保障。
1.2 秒殺系統(tǒng)相關(guān)問題
參考答案
秒殺應(yīng)該考慮哪些問題?
超賣問題
分析秒殺的業(yè)務(wù)場景,最重要的有一點就是超賣問題,假如備貨只有100個,但是最終超賣了200,一般來講秒殺系統(tǒng)的價格都比較低,如果超賣將嚴重影響公司的財產(chǎn)利益,因此首當其沖的就是解決商品的超賣問題。
高并發(fā)
秒殺具有時間短、并發(fā)量大的特點,秒殺持續(xù)時間只有幾分鐘,而一般公司都為了制造轟動效應(yīng),會以極低的價格來吸引用戶,因此參與搶購的用戶會非常的多。短時間內(nèi)會有大量請求涌進來,后端如何防止并發(fā)過高造成緩存擊穿或者失效,擊垮數(shù)據(jù)庫都是需要考慮的問題。
接口防刷
現(xiàn)在的秒殺大多都會出來針對秒殺對應(yīng)的軟件,這類軟件會模擬不斷向后臺服務(wù)器發(fā)起請求,一秒幾百次都是很常見的,如何防止這類軟件的重復(fù)無效請求,防止不斷發(fā)起的請求也是需要我們針對性考慮的。
秒殺URL
對于普通用戶來講,看到的只是一個比較簡單的秒殺頁面,在未達到規(guī)定時間,秒殺按鈕是灰色的,一旦到達規(guī)定時間,灰色按鈕變成可點擊狀態(tài)。這部分是針對小白用戶的,如果是稍微有點電腦功底的用戶,會通過F12看瀏覽器的network看到秒殺的url,通過特定軟件去請求也可以實現(xiàn)秒殺。或者提前知道秒殺url的人,一請求就直接實現(xiàn)秒殺了。這個問題我們需要考慮解決。
數(shù)據(jù)庫設(shè)計
秒殺有把我們服務(wù)器擊垮的風險,如果讓它與我們的其他業(yè)務(wù)使用在同一個數(shù)據(jù)庫中,耦合在一起,就很有可能牽連和影響其他的業(yè)務(wù)。如何防止這類問題發(fā)生,就算秒殺發(fā)生了宕機、服務(wù)器卡死問題,也應(yīng)該讓他盡量不影響線上正常進行的業(yè)務(wù)。
秒殺系統(tǒng)的設(shè)計方案
秒殺系統(tǒng)的數(shù)據(jù)庫設(shè)計
針對秒殺的數(shù)據(jù)庫問題,應(yīng)該單獨設(shè)計一個秒殺數(shù)據(jù)庫,防止因為秒殺活動的高并發(fā)訪問拖垮整個網(wǎng)站。這里只需要兩張表,一張是秒殺訂單表,一張是秒殺貨品表:
其實應(yīng)該還有幾張表,商品表:可以關(guān)聯(lián)goods_id查到具體的商品信息,商品圖像、名稱、平時價格、秒殺價格等,還有用戶表:根據(jù)用戶user_id可以查詢到用戶昵稱、用戶手機號,收貨地址等其他額外信息,這個具體就不給出實例了。
秒殺URL的設(shè)計
為了避免有程序訪問經(jīng)驗的人通過下單頁面url直接訪問后臺接口來秒殺貨品,我們需要將秒殺的url實現(xiàn)動態(tài)化,即使是開發(fā)整個系統(tǒng)的人都無法在秒殺開始前知道秒殺的url。具體的做法就是通過md5加密一串隨機字符作為秒殺的url,然后前端訪問后臺獲取具體的url,后臺校驗通過之后才可以繼續(xù)秒殺。
秒殺頁面靜態(tài)化
將商品的描述、參數(shù)、成交記錄、圖像、評價等全部寫入到一個靜態(tài)頁面,用戶請求不需要通過訪問后端服務(wù)器,不需要經(jīng)過數(shù)據(jù)庫,直接在前臺客戶端生成,這樣可以最大可能的減少服務(wù)器的壓力。具體的方法可以使用freemarker模板技術(shù),建立網(wǎng)頁模板,填充數(shù)據(jù),然后渲染網(wǎng)頁。
單體redis升級為集群redis
秒殺是一個讀多寫少的場景,使用redis做緩存再合適不過。不過考慮到緩存擊穿問題,我們應(yīng)該構(gòu)建redis集群,或采用哨兵模式,可以提升redis的性能和可用性。
使用nginx
nginx是一個高性能web服務(wù)器,它的并發(fā)能力可以達到幾萬,而tomcat只有幾百。通過nginx映射客戶端請求,再分發(fā)到后臺tomcat服務(wù)器集群中可以大大提升并發(fā)能力。
精簡SQL
典型的一個場景是在進行扣減庫存的時候,傳統(tǒng)的做法是先查詢庫存,再去update。這樣的話需要兩個sql,而實際上一個sql我們就可以完成的。可以用這樣的做法:update miaosha_goods set stock=stock-1 where goos_id={#goods_id} and version=#{version} and sock>0; 。這樣的話,就可以保證庫存不會超賣并且一次更新庫存,還有注意一點這里使用了版本號的樂觀鎖,相比較悲觀鎖,它的性能較好。
redis預(yù)減庫存
很多請求進來,都需要后臺查詢庫存,這是一個頻繁讀的場景。可以使用redis來預(yù)減庫存,在秒殺開始前可以在redis設(shè)值,比如 redis.set(goodsId,100),這里預(yù)放的庫存為100可以設(shè)值為常量),每次下單成功之后,Integer stock = (Integer)redis.get(goosId); 然后判斷sock的值,如果小于常量值就減去1。不過注意當取消的時候,需要增加庫存,增加庫存的時候也得注意不能大于之間設(shè)定的總庫存數(shù)(查詢庫存和扣減庫存需要原子操作,此時可以借助lua腳本)下次下單再獲取庫存的時候,直接從redis里面查就可以了。
接口限流
秒殺最終的本質(zhì)是數(shù)據(jù)庫的更新,但是有很多大量無效的請求,我們最終要做的就是如何把這些無效的請求過濾掉,防止?jié)B透到數(shù)據(jù)庫。限流的話,需要入手的方面很多:
- 前端限流:首先第一步就是通過前端限流,用戶在秒殺按鈕點擊以后發(fā)起請求,那么在接下來的5秒是無法點擊(通過設(shè)置按鈕為disable)。這一小舉措開發(fā)起來成本很小,但是很有效。
- 同一個用戶x秒內(nèi)重復(fù)請求直接拒絕:具體多少秒需要根據(jù)實際業(yè)務(wù)和秒殺的人數(shù)而定,一般限定為10秒。具體的做法就是通過redis的鍵過期策略,首先對每個請求都從String value = redis.get(userId);。如果獲取到這個value為空或者為null,表示它是有效的請求,然后放行這個請求。如果不為空表示它是重復(fù)性請求,直接丟掉這個請求。如果有效,采用redis.setexpire(userId,value,10).value 可以是任意值,一般放業(yè)務(wù)屬性比較好,這個是設(shè)置以userId為key,10秒的過期時間(10秒后,key對應(yīng)的值自動為null)。
- 令牌桶算法限流:接口限流的策略有很多,我們這里采用令牌桶算法。令牌桶算法的基本思路是每個請求嘗試獲取一個令牌,后端只處理持有令牌的請求,生產(chǎn)令牌的速度和效率我們都可以自己限定。
異步下單
為了提升下單的效率,并且防止下單服務(wù)的失敗。需要將下單這一操作進行異步處理。最常采用的辦法是使用隊列,隊列最顯著的三個優(yōu)點:異步、削峰、解耦。這里可以采用rabbitmq,在后臺經(jīng)過了限流、庫存校驗之后,流入到這一步驟的就是有效請求。然后發(fā)送到隊列里,隊列接受消息,異步下單。下完單,入庫沒有問題可以用短信通知用戶秒殺成功。假如失敗的話,可以采用補償機制,重試。
服務(wù)降級
假如在秒殺過程中出現(xiàn)了某個服務(wù)器宕機,或者服務(wù)不可用,應(yīng)該做好后備工作。之前的博客里有介紹通過Hystrix進行服務(wù)熔斷和降級,可以開發(fā)一個備用服務(wù),假如服務(wù)器真的宕機了,直接給用戶一個友好的提示返回,而不是直接卡死,服務(wù)器錯誤等生硬的反饋。
1.3 掃碼登錄流程
參考答案
什么是二維碼?
二維碼又稱二維條碼,常見的二維碼為QR Code,QR全稱Quick Response,是一個近幾年來移動設(shè)備上超流行的一種編碼方式,它比傳統(tǒng)的Bar Code條形碼能存更多的信息,也能表示更多的數(shù)據(jù)類型。-- 百度百科
在商品上,一般都會有條形碼,條形碼也稱為一維碼,條形碼只能表示一串數(shù)字。二維碼要比條形碼豐富很多,可以存儲數(shù)字、字符串、圖片、文件等,比如我們可以把 www.nowcoder.com 存儲在二維碼中,掃碼二維碼我們就可以獲取到牛客網(wǎng)的地址。
移動端基于token的認證機制
在了解掃碼登錄原理之前,有必要先了解移動端基于 token 的認證機制,對理解掃碼登錄原理還是非常有幫助的。基于 token 的認證機制跟我們常用的賬號密碼認證方式有較大的不同,安全系數(shù)比賬號密碼要高,如果每次驗證都傳入賬號密碼,那么被劫持的概率就變大了。
基于 token 的認證機制流程圖,如下圖所示:
基于 token 的認證機制,只有在第一次使用需要輸入賬號密碼,后續(xù)使用將不在輸入賬號密碼。其實在登陸的時候不僅傳入賬號、密碼,還傳入了手機的設(shè)備信息。在服務(wù)端驗證賬號、密碼正確后,服務(wù)端會做兩件事。
將 token 返回給移動端,移動端將 token 存入在本地,往后移動端都通過 token 訪問服務(wù)端 API ,當然除了 token 之外,還需要攜帶設(shè)備信息,因為 token 可能會被劫持。帶上設(shè)備信息之后,就算 token 被劫持也沒有關(guān)系,因為設(shè)備信息是唯一的。
這就是基于 token 的認證機制,將賬號密碼換成了 token、設(shè)備信息,從而提高了安全系數(shù),可別小看這個 token ,token 是身份憑證,在掃碼登錄的時候也會用到。
二維碼掃碼登錄的原理
好了,知道了移動端基于 token 的認證機制后,接下來就進入我們的主題:二維碼掃碼登陸的原理。先上二維碼掃碼登錄的流程圖:
掃碼登錄可以分為三個階段:待掃描、已掃描待確認、已確認。我們就來看看這三個階段。
帶掃描階段
待掃描階段也就是流程圖中 1~5 階段,即生成二維碼階段,這個階段跟移動端沒有關(guān)系,是 PC 端跟服務(wù)端的交互過程。
首先 PC 端攜帶設(shè)備信息想服務(wù)端發(fā)起生成二維碼請求,服務(wù)端會生成唯一的二維碼 ID,你可以理解為 UUID,并且將 二維碼 ID 跟 PC 設(shè)備信息關(guān)聯(lián)起來,這跟移動端登錄有點相似。
PC 端接受到二維碼 ID 之后,將二維碼 ID 以二維碼的形式展示,等待移動端掃碼。此時在 PC 端會啟動一個定時器,輪詢查詢二維碼的狀態(tài)。如果移動端未掃描的話,那么一段時間后二維碼將會失效。
已掃碼待確認階段
流程圖中第 6 ~ 10 階段,我們在 PC 端登錄微信時,手機掃碼后,PC 端的二維碼會變成已掃碼,請在手機端確認。這個階段是移動端跟服務(wù)端交互的過程。
首先移動端掃描二維碼,獲取二維碼 ID,然后將手機端登錄的信息憑證(token)和 二維碼 ID 作為參數(shù)發(fā)送給服務(wù)端,此時的手機一定是登錄的,不存在沒登錄的情況。
服務(wù)端接受請求后,會將 token 與二維碼 ID 關(guān)聯(lián),為什么需要關(guān)聯(lián)呢?你想想,我們使用微信時,移動端退出, PC 端是不是也需要退出,這個關(guān)聯(lián)就有點把子作用了。然后會生成一個一次性 token,這個 token 會返回給移動端,一次性 token 用作確認時候的憑證。
然后PC 端的定時器,會輪詢到二維碼的狀態(tài)已經(jīng)發(fā)生變化,會將 PC 端的二維碼更新為已掃描,請確認。
已確認
流程圖中的 第 11 ~ 15 步驟,這是掃碼登錄的最后階段,移動端攜帶上一步驟中獲取的臨時 token ,確認登錄,服務(wù)端校對完成后,會更新二維碼狀態(tài),并且給 PC 端生成一個正式的 token ,后續(xù) PC 端就是持有這個 token 訪問服務(wù)端。
PC 端的定時器,輪詢到了二維碼狀態(tài)為登錄狀態(tài),并且會獲取到了生成的 token ,完成登錄,后續(xù)訪問都基于 token 完成。
在服務(wù)器端會跟手機端一樣,維護著 token 跟二維碼、PC 設(shè)備信息、賬號等信息。
1.4 如何實現(xiàn)單點登錄?
參考答案
單點登錄全稱Single Sign On(以下簡稱SSO),是指在多系統(tǒng)應(yīng)用群中登錄一個系統(tǒng),便可在其他所有系統(tǒng)中得到授權(quán)而無需再次登錄,包括單點登錄與單點注銷兩部分。
登錄
相比于單系統(tǒng)登錄,sso需要一個獨立的認證中心,只有認證中心能接受用戶的用戶名密碼等安全信息,其他系統(tǒng)不提供登錄入口,只接受認證中心的間接授權(quán)。間接授權(quán)通過令牌實現(xiàn),sso認證中心驗證用戶的用戶名密碼沒問題,創(chuàng)建授權(quán)令牌,在接下來的跳轉(zhuǎn)過程中,授權(quán)令牌作為參數(shù)發(fā)送給各個子系統(tǒng),子系統(tǒng)拿到令牌,即得到了授權(quán),可以借此創(chuàng)建局部會話,局部會話登錄方式與單系統(tǒng)的登錄方式相同。這個過程,也就是單點登錄的原理,用下圖說明:
下面對上圖簡要描述:
用戶登錄成功之后,會與sso認證中心及各個子系統(tǒng)建立會話,用戶與sso認證中心建立的會話稱為全局會話,用戶與各個子系統(tǒng)建立的會話稱為局部會話,局部會話建立之后,用戶訪問子系統(tǒng)受保護資源將不再通過sso認證中心,全局會話與局部會話有如下約束關(guān)系:
- 局部會話存在,全局會話一定存在;
- 全局會話存在,局部會話不一定存在;
- 全局會話銷毀,局部會話必須銷毀。
注銷
單點登錄自然也要單點注銷,在一個子系統(tǒng)中注銷,所有子系統(tǒng)的會話都將被銷毀,用下面的圖來說明:
sso認證中心一直監(jiān)聽全局會話的狀態(tài),一旦全局會話銷毀,監(jiān)聽器將通知所有注冊系統(tǒng)執(zhí)行注銷操作。下面對上圖簡要說明:
部署
單點登錄涉及sso認證中心與眾子系統(tǒng),子系統(tǒng)與sso認證中心需要通信以交換令牌、校驗令牌及發(fā)起注銷請求,因而子系統(tǒng)必須集成sso的客戶端,sso認證中心則是sso服務(wù)端,整個單點登錄過程實質(zhì)是sso客戶端與服務(wù)端通信的過程,用下圖描述:
sso認證中心與sso客戶端通信方式有多種,這里以簡單好用的httpClient為例,web service、rpc、restful api都可以。
1.5 如何設(shè)計一個本地緩存?
參考答案
想要設(shè)計一個本地緩存,考慮點主要在數(shù)據(jù)用何種方式存儲,能存儲多少數(shù)據(jù),多余的數(shù)據(jù)如何處理等幾個點,下面我們來詳細的介紹每個考慮點:
數(shù)據(jù)結(jié)構(gòu)
首要考慮的就是數(shù)據(jù)該如何存儲,用什么數(shù)據(jù)結(jié)構(gòu)存儲。最簡單的就直接用Map來存儲數(shù)據(jù),或者復(fù)雜的如redis一樣提供了多種數(shù)據(jù)類型哈希,列表,集合,有序集合等,底層使用了雙端鏈表,壓縮列表,集合,跳躍表等數(shù)據(jù)結(jié)構(gòu)。
對象上限
因為是本地緩存,內(nèi)存有上限,所以一般都會指定緩存對象的數(shù)量比如1024,當達到某個上限后需要有某種策略去刪除多余的數(shù)據(jù)。
清除策略
上面說到當達到對象上限之后需要有清除策略,常見的比如有LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用)等策略。
過期時間
除了使用清除策略,一般本地緩存也會有一個過期時間設(shè)置,比如redis可以給每個key設(shè)置一個過期時間,這樣當達到過期時間之后直接刪除,采用清除策略+過期時間雙重保證。
線程安全
像redis是直接使用單線程處理,所以就不存在線程安全問題。而我們現(xiàn)在提供的本地緩存往往是可以多個線程同時訪問的,所以線程安全是不容忽視的問題,并且線程安全問題是不應(yīng)該拋給使用者去保證。
簡明的接口
提供一個傻瓜式的對外接口是很有必要的,對使用者來說使用此緩存不是一種負擔而是一種享受,提供常用的get,put,remove,clear,getSize等方法即可。
是否持久化
這個其實不是必須的,是否需要將緩存數(shù)據(jù)持久化看需求。本地緩存如ehcache是支持持久化的,而guava是沒有持久化功能的。分布式緩存如redis是有持久化功能的,memcached是沒有持久化功能的。
阻塞機制
問題。而我們現(xiàn)在提供的本地緩存往往是可以多個線程同時訪問的,所以線程安全是不容忽視的問題,并且線程安全問題是不應(yīng)該拋給使用者去保證。
簡明的接口
提供一個傻瓜式的對外接口是很有必要的,對使用者來說使用此緩存不是一種負擔而是一種享受,提供常用的get,put,remove,clear,getSize等方法即可。
是否持久化
這個其實不是必須的,是否需要將緩存數(shù)據(jù)持久化看需求。本地緩存如ehcache是支持持久化的,而guava是沒有持久化功能的。分布式緩存如redis是有持久化功能的,memcached是沒有持久化功能的。
阻塞機制
我們使用緩存的目的就是因為被緩存的數(shù)據(jù)生成比較費時,比如調(diào)用對外的接口,查詢數(shù)據(jù)庫,計算量很大的結(jié)果等等。這時候如果多個線程同時調(diào)用get方法獲取的結(jié)果都為null,每個線程都去執(zhí)行一遍費時的計算,其實也是對資源的浪費。最好的辦法是只有一個線程去執(zhí)行,其他線程等待,計算一次就夠了。但是此功能基本上都交給使用者來處理,很少有本地緩存有這種功能。
總結(jié)
以上是生活随笔為你收集整理的场景应用题目常见面试真题详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 贴吧爬虫
- 下一篇: 数据库小知识点(一直更新)