使用RabbitMQ優(yōu)化用戶注冊功能
1. 前言
Hello,大家好。在上個小節(jié)中,我們對傳統(tǒng)的用戶登錄功能做了基本的流程介紹,以及使用 RabbitMQ 偽代碼對傳統(tǒng)的用戶登錄功能進行了優(yōu)化。
考慮到用戶登錄功能往往會和用戶注冊功能同步出現(xiàn),所以,本小節(jié)會為同學們介紹,如何使用 RabbitMQ 消息中間件,去優(yōu)化我們使用傳統(tǒng)的方式,來實現(xiàn)的用戶注冊的功能點,包括對傳統(tǒng)用戶注冊功能的概述,以及使用 RabbitMQ 進行優(yōu)化的關鍵步驟,希望同學們可以對本節(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)項目中,項目的首要功能模塊,基本上都會要求我們來設計并實現(xiàn)一個完備的用戶模塊,而這個用戶模塊中,必定會包含用戶注冊與用戶登錄功能點。
只有在項目中實現(xiàn)了用戶注冊與用戶登錄這兩個最基本的功能點,我們的項目功能才能繼續(xù)向后進行開發(fā),因為項目后續(xù)的功能,或多或少都會與這兩個功能點相聯(lián)系。
所以,這就要求我們在實現(xiàn)用戶注冊與用戶登錄功能時,要從多方面進行考慮, 如果有一點我們沒有考慮到,很可能在進行項目后續(xù)功能開發(fā)時,還要回過頭去,修改這兩個基本功能,從而與項目后續(xù)的功能相結合。
那么,傳統(tǒng)的用戶注冊功能的實現(xiàn)方案是什么樣的呢?
傳統(tǒng)用戶注冊功能流程介紹
傳統(tǒng)的用戶注冊功能,其實和傳統(tǒng)的用戶登錄功能差別并不大,我們首先接觸用戶注冊功能,大部分的同學都是在自己學校的實驗課上,都是通過老師講授的方式來對用戶注冊功能有所了解,但是,這種實現(xiàn)方式都會存在諸多問題,下面讓我們來看一下這種方式是如何實現(xiàn)的。
對于用戶注冊功能來說,我們從事后端開發(fā)的同學,我們首先需要接收前端傳遞過來的數(shù)據(jù),也就是用戶的注冊數(shù)據(jù),然后我們會根據(jù)這一用戶注冊數(shù)據(jù)來進行相應的邏輯檢驗。
這些邏輯校驗包括對用戶所填寫的注冊用戶名、郵箱、手機等用戶唯一關鍵信息屬性的校驗。如果用戶所填寫的用戶名在系統(tǒng)中已經(jīng)存在了,那么用戶就不能使用該用戶名進行注冊了,郵箱和手機則同理,這里不再贅述。
在對注冊用戶名、注冊郵箱,以及注冊手機進行唯一性校驗之后,我們還需要對用戶所填寫注冊郵箱和注冊手機號進行邏輯合法校驗,即用戶所填寫的郵箱到底是不是一個可用的郵箱,以及用戶所填寫的手機到底是不是一個可用的手機號。下面,我們來通過一個功能流程圖來體現(xiàn)上述的業(yè)務流轉過程:

通過上述功能流程圖,我們可以很清楚地理解上述傳統(tǒng)用戶注冊功能的實現(xiàn)過程,如果沒有理解的同學,建議多看幾次,理解這一流程之后我們再繼續(xù)學習下面的內(nèi)容。
傳統(tǒng)用戶注冊功能代碼實現(xiàn)
以 SpringBoot 框架為例,我們來實現(xiàn)一下上述的用戶注冊功能,實現(xiàn)代碼如下所示:
實現(xiàn)代碼:
public Response<String> userRegister(User registUser){
int userExistsCount = userMapper.selectUserExixtsByUsername(registUser.getUsername());
if (userExistsCount > 0){
return Response.createByError("用戶已存在");
}
int userEmailCount = userMapper.selectUserEmailByUsernameEmail(registUser.getUsername(), registUser.getEmail());
if (userEmailCount > 0){
return Response.createByError("郵箱已存在");
}
int userPhoneCount = userMapper.selectUserPhoneByUsernamePhone(registUser.getUsername(), registUser.getPhone());
if (userPhoneCount > 0){
return Response.createByError("手機已存在");
}
int userRegist = userMapper.insert(registUser);
if (userRegist > 0){
return Response.createBySuccess("用戶注冊成功");
}
return Response.createByError("系統(tǒng)錯誤,用戶注冊失敗");
}
代碼解釋:
第 1 行,我們定義了一個名為 userRegister 的方法來處理用戶注冊的業(yè)務邏輯,該方法的返回值是 Response 類型,并且泛型被指定為 String 類型,這表明,我們的方法最終會返回注冊業(yè)務邏輯的字符串數(shù)據(jù)。
與用戶登錄方法不同,在該方法中,我們只需要定義一個參數(shù),那就是 User 類型的 registUser 參數(shù)。該參數(shù)負責接收前端傳遞到后臺的用戶注冊數(shù)據(jù),并且程序會根據(jù)這個數(shù)據(jù)做一些邏輯判斷。
第 2-24 行,我們分別使用了不同的數(shù)據(jù)庫查詢方法,來分別對用戶的注冊用戶名、注冊郵箱,以及注冊手機進行了邏輯唯一性校驗,即檢測這些關鍵字段屬性是不是已經(jīng)存在于系統(tǒng)中了,如果系統(tǒng)中已經(jīng)有了,那表明,可能當前需要注冊的用戶已經(jīng)注冊過了,不能重復注冊。
第 25-30 行,如果前面的邏輯校驗都通過,那么我們就要使用 MyBatis 自帶的 insert 方法,將這一合法的用戶注冊數(shù)據(jù),插入到我們的數(shù)據(jù)庫中所對應的用戶數(shù)據(jù)表中,并且在數(shù)據(jù)插入數(shù)據(jù)庫成功之后,提示用戶,當前用戶已經(jīng)注冊成功了。
如果在將用戶注冊數(shù)據(jù)插入到數(shù)據(jù)庫的過程中,出現(xiàn)了任何問題,導致用戶注冊數(shù)據(jù)無法插入到數(shù)據(jù)庫中,則提示當前注冊的用戶:系統(tǒng)錯誤,用戶注冊失敗。此時,需要等待系統(tǒng)管理員解決。
Tips: 1. 在用戶所填寫的用戶注冊數(shù)據(jù)通過邏輯檢驗時,我們應該將用戶注冊數(shù)據(jù)插入到我們的數(shù)據(jù)庫中,而不是直接放入到緩存中去,否則,我們剛剛注冊的用戶是無法進行登錄的;
2. 傳統(tǒng)的用戶注冊功能,在用戶注冊成功之后,我們不需要將用戶注冊成功的數(shù)據(jù)進行返回,或者進行一個狀態(tài)的處理,這點需要同學們注意;
3. 在將用戶注冊數(shù)據(jù)插入到數(shù)據(jù)庫的過程中,我們使用了 MyBatis 自帶的 insert 方法,同學們可以了解一下這個 insert 方法的作用。
通過上述代碼的編寫,我們基本上實現(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è)務邏輯還是正確的, 不管我們再怎么對用戶注冊功能進行優(yōu)化,這個傳統(tǒng)的用戶注冊功能業(yè)務流程始終不會發(fā)生改變,發(fā)生改變的只是我們的代碼實現(xiàn)層。
那么,傳統(tǒng)實現(xiàn)方案中存在的弊端,也就存在于我們的功能代碼實現(xiàn)層上。
傳統(tǒng)用戶注冊功能中存在的弊端,整體來說,最核心的也就只有一種弊端了。
那就是,無論我們的用戶注冊功能所在的場景是不是在高并發(fā)環(huán)境中,都會直接對我們的數(shù)據(jù)庫的訪問壓力造成較高的沖擊, 如果在高并發(fā)環(huán)境中,我們的數(shù)據(jù)庫可能會隨著用戶注冊請求數(shù)量的激增,而直接崩潰,這是非常致命的一點。
我們先來說非高并發(fā)環(huán)境下,用戶注冊數(shù)據(jù)在邏輯檢測通過之后,會直接訪問我們的數(shù)據(jù)庫,并將用戶注冊數(shù)據(jù)插入到我們的數(shù)據(jù)庫中,正常環(huán)境下我們的數(shù)據(jù)庫還是能扛得住的,但是,即使能扛得住,數(shù)據(jù)庫的壓力在此時也是較高的。
那么,在高并發(fā)環(huán)境下,由于我們沒有在用戶數(shù)據(jù)和數(shù)據(jù)庫之間做處理,這就導致,當大量請求都在同一時刻到來時,我們的數(shù)據(jù)庫的壓力會直線上升,并最終導致數(shù)據(jù)庫崩潰,這會直接造成我們的用戶注冊功能直接卡死,無法完成用戶注冊功能。
針對這種情況,我們又該如何優(yōu)化呢?下面就讓我們來看一下,如何使用 RabbitMQ 來優(yōu)化這一弊端。
3 使用 RabbitMQ 優(yōu)化傳統(tǒng)用戶注冊功能
我們注意到,對于用戶注冊功能而言,無論是在普通環(huán)境,還是在高并發(fā)環(huán)境,我們的傳統(tǒng)邏輯,都是在用戶注冊數(shù)據(jù)校驗正確后,直接地將用戶數(shù)據(jù)插入到數(shù)據(jù)庫中,中間并沒有一層過渡的措施。
正是由于我們?nèi)鄙龠@一措施,我們的數(shù)據(jù)庫壓力才會持續(xù)升高,而這一持續(xù)升高的結果,和普通環(huán)境和高并發(fā)環(huán)境并沒有太大的本質(zhì)因素,所以,我們的優(yōu)化重點就放在了這一中間措施上面。
針對這一問題,我們同樣的有兩種角度可以考慮:
角度一:使用緩存承載中間壓力
在眾多緩存中間件中,使用頻率和普適度最高的,要數(shù) redis 緩存中間件了。
對于用戶注冊而言,當用戶提交的注冊數(shù)據(jù)通過了我們的邏輯校驗之后,我們可以使用 redis 來將該用戶注冊數(shù)據(jù)進行存儲,并在一個固定的時間內(nèi),將位于 redis 緩存中的用戶注冊數(shù)據(jù),同步插入到我們的數(shù)據(jù)庫中, 這樣既可實現(xiàn) redis 緩存數(shù)據(jù)與數(shù)據(jù)庫數(shù)據(jù)之間的同步。
與此同時,當我們添加了 redis 緩存中間件之后,我們所對應的用戶登錄功能也要進行相應的調(diào)整,即從之前用戶登錄時,對數(shù)據(jù)庫所做的校驗,換做優(yōu)先對 redis 緩存中的用戶注冊數(shù)據(jù)進行校驗。
Tips: 在將用戶注冊數(shù)據(jù)存入 redis 緩存中間件前,我們應該設置好我們的 redis 緩存 key 值的生成策略,目的就是將不同業(yè)務場景所對應的 key 值進行區(qū)分。
當我們這樣設置之后,合法的用戶注冊數(shù)據(jù)會被優(yōu)先存儲到 redis 緩存中間件中,然后,在某一固定時間周期內(nèi),系統(tǒng)會自動將 redis 緩存中間中的數(shù)據(jù)同步到我們的數(shù)據(jù)庫中,這樣一來,我們的 redis 緩存中間件就承擔了大部分的數(shù)據(jù)庫壓力。
這是第一種優(yōu)化思路,接下來讓我們看第二種優(yōu)化思路,即通過消息隊列來分發(fā)數(shù)據(jù),從而減少數(shù)據(jù)庫的壓力。
角度二:采用消息隊列分發(fā)數(shù)據(jù)壓力
此種優(yōu)化措施,就需要使用我們的 RabbitMQ 了。
在角度一中所提到的優(yōu)化措施,在很多業(yè)務場景中是沒有大問題的,但是在高并發(fā)環(huán)境下,會出現(xiàn)一個問題,那就是由于數(shù)據(jù)同步的延遲,導致的用戶無法登錄的問題。 所以,當用戶注冊功能處于高并發(fā)環(huán)境下時,我們必須要使用 RabbitMQ 消息隊列來進行優(yōu)化了。
我們都知道,高并發(fā)環(huán)境下的請求數(shù)量是非常多的,那么,對于一個用戶注冊功能接口而言,在高并發(fā)環(huán)境下接收的用戶請求也是非常多的。
我們可以使用 RabbitMQ 來作為一個消息隊列,當合法的用戶注冊數(shù)據(jù)需要插入數(shù)據(jù)庫時,我們可以將數(shù)據(jù)發(fā)送到我們的 RabbitMQ 的消息隊列中去,然后我們定義的消費者會從消息隊列中獲取并消費該數(shù)據(jù)。
當消費者將消息隊列中的用戶注冊數(shù)據(jù)進行消費之后,我們可以將這一數(shù)據(jù)直接插入到數(shù)據(jù)庫中,無須經(jīng)過角度一中提到的 redis 緩存中間件層,因為我們的 RabbitMQ 消息隊列已經(jīng)對數(shù)據(jù)壓力進行了分發(fā)。
即,當有一個用戶注冊請求需要處理時,RabbitMQ 就會在消息隊列中存儲這個請求所對應的合法的用戶注冊數(shù)據(jù),并在消息進行消費之后,再將數(shù)據(jù)進行持久化存儲。
這樣一來,通過集成 RabbitMQ 消息隊列,將用戶注冊時的數(shù)據(jù)壓力通過消息隊列進行分發(fā),從而達到減小數(shù)據(jù)庫壓力的目的。
我們來簡單看下這種問題的優(yōu)化代碼,優(yōu)化代碼如下所示:
實現(xiàn)代碼:
public Response<String> userRegister(User registUser){
int userExistsCount = userMapper.selectUserExixtsByUsername(registUser.getUsername());
if (userExistsCount > 0){
return Response.createByError("用戶已存在");
}
int userEmailCount = userMapper.selectUserEmailByUsernameEmail(registUser.getUsername(), registUser.getEmail());
if (userEmailCount > 0){
return Response.createByError("郵箱已存在");
}
int userPhoneCount = userMapper.selectUserPhoneByUsernamePhone(registUser.getUsername(), registUser.getPhone());
if (userPhoneCount > 0){
return Response.createByError("手機已存在");
}
boolean isAck = false;
rabbitTemplate.convertAndSend("userRegistQueue", "user.register", user);
// 省略消費者消費數(shù)據(jù)過程 isAck = true;
if(isAck) {
int userRegist = userMapper.insert(registUser);
if(userRegist > 0) {
return Response.createBySuccess("用戶注冊成功");
}
return Response.createByError("系統(tǒng)錯誤,用戶注冊失敗");
}
return Response.createByError("系統(tǒng)錯誤,用戶注冊失敗");
}
代碼解釋:
第 1-19 行,像傳統(tǒng)用戶注冊那樣,我們對用戶所提交的注冊數(shù)據(jù)進行了校驗,直到用戶的注冊數(shù)據(jù)通過了我們的邏輯檢測為止。
第 20 行,我們聲明了一個 boolean 類型的變量 isAck ,并且將他的默認值設為了 false ,該變量表示我們的消息是否已經(jīng)被消費了。
第 21 行,我們使用 rabbitTemplate 的 convertAndSend 方法,將合法的用戶注冊數(shù)據(jù)發(fā)送到 RabbitMQ 的消息隊列中去,等待消費者消費。
第 22 行,我們對 isAck 進行了檢測,當消費者成功從消息隊列中獲取并消費了用戶注冊數(shù)據(jù)的消息之后,isAck 標志位會被置位 true 。
第 23-28 行,如果 isAck 標志位為 true ,則將用戶注冊數(shù)據(jù)插入到我們的數(shù)據(jù)庫中,并提示用戶注冊成功,如果用戶數(shù)據(jù)在插入數(shù)據(jù)庫過程中遇到問題,導致數(shù)據(jù)無法插入,則提示用戶:系統(tǒng)錯誤,用戶注冊失敗。
由于消費者的實現(xiàn)比較復雜,考慮到篇幅原因,所以在這里代碼就沒有給出。
Tips: 使用 RabbitMQ 消息隊列去優(yōu)化用戶注冊功能時,一定要根據(jù)上述代碼片段的先后順序來進行優(yōu)化,特別是使用 RabbitMQ 代碼部分,同學們注意。
4. 小結

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