倪 明
(江陰職業(yè)技術學院)
隨著我國高職教育的迅速發(fā)展,我校招生規(guī)模逐年擴大,為了適應這些變化,必須改變以往僵硬的教學體系,推進學分制是我校教學管理改革的一個重要舉措。根據(jù)實際情況,我校沒有采用純學分制,而是選擇了學年學分制。
學生選課是學分制的前提和基礎,目前我校選修課主要分為公共選修課、專業(yè)選修課及體育選修課等幾種。隨著我校數(shù)字化校園的建設和發(fā)展,學生選課已經(jīng)從人工選課轉換為網(wǎng)絡選課。如何提高選課系統(tǒng)的性能,為學生選課提供更好的服務,成為選課系統(tǒng)的一項關鍵指標。
我校選課系統(tǒng)問題主要出現(xiàn)在公共選修課及體育選修課,學生在集中選課的過程中,系統(tǒng)經(jīng)常無法響應,造成學生無法正常選課。分析其原因,主要存在以下兩方面的問題:
圖1 負載均衡
選課過程中,熱門課程必定成為學生優(yōu)先的選擇。由于選修課人數(shù)限制,學生必然會在選課系統(tǒng)開放的同時進行選課,短時間內(nèi)大量學生的涌入,這是一個高并發(fā)的過程,當系統(tǒng)無法在這個時間段抗住高并發(fā),就會造成系統(tǒng)無法響應。
選課約束條件復雜體現(xiàn)在多個方面,如選課學分約束、選課門數(shù)約束、學生專業(yè)約束、上課時間約束、選修前導課約束、選修課人數(shù)等。如此多的約束條件處理,必將消耗大量系統(tǒng)資源,造成系統(tǒng)運行緩慢。
如果只用一臺服務器,服務器處理的并發(fā)數(shù)是有限的,選課系統(tǒng)開啟后,大量學生進入選課,Tomcat服務器一般默認只能開啟150個線程來處理并發(fā)任務,一旦并發(fā)數(shù)超過這個線程,新的請求只能排隊等候處理。這里我們采用Nginx組為負載均衡服務器,Nginx服務器將所有請求分配到多個Tomcat服務器。
Nginx主要有輪詢、最少連接數(shù)、加權、IP-Hash四種負載均衡策略。輪詢負載策略是將請求輪流發(fā)送到響應的Tomcat服務器,最少連接數(shù)負載策略是將請求發(fā)送到當前連接數(shù)最少的Tomcat服務器,加權負載策略是優(yōu)先把請求發(fā)送到權重高的Tomcat服務器。以上三種策略不能將每個客戶端的請求固定分配到同一臺Tomcat服務器上,當用戶登錄信息是保存在某一臺服務器上時,用戶的下次請求可能就會分配到另外一臺服務器上,這時候就需要用戶重新進行登錄驗證,所以我們采用IPHash負載策略,此策略可以將每個客戶端的請求隨機固定到某一臺Tomcat服務器。
當選課系統(tǒng)啟動時,首先將課程班信息存入Redis緩存。
課程班信息包括課程班Id、課程名稱、學分、課程類別、任課教師Id、任課教師姓名、限選人數(shù)、已選人數(shù)、未選人數(shù)、上課時間、上課地點、選修狀態(tài)等相關信息,并為課程班添加UUID隨機碼加密鏈接。這些課程班信息通過Redisson生成一個分布式信息量,信息量的Key為UUID隨機碼,Value為課程班的限選人數(shù)。
學生選課時,為了防止惡意請求,將選課系統(tǒng)開啟時產(chǎn)生的UUID作為課程班信息的關鍵字,禁止使用課程班Id作為關鍵字,這樣就能防止學生在選課系統(tǒng)未開啟時進行搶課。
選課開始產(chǎn)生的選課的課程班信息及每個課程班的分布式信息量需要存入Redis緩存。存入Redis緩存一般有Key-Value和Hash兩種形式。
采用Key-Value形式又能分為兩種方法。
第一種方法,將課程班UUID作為Key,課程班的其它信息作為一個序列化對象作為Value存儲,如圖2所示。
圖2 Key-序列化Value存儲
采用這種方法的缺點是,學生選課后,修改選課人數(shù)時,需要將整個Value對象取出,增加序列化/發(fā)序列化開銷,并且在修改操作中需要對數(shù)據(jù)進行并發(fā)保護。
第二種方法是將課程班信息按課程班UUID+數(shù)據(jù)項存成多個Key-Value,如圖3所示。
圖3 Key-Vlaue存儲
采用這種方法可以解決第一種方法中序列化/反序列化的開銷問題,但是存在大量課程班UUID重復數(shù)據(jù),存儲空間浪費較大。
采用以上兩種Key-Value形式存儲時雖然實現(xiàn)比較簡單,但存在系統(tǒng)開銷比較大、存儲空間浪費等缺點,采用Hash形式能很好的解決這些問題。
Hash形式將課程班信息數(shù)據(jù)項作為一個HashMap存入,并且Redis提供了直接存取Map成員的接口,如圖4所示。
從圖4可以看出,Key仍然是課程班UUID,value是一個Map,這個Map的field是課程班信息數(shù)據(jù)項的屬性名,value是數(shù)據(jù)項的值,這樣對課程班信息的修改和存取都可以直接通過其內(nèi)部Map的field,也就是通過Key(課程班UUID)+field(數(shù)據(jù)項標簽)就可以操作對應數(shù)據(jù)了。這樣就解決Key-Value序列化/反序列化開銷過大,在修改操作中需要對數(shù)據(jù)進行并發(fā)保護以及重復存儲課程班UUID數(shù)據(jù)的問題。
圖4 Hash存儲
由于選課系統(tǒng)部署在Tomcat服務器集群,每個Tomcat服務器都會同時把數(shù)據(jù)存入Redis緩存,為了保證Redis中只存在一條線程的緩存數(shù)據(jù),因此在緩存前需要判斷Key是否已經(jīng)存在,并且還需要通過Redisson加分布式鎖,由于多線程同時緩存只會在選課系統(tǒng)開啟時出現(xiàn),因此不需要采用默認的看門狗機制,只需要對分布式鎖設置超時時間,超時后自動解鎖。
當學生點擊確認選課按鈕后,經(jīng)過Nginx網(wǎng)關過濾,來到Tomcat選課接口。首先經(jīng)過身份驗證,驗證通過后獲取學生Id,然后從Redis中獲取選課開始時間和結束時間,用來判斷請求是否在可選課時間段內(nèi)。以上驗證通過后,查詢Redis中選課狀態(tài)Hash結構中是否存在該學生Id及所選課程班Id,不存在則為該學生選擇了該課程班,存在則為該學生退選了該課程班,然后修改該課程班的分布式信息量。
在學生點擊確認選課按鈕時,由于前端卡頓無法及時設置按鈕灰色不可使用,學生有可能會連續(xù)點擊多次按鈕,造成一瞬間發(fā)起多個請求,從而造成連續(xù)扣減分布式信息量,結果就是該學生重復多次選擇了該課程班。
這里我們采用Token令牌來解決這個冪等性問題。首先在學生點擊選課按鈕時,在返回給課程班詳細信息中生成一個Token令牌,并在Redis中也保存這個Token令牌,設置過期時間如10分鐘。學生點擊確認選課后,相應的數(shù)據(jù)和Token令牌都傳給后端,后端收到數(shù)據(jù)后,將數(shù)據(jù)中的Token令牌和Redis中的令牌進行比較,如果兩者相等,則刪除Redis中的Token令牌。這個比較刪除操作必須作為一個整體執(zhí)行,中間不能被其它命令插入,因此采用Lua腳本來執(zhí)行這個原子操作。這樣學生一瞬間發(fā)起的多個請求,后端接收第一個請求后就會立即刪除Redis中的Token令牌,后續(xù)請求因無法匹配到相應的Token令牌而失敗,從而避免了該學生重復多次選擇該課程班。
學生點擊確認選課后,為了保證實時性要求,需要在Redis中根據(jù)課程班Id和學號Id,在選課狀態(tài)Hash結構中設置為“已選”狀態(tài),同時根據(jù)課程班Id從課程班Hash結構中獲取該課程班的信息,再將其結果存入選課信息Hash結構里,其中Key為課程班Id+學號Id,Value為課程班信息。這樣學生就能實時查到課程的選修狀態(tài)及選修了哪些課程。
前面學生選課操作都在Redis緩存中進行,速度較快,但是最終數(shù)據(jù)還是要寫入數(shù)據(jù)庫中。由于對數(shù)據(jù)庫進行讀寫操作相對于內(nèi)存讀寫是一個緩慢的過程,同步把數(shù)據(jù)寫入數(shù)據(jù)庫,這又將成為選課系統(tǒng)的一個瓶頸。
要解決這個瓶頸,首先要做到系統(tǒng)解耦,采用的方式是學生選課成功后生成一個選課單,選課單包括課程班Id、學號Id、選課狀態(tài)(選課、退選)以及用于解決可能存在的消息亂序問題而加入的創(chuàng)建時間,然后這個選課單傳給RabbitMQ消息隊列,然后由RabbitMQ消息隊列把選課單存入數(shù)據(jù)庫,如圖5所示。
圖5 RabbitMQ消息隊列
RabbitMQ消息發(fā)送后可能因為網(wǎng)絡原因沒有到達Broker或者Broker未持久化消息而宕機。為了解決這個問題,RabbitMQ發(fā)送消息之前,首先將相關信息存入Redis,Redis采用AOF持久化,消息狀態(tài)為未投遞并設置投遞計數(shù)器為0,每次該消息投遞后,計數(shù)器就加1,Broker收到消息后通過ConfirmCallback回調(diào)修改消息為已投遞,并通過定時器把投遞失敗的消息重新發(fā)送,當投遞計數(shù)器到5時,就將該消息交給死信隊列進行人工處理。通過這些操作就可以解決RabbitMQ消息丟失的問題,保障消息的傳輸可靠。
當RabbitMQ消息投遞后,可能由于網(wǎng)絡原因造成Broker收到消息通過ConfirmCallback回調(diào)失敗,RabbitMQ會將消息重復投遞,從而會產(chǎn)生消息重復發(fā)送的問題。此處采用后臺接口處理,后臺接口接收到數(shù)據(jù)后,根據(jù)課程班Id和學號Id來判斷是否接收過此數(shù)據(jù),從而保證數(shù)據(jù)冪等性。
當選課高峰時,系統(tǒng)無法及時處理RabbitMQ中的消息,未及時處理的消息將積壓在消息隊列中,等選課高峰一過,系統(tǒng)就會把積壓的消息處理完成,從而達到消峰的作用。
通過RabbitMQ消息隊列解耦、消峰操作,從而解決在學生選課高峰時的高并發(fā)操作,順利把選課數(shù)據(jù)存入數(shù)據(jù)庫。
本次選課系統(tǒng)優(yōu)化的目的是保證大量學生同時選課的情況下系統(tǒng)不宕機,并提高系統(tǒng)的QPS,為學生提供更好的選課體驗。為了驗證優(yōu)化效果,采用Apache JMeter5.5進行壓力測試。
首先導出系統(tǒng)中5000個學生的學號,用于模擬5000個學生同時選課,并使用JMeter運行測試方案,優(yōu)化前及優(yōu)化后的測試結果如表1所示。
表1 JMeter匯總報告
從以上JMeter匯總報告可以得出優(yōu)化后系統(tǒng)性能得到很大的提升,在學生實際使用中,系統(tǒng)也未出現(xiàn)無法響應甚至宕機,此次優(yōu)化效果結果比較滿意。
根據(jù)我校選課系統(tǒng)目前存在的問題,在學生選課高峰時,系統(tǒng)容易出現(xiàn)運行緩慢甚至宕機。硬件上采用服務器集群與負載均衡,軟件上使用Redis緩存來處理選課時高速的讀寫操作及使用RabbitMQ消息隊列處理選課結果。最后使用JMeter進行壓力測試,在學生正式選課中也取得了良好的效果。