兩次MD5加密:前端
理解:java
第一次是爲了防止傳輸過程當中,用戶明文密碼被截取獲取。mysql
第二次是爲了防止數據庫被盜後,別人知道了密碼的md5值,而後直接僞造請求,傳遞這個md5值過來登陸成功;二次加密後,由於瀏覽器傳遞的值和數據庫中存放的值不一致,因此丟失後也不會形成密碼丟失問題。redis
生成請求地址一一對應的緩存,能防止瞬間併發高帶來的性能問題,但頁面的實時性就下降了,由於緩存所展現的內容老是固定的,過時從新渲染後,內容纔可能發生變化。算法
--建立秒殺庫存表 CREATE TABLE seckill( `seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品庫存id', `name` varchar (120) NOT NULL COMMENT '商品名稱', `number` int NOT NULL COMMENT '庫存數量', `start_time` timestamp NOT NULL COMMENT '秒殺開啓時間', `end_time` timestamp NOT NULL COMMENT '秒殺結束時間', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', PRIMARY KEY(seckill_id), key idx_start_time(start_time), key idx_end_time(end_time), key idx_create_time(create_time) )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒殺庫存表'; --秒殺成功明細表 CREATE TABLE success_killed( `seckill_id` bigint NOT NULL COMMENT '秒殺商品id', `user_phone` bigint NOT NULL COMMENT '用戶手機號,簡單起見經過手機來惟一對應用戶', `state` bigint NOT NULL DEFAULT -1 COMMENT '狀態表示:-1:無效,0:成功,1:已付款', `create_time` timestamp NOT NULL COMMENT '建立時間', PRIMARY KEY (seckill_id,user_phone), key idx_create_time(create_time) )ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒殺成功明細表';
客戶端刷新來訪問接口:以服務端的時間和數據庫中記錄的活動的時間進行對比,來判斷秒殺活動是否處於開啓中。開啓的話,則返回一個惟一令牌。sql
發送秒殺請求時,參數中須要有一個活動id加鹽md5計算得出的令牌,這個令牌與當前秒殺活動一一對應,之因此要傳令牌,而不是傳活動的id,是爲了防止別人在活動開始前作好工具來自動秒殺,活動id能提早拿獲得,而令牌是活動開始後才能拿獲得。這樣一來就算作好了秒殺工具,也要在活動開始以後來改參數,工具才能用。可是改參數的話,速度就比別人慢了,別人進入頁面,點一個按鈕就搶完了。這就從必定程度上防範了自動化秒殺工具的使用。數據庫
執行秒殺時,回傳秒殺地址中的md5令牌,服務端以相同的算法計算出實際令牌,校驗令牌準確性,準確才能夠執行後續操做。後端
理解:令牌僅僅在活動開始纔會給瀏覽器生成,而且秒殺操做必需要有令牌,就能保證秒殺的公平性。瀏覽器
以上令牌實際有兩種生成方式:緩存
1. md5(productId + 固定的鹽) => 令牌:每一個用戶的令牌都相同
2. md5(productId + userId) => 令牌 :每一個用戶的令牌都不一樣
不一樣人輸入驗證碼的速度不同,因此能夠下降(分散)一個短暫時間內的併發請求的壓力,同時還能防止機器人刷請求。
數學公式驗證碼,如1+2-3,輸入運算的結果
生成圖片背景:隨機的弧線、線段,用於干擾
生成公式:如生成三個隨機數,這三個隨機數之間插入兩個隨機的運算符,四則運算分別對應0-3這四個隨機數(生成這個公式注意/0的狀況)。
公式以字符串形式存在。使用ScriptEngineManager這個類對字符串公式進行運算,得出結果。而後結果對應用戶,保存到redis中,後續秒殺請求中,要對提交上來的驗證結果進行校驗。校驗經過以後,將redis中的結果移除掉,由於校驗經過一次後續就沒有必要再保留了
限制同一個用戶一段時間(10秒內或一分鐘等)內對接口的訪問次數。
每次訪問接口,次數記錄到redis中,key=接口名+userid。使用incr進行增長,設置這個key的有效期,超過以後自動刪除。遞增以後,若是大於某個值,則接口返回拒絕訪問。
實現思路:攔截器中計算訪問次數,沒有超過次數,則放行請求。
第一次設置的時候,就設置有效期爲1s:
註解實現配置化,同時設置好後續要用的用戶信息
建立一個攔截器,獲取方法上的註解,並進行計數判斷,肯定是否放行(光標處未給出)
在攔截器中獲取好user對象,設置到threadlocal中,接着service方法中就能獲取到了。
或者使用【注入自定義對象】,在處理器的處理方法中從threadLocal中獲取對象,這樣service方法直接注入user對象就可使用了
某一個商品進入秒殺時,成爲熱點數據,同一個數據行的競爭變得很是大。
服務啓動後,監聽bean生命週期,進行redis數據冗餘(productId->productCount)。
接收秒殺請求後,執行redis decr命令,返回數據自減以後的值,若是這個值大於0,說明秒殺成功,由於沒有操做mysql,實際的操做不多,一會兒就響應給客戶端了,提示客戶端正在排隊中,服務後臺往MQ中放一個消息,表示某個用戶秒殺到了某個商品。
客戶端輪訓服務器,以更新當前當前的排隊中的狀態,以得知是否成功,服務接口查詢mysql中的秒殺記錄,而後將當前的狀態返回給客戶端。
服務器不停地消費消息,實現數據落地(mysql中生成訂單、庫存數量減1、生成秒殺記錄)。這裏的邏輯相對於秒殺請求來講,是異步執行的,生成秒殺記錄用於給客戶端輪訓時更新客戶端的狀態。數據落地過程當中,可能還沒開始執行,則當前秒殺記錄爲空,執行成功、執行失敗時,都要記錄秒殺記錄,標記成功仍是失敗;失敗的狀況有:庫存數量減一時,發現沒得減了,則失敗。但這個狀況基本不會出現,由於已經提早冗餘了一份數據到redis中,redis能成功減一的話,mysql裏也能成功減一的,由於這兩份數據在最開始時,數值是同樣的。
以上的細節優化:
redis預減庫存時,調用decr函數,這其實是一個阻塞的網絡請求。這個函數返回的是減了以後的結果,因此只要有一次出現負數,後續的全部調用都會是負數,這樣的網絡請求沒有必要。因此在服務器內存中,創建一個map(productId -> boolean),標記某個商品是否出現負數了,作好標記後,後續再出現redis預減狀況,先判斷這個map,而不用每次都調用decr函數了,少一次網絡請求,提高接口的響應速度。
對MQ的理解:
不用MQ以前,觸發某些邏輯是直接調用對應的函數的,同步的話,則會形成阻塞。
使用MQ後,觸發某些邏輯就不是調用函數了,而是往消息隊列中發送一個消息,而後當前函數就結束了;
另外一個地方提早註冊好了消息的處理函數,這個函數會被異步執行,也就是這時候才實際觸發了某些邏輯。經過消息隊列來解除邏輯之間的同步關係,實現異步調用。
經過MQ實現了異步調用。這提高了QPS,可是感受有點奇怪,是否是能夠認爲這個QPS意義不大,就好比一我的來找你辦事情,你還沒辦就直接跟他說我登記一下,你先回去等通知吧,一小時來了一百我的,你都這麼處理,而後跟別說,我這一小時內處理了一百我的的事情,說這句話看上去效率很是高,但實際上有變高嗎?由於實際要作的事情時延遲作了。不使用異步處理的話,對於一小時的秒殺活動,則全部的數據處理必須在這一小時內處理完,這會給服務器帶來大負荷。而使用異步處理後,這一小時內只須要執行redis預減就好了,而不用執行數據落地,這一小時內執行的是秒殺活動最關鍵的部分,就是誰能搶獲得,就只作這個工做,其餘有空在作,如實際的數據落地和狀態更新能夠在這一小時以後才執行,至關於把這一小時內的集中的工做量,平攤到後續的一段時間內了。實際能夠理解爲:一件事情的處理效率沒有本質變化,只不過異步的話,可讓完成這件事情有更多的時間,因此完成起來就更加遊刃有餘了。
缺點:分佈式redis、分佈式MQ的運維成本、保證數據一致性、如何數據回滾、又須要一個分佈式系統來記錄誰已經秒殺過了,而不能再屢次秒殺、不適合新手
mysql測試,同一行的高併發update,能達到4W QPS左右,因此純粹討論mysql,也不慢了。
常規實現的瓶頸分析,一次秒殺過程須要發送4次數據庫指令:
開啓事務、update減庫存、(期間還可能會存在GC耗時)、insert添加購買明細、提交事務【數據庫服務器與java客戶端之間也存在網絡延時】
佔用行級鎖的時間 = update語句開始佔用的時間(而不是事務開始時間) 至 提交事務/回滾事務,釋放行級鎖時間
以上這個時間內包含了 GC耗時、update語句結果返回耗時,insert語句和結果往返耗時。簡單來講就是行級鎖佔用時間=GC耗時 + java服務端與mysql服務端的網絡通訊耗時
而對同一行數據的秒殺操做,併發量與行級鎖佔用時間直接相關。以上步驟出現併發執行時,只能串行。由於每一個事務鎖住的都是同一行數據。
優化方式:把Java客戶端邏輯放到mysql服務端,避免網絡延遲和GC耗時
1.mysql源碼層的修改方案,定製update語句,使update語句更新行爲1時,自動提交,小公司基本沒有這樣的團隊實力去作這樣的事
2.使用存儲過程,存儲過程的目的是將一組sql組成一個事務,在數據庫服務端完成,避免客戶端去完成事務,從而下降性能的消耗(避免了java GC耗時和mysql與java服務器之間的通訊耗時)
以上佔用行級鎖的時間中,包含了insert操做,因此把insert操做移出來,能提高下降行級鎖佔用時間。
把insert和update的位置互換一下。
被insert的表使用聯合主鍵(用戶手機、秒殺商品id),若是成功插入數據,說明用戶之前沒秒殺過,插入失敗則說明是重複秒殺了。插入成功返回1,失敗返回0(這個實現經過insert ignore來實現:加了ignore,若是出現主鍵衝突,則不報錯,而是返回0);
僅當插入成功後,才進行update減庫存。若是update影響行數是1,則說明能秒殺到,不然影響行數爲0,說明沒秒殺到。沒秒殺到,則拋出異常,好讓事務回滾。
以上優化延遲獲取行級鎖,而且行級鎖的獲取到釋放之間(事務回滾或提交)的操做更少了,則佔用行級鎖的時間更少了。原來的邏輯,期間有一個insert操做,互換位置以後,insert操做就不佔用行級鎖時間了。
理解:行級鎖佔用從update開始至事務結束,期間的操做越少越好,因此把insert移出去,行鎖佔用時間,併發效率更高。
// 先使用insert ignore插入秒殺記錄,返回影響行數 int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); if (insertCount <= 0) { //出現重複秒殺 throw new RepeatKillException("seckill repeated"); } else { // 庫存減一,這裏開始佔用行鎖: update table set num = num - 1 where num > 0 int updateCount = seckillDao.reduceNumber(seckillId, nowTime); if (updateCount <= 0) { //沒有更新到記錄,跑出異常,使事務回滾,還原以前的插入操做 throw new SeckillCloseException("seckill is closed"); } else { SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); // 返回秒殺成功 return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled); } }
使用存儲過程,避免GC耗時,以及最大程度下降了java服務端與mysql服務器之間的網絡通訊耗時。由於使用存儲過程,就只有一次請求往返耗時。
insert ignore:若是中已經存在相同的記錄(惟一索引、主鍵來判斷),則忽略當前新數據。【success_killed表使用id和phone做爲聯合主鍵】
-- 秒殺執行存儲過程 DELIMITER $$ -- console ; 轉換爲 $$ -- 定義存儲過程 -- 參數: in 輸入參數; out 輸出參數 -- row_count():返回上一條修改類型sql(delete,insert,update)的影響行數 -- row_count: 0:未修改數據; >0:表示修改的行數; <0:sql錯誤/未執行修改sql CREATE PROCEDURE `seckill`.`execute_seckill` (in v_seckill_id bigint,in v_phone bigint, in v_kill_time timestamp,out r_result int) BEGIN DECLARE insert_count int DEFAULT 0; START TRANSACTION; insert ignore into success_killed (seckill_id,user_phone,create_time) values (v_seckill_id,v_phone,v_kill_time); select row_count() into insert_count; IF (insert_count = 0) THEN ROLLBACK; set r_result = -1; ELSEIF(insert_count < 0) THEN ROLLBACK; SET R_RESULT = -2; ELSE update seckill set number = number-1 where seckill_id = v_seckill_id and end_time > v_kill_time and start_time < v_kill_time and number > 0; select row_count() into insert_count; IF (insert_count = 0) THEN ROLLBACK; set r_result = 0; ELSEIF (insert_count < 0) THEN ROLLBACK; set r_result = -2; ELSE COMMIT; set r_result = 1; END IF; END IF; END; $$ -- 存儲過程定義結束
前端控制,點擊秒殺以後,隱藏按鈕,防止再次點擊
動靜態數據分離,CDN緩存(將請求從咱們的服務中剝離出去),後端緩存(redis)
事務競爭優化,減小事務鎖的時間
redis緩存:實現對熱點數據的快速存取,分攤mysql的壓力
java對象存入redis:序列化爲 byte[] 存入,取出byte[] ,反序列爲實際對象
序列化最高效的是protostuff。這是谷歌protobuf的再次包裝,而不須要本身再寫描述文件來幫助序列化和反序列化了,包裝後能動態生成描述文件
從redis獲取數據,並反序列化
序列化對象,並存入redis
作好的java服務,不直接暴露出去,而是經過ngnix反向代理
完整附件:https://files.cnblogs.com/files/hellohello/seckill-master.7z