使用RabbitMQ優(yōu)化用戶登錄功能
1. 前言
Hello,大家好。通過上述幾個小節(jié)的介紹,我們已經(jīng)對 RabbitMQ 中的消息發(fā)送模式有了代碼實現(xiàn)層面的了解,這些實操代碼是應(yīng)用 RabbitMQ 基礎(chǔ)中的基礎(chǔ),同學(xué)們必須要掌握并記憶。
那么,從本節(jié)開始,會為大家?guī)恚谖覀兊膶嶋H的一個日常開發(fā)工作中,針對常用的幾個功能,我們應(yīng)該如何使用 RabbitMQ 來進(jìn)行實現(xiàn),同時,通過對常用功能的 RabbitMQ 集成,可以提升傳統(tǒng)功能實現(xiàn)中的一些性能等指標(biāo),這對后續(xù)傳統(tǒng)功能的優(yōu)化是很有必要的。
本小節(jié)會為同學(xué)們介紹,如何使用 RabbitMQ 消息中間件,去優(yōu)化我們使用傳統(tǒng)的方式,來實現(xiàn)的用戶登錄功能點,包括對傳統(tǒng)用戶登錄功能的概述,以及使用 RabbitMQ 進(jìn)行優(yōu)化的關(guān)鍵步驟,希望同學(xué)們可以對本節(jié)內(nèi)容有所了解。
本節(jié)主要內(nèi)容:
-
傳統(tǒng)用戶登錄功能概述;
-
使用 RabbitMQ 優(yōu)化傳統(tǒng)用戶登錄功能。
2.傳統(tǒng)用戶登錄功能概述
2.1 傳統(tǒng)實現(xiàn)方案介紹
眾所周知,我們現(xiàn)在的信息化系統(tǒng)項目,項目的首要功能模塊,基本上都會要求我們來設(shè)計并實現(xiàn)一個完備的用戶模塊,而這個用戶模塊中,必定會包含用戶注冊與用戶登錄功能點。
只有在項目中實現(xiàn)了用戶注冊與用戶登錄這兩個最基本的功能點,我們的項目功能才能繼續(xù)向后進(jìn)行開發(fā),因為項目后續(xù)的功能,或多或少都會與這兩個功能點相聯(lián)系。
所以,這就要求我們在實現(xiàn)用戶注冊與用戶登錄功能時,要從多方面進(jìn)行考慮, 如果有一點我們沒有考慮到,很可能在進(jìn)行項目后續(xù)功能開發(fā)時,還要回過頭去,修改這兩個基本功能,從而與項目后續(xù)的功能相結(jié)合。
那么,傳統(tǒng)的用戶登錄功能的實現(xiàn)方案是什么樣的呢?
傳統(tǒng)用戶登錄功能流程介紹
我們首先接觸用戶登錄功能或者用戶注冊功能,有絕大部分同學(xué)是在自己的學(xué)校內(nèi),也就是學(xué)校老師教授我們的內(nèi)容,往往這種實現(xiàn)方式都會存在諸多問題,下面讓我們來看一下這種方式是如何實現(xiàn)的。
針對用戶登錄功能,對于后端開發(fā)的同學(xué),我們首先需要接收前端傳遞過來的數(shù)據(jù),也就是用戶的登錄數(shù)據(jù),然后我們會根據(jù)這一用戶登錄數(shù)據(jù)來進(jìn)行相應(yīng)的邏輯檢驗。
這些邏輯校驗包括對用戶的名稱進(jìn)行檢測,以查驗當(dāng)前要登錄的用戶是否在我們的系統(tǒng)中存在,如果不存在,則提示用戶不存在與本系統(tǒng)中,如果存在,則檢測用戶所輸入的登錄密碼是否與用戶注冊時所填寫的一致,如果不一致,則提示用戶,用戶登錄密碼錯誤。
如果當(dāng)前要登錄的用戶在本系統(tǒng)中存在,且用戶所輸入的登錄密碼與用戶注冊時所填寫的登錄密碼一致,則表明用戶登錄成功,此時,需要提示用戶登錄成功,并跳轉(zhuǎn)到我們的系統(tǒng)首頁。下面,我們來通過一個功能流程圖來體現(xiàn)上述的業(yè)務(wù)流轉(zhuǎn)過程:

通過上述功能流程圖,我們可以很清楚地理解上述傳統(tǒng)用戶登錄功能的實現(xiàn)過程,如果沒有理解的同學(xué),建議多看幾次,理解這一流程之后我們再繼續(xù)學(xué)習(xí)下面的內(nèi)容。
傳統(tǒng)用戶登錄功能代碼實現(xiàn)
以 SpringBoot 框架為例,我們來實現(xiàn)一下上述的用戶登錄功能,實現(xiàn)代碼如下所示:
實現(xiàn)代碼:
public Response<User> userLogin(User user, HttpSession session){
int userExistsCount = userMapper.selectUserExixtsByUsername(user.getUsername());
if (userExistsCount <= 0){
return Response.createByError("用戶不存在");
}
User userLogin = userMapper.selectLoginUserByUsernamePassword(user.getUsername(), user.getPassword());
if (userLogin != null){
userLogin.setPassword("");
session.setAttribute("loginUser", userLogin);
return Response.createBySuccess("登錄成功", userLogin);
}
return Response.createByError("用戶登錄密碼輸入錯誤,請重新輸入");
}
代碼解釋:
第 1 行,我們定義了一個名為 userLogin 的方法來處理用戶登錄的業(yè)務(wù)邏輯,該方法的返回值是 Response 類型,并且泛型被指定為 User 類型,這表明,我們的方法最終會返回 User 用戶數(shù)據(jù)。
在該方法中,我們定義了兩個參數(shù),分別是 User 類型的 user 參數(shù),以及 HttpSession 類型的 session 參數(shù)。其中,user 參數(shù)就表示用戶登錄時所填入的登錄數(shù)據(jù),session 參數(shù)則表示存儲用戶登錄狀態(tài)和登錄數(shù)據(jù)的參數(shù)。
第 2-5 行,我們通過獲取到用戶的用戶名,并且調(diào)用數(shù)據(jù)庫 ORM 層的方法,來對系統(tǒng)數(shù)據(jù)庫進(jìn)行查詢,查詢當(dāng)前用戶名是否存在我們的數(shù)據(jù)庫中,如果存在,則表明用戶注冊過我們的系統(tǒng),即這個用戶是在我們的系統(tǒng)中的。如果不存在,則會提示用戶當(dāng)前用戶名不存在。
第 6-14 行,我們通過獲取到的用戶的用戶名和用戶登錄密碼,調(diào)用數(shù)據(jù)庫 ORM 層的犯法,來對系統(tǒng)數(shù)據(jù)庫進(jìn)行查詢,查詢當(dāng)前用戶名和密碼是否在系統(tǒng)中匹配。
即,只有用戶名匹配的話是不行的,還要將這個用戶名和其所對應(yīng)的密碼進(jìn)行匹配,當(dāng)使用這兩個條件進(jìn)行匹配查詢,且可以正確查詢到結(jié)果時,表明用戶已經(jīng)登錄成功了,此時,我們可以將用戶登錄的數(shù)據(jù)放到 session 中,以備后續(xù)功能的使用,與此同時,我們還應(yīng)該提示用戶登錄成功了。
如果根據(jù)這兩個查詢條件,沒有查詢到任何數(shù)據(jù),即 userLogin 變量為 null 時,則表明當(dāng)前用戶名是存在的,但是當(dāng)前登錄用戶輸入的登錄密碼和注冊時所輸入的是不一樣的,此時,系統(tǒng)會提示用戶,用戶登錄密碼錯誤,可以進(jìn)行重試。
Tips:
1. 在我們的 userLogin 方法中存在一個參數(shù) session ,我們只是對 session 進(jìn)行了簡單介紹,因為這不屬于我們的重點內(nèi)容,所以,有不了解的同學(xué)可以自行查閱資料來了解;
2. 可以看到,在 userLogin 方法中,存在一行 userLogin.setPassword("") ,這句代碼的作用是將用戶登錄成功后的密碼設(shè)置為空,并不會同步更新數(shù)據(jù)庫,并將 userLogin 用戶數(shù)據(jù)返回給前端,出于安全考慮,這里將密碼設(shè)置為空了,前端是獲取不到用戶的登錄密碼的;
3. userLogin 方法中所使用的 userMapper 為 MyBatis 數(shù)據(jù)庫 ORM 框架所定義的內(nèi)容,有不了解的同學(xué)可以自行查閱資料進(jìn)行了解,本小節(jié)不會對其進(jìn)行介紹。
通過上述代碼的編寫,我們基本上實現(xiàn)了傳統(tǒng)的用戶登錄功能,但是這種登錄功能是最簡單的實現(xiàn),完全沒有考慮其他因素,如果我們在實際項目中這樣來實現(xiàn),那么在開發(fā)后續(xù)功能時,必定會出現(xiàn)一些意料之中的問題。
那么,我們采用這種實現(xiàn)方式所實現(xiàn)的用戶登錄功能到底有哪些弊端呢?下面就讓我們一探究竟。
2.2 傳統(tǒng)實現(xiàn)方案中存在的弊端
整體來說,我們傳統(tǒng)的實現(xiàn)用戶登錄功能的業(yè)務(wù)邏輯還是正確的, 不管我們再怎么對用戶登錄功能進(jìn)行優(yōu)化,這個傳統(tǒng)的用戶登錄功能業(yè)務(wù)流程始終不會發(fā)生改變,發(fā)生改變的只是我們的代碼實現(xiàn)層。
那么,傳統(tǒng)實現(xiàn)方案中存在的弊端,也就存在于我們的功能代碼實現(xiàn)層上。
弊端一 用戶登錄狀態(tài)的處理
對于上述代碼來說,我們處理用戶登錄狀態(tài)所采用的手段,是通過將用戶登錄成功后的數(shù)據(jù)存放在服務(wù)端中的 session 中,后續(xù)如果需要使用到用戶登錄狀態(tài),我們可以直接對 session 進(jìn)行判斷,并返回相應(yīng)的判斷結(jié)果。
但是,這種模式如果遇到了前后端分離的項目,我們就無法再使用 session 了,因為前端框架不能直接操控我們后端的 session ,所以,我們就要把 session 替換掉,替換成前后端均可操控的手段來實現(xiàn)。
弊端二 高并發(fā)環(huán)境下容易出現(xiàn)的問題
如果我們的項目在使用過程中出現(xiàn)了用戶激增的情況,即會有越來越多的人來訪問我們的用戶登錄功能,如果我們只是采用上述的實現(xiàn)方式來處理,不會這種情況進(jìn)行考慮,那么我們的用戶登錄功能就會在非常短的一段時間過后癱瘓,我們的用戶登錄功能不會再返回任何響應(yīng)數(shù)據(jù)。
這就是高并發(fā)環(huán)境下傳統(tǒng)的用戶登錄功能最容易出現(xiàn)問題的地方,而這一地方則體現(xiàn)在用戶登錄前和用登錄后的數(shù)據(jù)不一致問題上。
即,在高并發(fā)環(huán)境下,我們的用戶 A 進(jìn)行了登錄,與此同時,我們的用戶 B 也進(jìn)行了登錄,這兩個用戶進(jìn)行登錄的時刻恰好重疊了,由于我們并沒有對這種場景進(jìn)行處理,所以,在這兩個用戶登錄之后,就有可能出現(xiàn)返回給用戶 A 的用戶數(shù)據(jù),其實是用戶 B 的。
除了數(shù)據(jù)不一致問題,還有一種問題也比較容易出現(xiàn),那就是當(dāng)我們的用戶數(shù)激增時,同一時刻進(jìn)行登錄的用戶也隨之激增,我們的用戶登錄功能接口無法在一瞬間響應(yīng)這么多的用戶登錄請求,導(dǎo)致了后續(xù)登錄的用戶只能等待,只能等待當(dāng)先用戶登錄完成之后,才能進(jìn)行登錄。
這個等待過程是不確定的,可能很長,也可能很短,這就對用戶的體驗造成了非常嚴(yán)重的不良影響。
上述就是兩個在傳統(tǒng)用戶登錄功能中,出現(xiàn)的顯而易見的問題,我們會對這兩個功能進(jìn)行優(yōu)化,并且會使用 RabbitMQ 對第二個弊端進(jìn)行優(yōu)化。
3 使用 RabbitMQ 優(yōu)化傳統(tǒng)用戶登錄功能
3.1 優(yōu)化用戶登錄狀態(tài)的處理問題
前面我們已經(jīng)介紹了,在傳統(tǒng)用戶登錄功能中,當(dāng)我們的項目架構(gòu)采用前后端分離的架構(gòu)方式來管理時,出現(xiàn)的用戶登錄狀態(tài)的問題,那么我們可以怎樣來優(yōu)化這個問題呢?
優(yōu)化出現(xiàn)的用戶登錄狀態(tài)問題,不需要我們使用 RabbitMQ 來處理,我們可以將 session 參數(shù)使用一種 token 機制來替代掉。
即在處理用戶狀態(tài)時,我們不使用 session 來存儲,而是使用 token 來進(jìn)行存儲,這個 token 我們一般會使用 JWT 框架來生成,并為生成的 token 設(shè)置一個有效期。
這樣一來,當(dāng)前端發(fā)來用戶登錄請求時,就會將用戶登錄成功的 token 進(jìn)行傳遞,然后我們后臺需要接收這個 token ,并將這個加密的 token 進(jìn)行解析。這樣一來,前端在用戶登錄成功之后,會將這個 token 保存到前端的狀態(tài)機中,而后端默認(rèn)是會存儲這個 token 的。
這就實現(xiàn)了一種前后端都可以操作這個 token 的功能,通過這樣的優(yōu)化,無論是前后端分離項目,還是其他類型的一些項目,我們都可以很好地處理用戶的登錄狀態(tài)和用戶數(shù)據(jù)。
3.2 優(yōu)化高并發(fā)環(huán)境下的用戶登錄功能
在高并發(fā)環(huán)境下,我們的用戶登錄功能需要在同一時刻,處理大量的用戶請求,如果我們的用戶登錄功能沒有考慮到這樣的業(yè)務(wù)場景,也就不會對這種業(yè)務(wù)場景進(jìn)行優(yōu)化,所以,一旦請求量上來之后,我們的用戶登錄功能肯定就會癱瘓,不能正常使用了。
我們可以從兩個角度去優(yōu)化高并發(fā)環(huán)境下的用戶登錄功能。第一個角度就是優(yōu)化我們的數(shù)據(jù)庫訪問頻率。
角度一:優(yōu)化數(shù)據(jù)庫訪問頻率
我們都知道,在后臺程序代碼中,比較耗時的操作無疑就是訪問數(shù)據(jù)庫了,因為訪問數(shù)據(jù)庫之前,我們需要先通過代碼的方式,來將我們的項目與數(shù)據(jù)庫相連接,最后,通過 ORM 框架來達(dá)到對數(shù)據(jù)庫中的數(shù)據(jù)增刪改查的一個目的。
我們在對數(shù)據(jù)庫中的數(shù)據(jù)進(jìn)行增刪改查時,需要我們將處理好的數(shù)據(jù)傳入到數(shù)據(jù)庫中,然后數(shù)據(jù)庫通過執(zhí)行 SQL 腳本來根據(jù)我們的數(shù)據(jù)去處理數(shù)據(jù)庫中的數(shù)據(jù),并最后返回給我們處理數(shù)據(jù)的結(jié)果。
在用戶登錄功能中,訪問數(shù)據(jù)庫的次數(shù)一般都是兩次,第一次是校驗當(dāng)前要登錄的用戶名在系統(tǒng)中是否存在;第二次是校驗當(dāng)前要登錄的用戶名和密碼是否在系統(tǒng)中匹配。 只有這兩個校驗通過之后,用戶才能成功登錄,而數(shù)據(jù)庫的耗時操作也就出現(xiàn)在這兩個過程中。
那么我們應(yīng)該怎么來優(yōu)化呢?
我們可以使用 Redis 來緩存我們的用戶請求數(shù)據(jù),即當(dāng)有用戶請求數(shù)據(jù)請求我們的用戶登錄功能接口時,我們會首先進(jìn)行第一次的數(shù)據(jù)庫操作,此時,在數(shù)據(jù)庫將操作結(jié)果返回給我們時,我們可以將這一結(jié)果緩存在 redis 中,后續(xù)如果再有相同的請求時,我們可以直接訪問 redis ,而不是再次重復(fù)地去訪問我們的數(shù)據(jù)庫。
這樣可以提高我們的服務(wù)響應(yīng)速度,因為訪問 redis 的速度要比單純訪問數(shù)據(jù)庫的速度快很多。所以,第二次的數(shù)據(jù)庫操作我們也可以這樣來進(jìn)行處理。
Tips: 關(guān)于什么是 Redis ,以及 redis 的一些基本操作,我們會在后續(xù)的小節(jié)中進(jìn)行介紹,這里只介紹功能優(yōu)化的思路,感興趣的同學(xué)可以自行查閱資料實現(xiàn)。
這是第一種優(yōu)化思路,接下來讓我們看第二種優(yōu)化思路,即優(yōu)化我們的用戶請求序列。
角度二:優(yōu)化用戶請求序列
此種優(yōu)化措施,就需要使用我們的 RabbitMQ 了。
我們都知道,高并發(fā)環(huán)境下的請求數(shù)量是非常多的,那么,對于一個用戶登錄功能接口而言,在高并發(fā)環(huán)境下接收的用戶請求也是非常多的,而且這些用戶請求都是無序的。
即,例如我們在一瞬間有 5000 個用戶登錄請求,根據(jù)計算機 CPU 以及操作系統(tǒng)的處理時序可以得出,在這 5000 個用戶登錄請求中,只要誰先獲取到了 CPU 資源,誰就會先執(zhí)行,那么其他沒有獲取到 CPU 資源的請求就只能等待,這就是高并發(fā)環(huán)境下的資源搶占問題。
對于 CPU 來說,我們的一個用戶請求就代表著是一個計算機線程,比如我們現(xiàn)在有兩個用戶請求,即兩個線程來訪問我們的用戶登錄接口,我們分別使用線程 A 和線程 B 來命名。
假設(shè)我們的線程 A 先訪問到了我們的用戶登錄接口,而線程 B 比線程 A 晚 1 秒才訪問到,那么,當(dāng)我們的線程 A 在執(zhí)行業(yè)務(wù)邏輯時,線程 B 也會跟著執(zhí)行。
如果我們的線程 A 的用戶數(shù)據(jù)比較長,需要執(zhí)行數(shù)據(jù)庫查詢所需的時間較長,而線程 B 的用戶數(shù)據(jù)則比較端,需要執(zhí)行數(shù)據(jù)庫查詢所需的時間較短,那么,在統(tǒng)一時刻,可能晚于線程 A 才訪問到的用戶登錄接口線程 B 就會先于線程 A 執(zhí)行結(jié)束。
這就會造成:我們實際上是處理的線程 A 的業(yè)務(wù)邏輯,但是返回的時候卻將線程 B 的用戶數(shù)據(jù)進(jìn)行了返回,這也是高并發(fā)環(huán)境下非常容易出現(xiàn)的一個問題。
那么我們應(yīng)該怎么來優(yōu)化呢?
我們可以使用 RabbitMQ 來作為一個消息隊列,當(dāng)用戶請求來請求我們的服務(wù)時,我們可以將所有的用戶請求放入到我們的 RabbitMQ 消息隊列中,由于 RabbitMQ 消息隊列可以設(shè)置單條消息進(jìn)行消費,所以,我們的用戶請求在處理時就會逐條進(jìn)行處理,不會出現(xiàn)上述所描述的問題。
我們來簡單看下這種問題的優(yōu)化代碼,優(yōu)化代碼如下所示:
實現(xiàn)代碼:
public Response<User> userLogin(User user, HttpSession session){
rabbitTemplate.convertAndSend("userLoginQueue", "user.login", user);
// 獲取用戶數(shù)據(jù)并處理用戶登錄業(yè)務(wù)邏輯
}
代碼解釋:
第 2 行,我們使用了 rabbitTemplate 的 convertAndSend 方法,來將我們的用戶登錄數(shù)據(jù)發(fā)送到 RabbitMQ Server 中。
由于消費者的實現(xiàn)比較復(fù)雜,考慮到篇幅原因,所以在這里代碼就沒有給出。
通過上述代碼,當(dāng)有用戶請求我們的用戶登錄接口時,會首先將該請求的用戶登錄數(shù)據(jù)存儲到我們的 RabbitMQ Server 中,存儲完成后,接著會從 RabbitMQ Server 中將該消息進(jìn)行取出并消費,只有一條消息被消費之后,RabbitMQ 才會繼續(xù)消費下一條消息,這就保證了用戶請求的有序性,也就不會出現(xiàn)上述所提到的問題。
Tips: 我們在優(yōu)化用戶登錄功能時,一定要先理解我們所優(yōu)化問題的產(chǎn)生原因,這樣我們才能從根本上優(yōu)化這一問題。
4. 小結(jié)

本小節(jié)為同學(xué)們詳細(xì)介紹了傳統(tǒng)的用戶登錄功能的基本概念和基本實現(xiàn)方法,以及在傳統(tǒng)的用戶登錄功能中所出現(xiàn)的可優(yōu)化的問題,針對出現(xiàn)頻率較高的問題,我們分別介紹了不同問題的不同優(yōu)化措施,并針對可以使用 RabbitMQ 進(jìn)行優(yōu)化的場景,我們使用了 Spring 生態(tài)中的 RabbitMQ 中的方法來進(jìn)行了優(yōu)化。