這篇博客是筆者學習慕課網若魚老師的《Java秒殺系統方案優化 高性能高並發實戰》課程的學習筆記。若魚老師授課循循善誘,講解由淺入深,歡迎大家支持。
本文記錄課程中的注意點,方便以后code review。此外,本文將注意點相關的優質講解鏈接在了一起,方便初學者系統學習。
本文並非單純介紹秒殺系統特有的技術點,不適合高手。進階學習的話,極客時間有個不錯的小專欄——如何設計一個秒殺系統,阿里高級技術專家講解秒殺系統的設計要點,那個課程挺干貨的。
設計秒殺系統的技術要點
1. 登錄的密碼傳輸:
用戶的數據庫表設計,需要增加一字段保存密碼的Salt值
兩次MD5操作(敏感數據一定要使用https協議傳輸
):
- 客戶端:將明文password和客戶端硬編碼的Salt值進行拼接,然后進行MD5操作。
不用鹽的話,MD5字符串有可能會被彩虹表或者社工庫破解
- 服務端:將客戶端傳過來的MD5字符串和數據庫用戶對應的Salt字段進行拼接。然后進行MD5操作。
這次加鹽MD5,可以有效防止內部員工泄露或者數據庫被拖庫后,明文密碼泄露
2. 自定義JSR303的校驗器
可以參照javax.validation.constraints.NotNull注解,自定義自己的校驗器,將校驗代碼與業務代碼分離。不過由於校驗失敗會輸出BindException異常,所以最好配合全局捕獲異常進行友好的輸出。
自定義校驗器很簡單,只需要定義一個注解和對應的校驗類
3. 自定義全局異常捕獲
使用@ControllerAdvice注解,定義全局的異常捕獲,並從異常中獲取異常信息解析出來,發送給前端
可以自定義一個GlobalException異常,利用全局異常捕獲,將所有服務器處理異常集中處理。(Service層處理異常后不設置狀態碼,而是直接拋GlobalException全局異常)
不返回狀態碼的好處是Controller層不需要再繁瑣的判斷Service層的返回值,代碼更簡潔
4. 數據庫表設計
- 通過將訂單建立唯一索引來保證用戶只能創建一個秒殺訂單
- 商品金額最好以分為單位,比較安全
- 商品ID最好不要使用自增,會暴露商品總數等信息。可以使用UUID,但范圍查找時會有性能損耗。所以一般采用SnowFlake算法生成ID
另外,自增ID的缺點也就是無法在多個表中,或者多個數據庫中保持ID主鍵唯一不重復,所以若是使用分布式數據庫以及數據合並的情況下時不能使用自增ID的。
5. 代碼規范
- 更新字段越多,產生的數據庫Binlog就越多。所以只更新數據庫部分字段的時候,最好新建一個對象,只賦值要更新的字段,然后調用mybatis的@Update,這樣不做全量更新可以提高性能
- 前端回包使用Result包裝類封裝,對報錯信息使用CodeMsg包裝類封裝,保持代碼風格統一
- Service只注入跟自己同名的dao,如果需要別的dao,請注入對應的Service
Service的api相比dao會多一些防御代碼(例如,直接修改了別的模塊dao數據,但緩存未清理),更加安全
6. 事務
秒殺有兩個事務:
- 減庫存->創建秒殺訂單
- 創建秒殺訂單
秒殺中涉及到上述兩個事務,為了保障數據安全,可以使用聲明式事務(Spring的@Transactional)
PROPAGATION_REQUIRED是Spring默認的傳播機制,如果外層有事務,則當前事務加入到外層事務,一塊提交,一塊回滾。本工程的場景使用默認事務傳播機制即可
有關Spring事務傳播機制可以查看這篇博客
7. 壓測
- 在生產環境中,秒殺系統要獨立運行與其他業務系統,實現資源隔離,避免業務系統相互影響穩定性
- 請求入口可以使用nginx,LVS,F5等不同的負載均衡器
- Jmeter 隨機生成用戶數據,然后使用Jmeter模擬用戶壓測。壓測運行環境最好與被測服務器環境隔離。
接口測試可以還使用Postman和ApacheBench
8. 頁面優化技術
- 頁面/URL緩存。用於數據變化不頻繁的頁面或者熱點網頁。如果數據較多需要分頁的數據,類似商品詳情數據,一般可以考慮只緩存前兩頁(根據訪問量作取舍)
緩存方法:將渲染好的html文件存放到Redis。在訪問Url時,首先檢測Redis是否有html緩存。有緩存的話則直接返回緩存;沒有緩存的話則渲染后存入Redis,並返回給前端。頁面緩存過期時間具體根據業務場景判斷。
-
頁面局部緩存。熱點數據緩存,當Ajax請求信息更新,涉及的可能是需要保存在數據庫的操作,例如表格信息等時,可以采用Redis緩存,方法同頁面緩存一樣,定義好可以區分業務的Key即可
-
靜態資源優化
- JS/CSS壓縮,減少流量(可通過升級HTTP2來解決)
- 多個JS/CSS組合,減少連接數(例如:tengine)
- CDN就近訪問
如果需要采用JS/CSS壓縮或者減少連接數等方法,可以使用HTTP2來提升性能
- 對象緩存。例如使用Redis保存Session對象。對象緩存涉及到一個雙寫一致性問題,有關雙寫一致性問可以查看這篇博客
9. 秒殺的邏輯優化
順序:
- 系統初始化,把商品庫存數量加載到Redis
- 收到請求,Redis原子操作預減庫存,庫存不足,直接返回,否則進入3
- 請求入隊,立即返回前端“排隊中”
- 請求出隊,生成訂單,減少庫存(服務端)
- 客戶端輪詢,是否秒殺成功(客戶端)和4同步,得到結果刷新結果顯示
優化:
- 在第二步預減庫存時,可以在內存里加一個map,ID為商品ID,value為是否有庫存,這樣當庫存沒有之后,直接通過內存中的值判斷是否還有庫存,減少對Redis的訪問。
- 購買請求加入消息隊列,異步下單(前端顯示排隊中),增強用戶體驗
- 前端要盡量減少重復請求
10. 安全優化
10.1 秒殺接口地址隱藏
- 每次點擊秒殺按鈕,先從服務器獲取動態拼接而成的秒殺地址。
- Redis以緩存用戶ID和商品ID為Key,秒殺地址為Value緩存秒殺地址
- 用戶請求秒殺商品的時候,要帶上秒殺地址進行校驗
10.2 數學公式驗證碼
- 防止惡意腳本搶購
- 使請求時間分散
10.3 接口限流防刷
使用計數法,在攔截器做限制請求頻率。利用Redis緩存的有效期(以用戶ID拼接Url作為key,以訪問次數為value),指定緩存有效期為1秒,訪問接口每次將value+1,到達閾值跳轉全局異常。
優化:使用攔截器+自定義注解,減少對業務代碼的侵入。有關攔截器可以查看這篇博客
另外對於接口限流也可以考慮使用令牌桶,控制對mysql的訪問。
最后,限於筆者經驗水平有限,歡迎讀者就文中的觀點提出寶貴的建議和意見。如果想獲得更多的學習資源或者想和更多的技術愛好者一起交流,可以關注我的公眾號『全菜工程師小輝』后台回復關鍵詞領取學習資料、進入前后端技術交流群和程序員副業群。同時也可以加入程序員副業群Q群:735764906 一起交流。