1.Redis伺服器 can not get resource from pool.
1000個執行緒併發還能跑,5000個執行緒的時候出現這種問題,查後臺debug日誌,發現redis 執行緒池不夠。剛開始設定的是:
# redis 配置檔案
#redis
redis.host=127.0.0.1
redis.port=6379
redis.timeout=300 等待時間 10s改為300s
redis.password=123456
redis.poolMaxTotal=1000 連線數,剛開始最大連線數 設定為100.
redis.poolMaxIdle=500 最大空閒連線數 100改成500
redis.poolMaxWait=300
複製程式碼
順便也改了一下jdbc 的連線池引數,最大空閒和最大連線數都改成1000.在測一下。可以
spring.datasource.filters=stat
spring.datasource.maxActive=1000
spring.datasource.initialSize=100
spring.datasource.maxWait=60000
spring.datasource.minIdle=500
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxOpenPreparedStatements=20
複製程式碼
2.5000併發下的問題,20個商品,庫存減到-4980。
後來看程式碼發現,判斷庫存用的是if(stock==0 ) 丟擲異常。應該用stock<0,因為 若此時同時2個執行緒進來,就永遠小於0,後面的業務邏輯都可以執行。
3.然後就是超賣的問題
第一次壓力測試的時候,5000個執行緒,分別取不同的token(sessionId),同時訪問 秒殺這個介面,商品個數只放了20個。結果出現最後商品數量變負的問題。
4.編碼的問題
介面限流防刷的時候,通過計數器限流,如果超過某個閾值,向前端返回一個codeMsg物件用於顯示的時候,顯示的是String是亂碼的問題,之前由於一直返回都是json 格式,都是封裝好在data裡。
這次返回是直接通過輸出流直接寫到response直接返回位元組陣列的,而不是spring controller 返回資料(springboot 預設utf-8),出現亂碼問題,用utf-8編碼,解決。
5.壓測是如何壓測的,以及壓測的瓶頸?
壓測是利用Jmeter壓測。(Apache開發的基於java的壓測工具)。
壓測具體實現:
1.在資料庫中提前插入5000個使用者密碼(指令碼 for迴圈 id是13000000+i),密碼統一為“123456”,隨機鹽值也是固定的,方便操作。用JDBC存入資料庫。作為5000個備用使用者。
2.然後寫了一個小指令碼讓5000個使用者post請求我的登陸介面(login),生成sessionId並存入快取,並改寫了一下login介面讓其換回sessionId。把這5000個使用者的id和對應sessionid寫到了一個TXT檔案裡面。
3.最後利用jmeter 建立5000個執行緒,賬號每個執行緒攜帶提前寫好的使用者token(sessionId),引數就是商品id和sessionid,商品id確定我要買的票是哪個,sessionid用來獲取使用者資訊。(從快取中拿)
壓測的瓶頸:
qps-126/s----靜態化-250/s---介面優化-860/s.
瓶頸主要是對資料庫的訪問。
1.資料庫讀取,寫入,處理請求的速度。
資料庫讀取寫入加上網路IO速度很慢,減少對資料庫的訪問,在快取這一端就遮蔽掉大部分訪問資料庫的請求(Redis預減庫存操作)
2.利用訊息佇列,非同步業務邏輯的處理速度慢,可以先返回結果,讓其輪詢。
3.利用記憶體map,減少對Redis伺服器的訪問,flag機制。
4.其他想到的但還沒實現
伺服器系統的負載均衡+叢集
資料庫資料達到1000W以上就很慢,分庫分表
6.使用者登陸的整個流程是如何實現的?
1.首先輸入登陸頁面的url.http://localhost:8080/login/to_login,controller根據map對映返回給html頁,到達登陸頁面
2.整個頁面是一個login表單,包含使用者名稱和密碼兩個輸入框部分,還有一個登陸按鈕和重置按鈕。
3.在前端,給登陸按鈕繫結一個login()方法,login()方法中會獲取表單中的使用者名稱和密碼,然後將密碼利用封裝好的md5()函式以及設定的固定鹽值進行拼接,鹽值設定為“1a2b3c”,然後進行MD5演算法生成4個32位拼接的雜湊值作為輸入密碼(用於 網路傳輸),作為引數傳給後端。(這裡的目的主要是第一道加密,防止http明文傳輸,洩漏密碼)。
4.然後ajax非同步訪問do_login 介面,引數為使用者名稱和md5之後的密碼,後端接收到前端傳輸來的引數後,會對使用者名稱和密碼進行引數校驗,驗證是否為空,是否有格式問題(密碼長度6位以上,使用者名稱格式11位等等),如果驗證不通過,返回CodeMsg(),封裝好的對應的錯誤資訊給前端。
5.如果驗證成功,進入下一步,使用者的登陸,首先通過使用者名稱取使用者物件資訊(先從快取中取,取不到取資料庫取,取到了將使用者資訊存入快取中,下一次登入我們可以先從快取中取使用者,降低資料庫壓力),然後返回一個user物件,再判斷這個user物件是否為空,若是空就丟擲異常,不是空的情況說明資料庫中有該使用者,然後根據傳入的密碼和資料中儲存的隨機鹽值,進行md5再次拼接,獲得的值若是和資料庫中的密碼一致,那麼說明登陸成功。
關鍵點: 6.登陸成功的時候,隨機生成uuid作為sessionId,將其寫入cookie中返回給客戶端,並且將模組字首+該使用者id作為key和sessionId 作為值,存入快取(這裡為分散式快取提供的基礎)。這時候跳轉到 搶票列表頁面,如果密碼不匹配,丟擲異常,返回。
7.秒殺的兩個關鍵點如何應對--高併發應對策略+頁面載入速度?
短時間的大訪問量 網站伺服器 同網站,不同專案部署,/獨立域名 避免對網站造成影響
高併發問題,不停重新整理 資料庫 頁面靜態化
同網站,不同專案部署,/獨立域名 避免對網站造成影響 寬頻 同網站,不同專案部署,/獨立域名 避免對網站造成影響
不能提前下單 伺服器 url動態化,+隨機數
下單之後的搶的問題 sql 樂觀鎖
大量訪問高併發的應對(主要訪問大量訪問資料庫崩潰)
1.Redis預減庫存減少資料庫訪問
2.map標記減少Redis訪問遮蔽一定的請求減輕快取壓力
3.訊息佇列非同步處理
流量削峰 開始搶購的瞬間 大量併發進入,先將請求入隊,若佇列滿了,那麼捨棄再入隊的請求返回一個異常
先給前端一個資料返回表示排隊中,再進行後續的業務處理,前端輪詢最後成功或者失敗在顯示業務結果
4.資料庫執行的問題,傳統的sql寫成儲存過程(直接呼叫),加速sql
5.資料庫裡鎖及唯一索引來處理搶的問題。
頁面載入速度
頁面靜態化,快取在客戶端
CDN伺服器
在上表中列出來的解決方案中看出,利用 頁面靜態化、資料靜態化,反向代理 等方法可以避免 頻寬和sql壓力 ,但是隨之而來一個問題,頁面搶單按鈕也不會重新整理了,可以把 js 檔案單獨放在js伺服器上,由另外一臺伺服器寫 定時任務 來控制js 推送。
另外還有一個問題,js檔案會被大部分瀏覽器快取,我們可以使用xxx.js?v=隨機數 的方式來避免js被快取
8.頁面靜態化的過程
更為激進的快取方式(之前可以用將html原始碼快取起來再讀,避免伺服器渲染html過程)。
什麼是瀏覽器快取:
簡單來說,瀏覽器快取就是把一個已經請求過的Web資源(如html頁面,圖片,js,資料等)拷貝一份副本儲存在瀏覽器中。快取會根據進來的請求儲存輸出內容的副本。當下一個請求來到的時候,如果是相同的URL,快取會根據快取機制決定是直接使用副本響應訪問請求,還是向源伺服器再次傳送請求。比較常見的就是瀏覽器會快取訪問過網站的網頁,當再次訪問這個URL地址的時候,如果網頁沒有更新,就不會再次下載網頁,而是直接使用本地快取的網頁。只有當網站明確標識資源已經更新,瀏覽器才會再次下載網頁。
頁面靜態化的好處:
我們知道瀏覽器會將html,圖片等靜態資料,快取到本地,在高併發搶票場景,使用者會通過不斷的重新整理頁面來進行搶票操作,這樣帶來Web頻寬的浪費以及伺服器的訪問壓力。於是,我們可以通過將搶票頁面做成靜態頁面html頁,其中的票務資料通過ajax非同步呼叫介面來獲取,僅僅互動的是部分資料,減少了頻寬,也加快使用者訪問的速度。
function getDetail() {
var goodsId = g_getQueryString("goodsId");
$.ajax({
url : "/goods/to_detail/"+goodsId,
type : "GET",
success: function (data) {
if (data.code == 0) {// 訪問後端detail 介面拿到資料
render(data.data);//渲染介面的方法
}else {
layer.msg(data.msg)
}
},
error:function () {
layer.msg("客戶端請求有誤!")
}
})
}
function render(detail) {
var goodsVo =detail.goodsVo;
var miaoshaStatus =detail.miaoshaStatus;
var remainSeconds =detail.remainSeconds;
var user =detail.user;
if (user) {
$("#userTip").hide();//沒有就不展示
}
//用獲取的引數 放入 對應的模板中
$("#goodsName").text(goodsVo.goodsName);
$("#goodsImg").attr("src", goodsVo.goodsImg);
$("#startTime").text(new Date(goodsVo.startDate).format("yyyy-MM-dd hh:mm:ss"));
$("#remainSeconds").val(remainSeconds);
$("#goodsId").val(goodsVo.id);
$("#goodsPrice").text(goodsVo.goodsPrice);
$("#miaoshaPrice").text(goodsVo.miaoshaPrice);
$("#stockCount").text(goodsVo.stockCount);
countDown();//呼叫倒數計時
}
function countDown() {
var remainSeconds = $("#remainSeconds").val();
// var remainSeconds = $("#remainSeconds").val();
var timeout;//定義一個timeout 儲存Timeout 值
if (remainSeconds>0){//秒殺未開始
$("#buyButton").attr("disabled",true);/*還沒開始的時候按鈕不讓點*/
$("#miaoshaTip").html("秒殺倒數計時:"+remainSeconds+"秒");
/*且做一個倒數計時*/
timeout=setTimeout(function () {//setTimeout 為時間到了之後執行 該函式
$("#countDown").text(remainSeconds-1);//將顯示中的值 -1
$("#remainSeconds").val(remainSeconds-1);// remianSeconds 值減一
countDown();//在呼叫該方法 實現迴圈
},1000)
}else if (remainSeconds == 0){//秒殺進行中
$("#buyButton").attr("disabled",false);
//當remainSeconds =0
clearTimeout(timeout);//取消timeout 程式碼執行
$("#miaoshaTip").html("秒殺進行中!")//修改其中的內容
/**加入秒殺數學驗證碼 功能
* 1.一開始圖形驗證碼和輸入框都是隱藏的
* 2.當秒殺進行的時候,顯示驗證碼和輸入框
* */
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());//訪問驗證碼介面
$("#verifyCodeImg").show();
$("#verifyCode").show();
} else {//秒殺結束
$("#buyButton").attr("disabled",true);
$("#miaoshaTip").html("結束!!!")//修改其中的內容
}
}
複製程式碼
做法:首先將票務詳情這個template 模板 html頁放在static 檔案下,然後改掉thymeleaf 模板語言標籤讓其成為純html語言,然後將票務列表中的連結指向(本來是requestMapping,向後端contrller 請求這個詳情業務及資料,然後利用spring渲染模板,在返回的),現在直接指向static檔案下的票務詳情頁(連結中帶商品id作為引數),最後在這個html頁面寫ajax非同步訪問後端介面/getdetail,後端介面也改造一下返回的是這個商品的全部詳細資訊,封裝在data裡,以json的形式,然後寫了一個render(),把從後端傳來的資料寫進對應資料中。
/** 頁面靜態化:商品詳情頁面
* 方法:返回的是一個靜態html 頁面 + 利用ajax(通過介面)從服務端獲取對應資料 + js技術將資料放入html
* */
@RequestMapping(value = "/to_detail/{goodsId}") // 前端傳入的引數 goodsId
@ResponseBody
public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user,
@PathVariable("goodsId") Long goodsId){//通過註解@PathVariable獲取路徑引數
/*先將user 傳進去 用來判斷是否登入*/
model.addAttribute("user",user);
/*根據傳入的Id 通過service 拿到對應的Good資訊*/
GoodsVo goods = goodsService.getGoodsById(goodsId);
model.addAttribute("goods",goods);
long startTime = goods.getStartDate().getTime();
long endTime = goods.getEndDate().getTime();
long nowTime = System.currentTimeMillis();/* 拿到現在時間的毫秒值*/
/**這裡要做一個秒殺時間的判斷 秒殺開始 秒殺結束 秒殺進行
* */
int miaoshaStatus = 0;/*用該變數來表示 秒殺的狀態 0 表示秒殺未開始 1 開始 2 結束*/
int remainSeconds = 0; /*表示剩餘時間 距離秒殺開始的時間*/
if (nowTime<startTime){//秒殺未開始
miaoshaStatus = 0;
remainSeconds = (int)((startTime-nowTime)/1000);//注意此時是 毫秒值 要除以1000
}else if (endTime<nowTime){//秒殺結束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒殺進行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("remainSeconds",remainSeconds);
model.addAttribute("miaoshaStatus",miaoshaStatus);
/*
將我們需要的資料 封裝到GoodsDetailVo中
*/
GoodsDetailVo goodsDetailVo = new GoodsDetailVo();
goodsDetailVo.setGoodsVo(goods);
goodsDetailVo.setMiaoshaStatus(miaoshaStatus);
goodsDetailVo.setRemainSeconds(remainSeconds);
goodsDetailVo.setUser(user);
return Result.success(goodsDetailVo);
複製程式碼