1000個線程併發還能跑,5000個線程的時候出現這種問題,查後臺debug日誌,發現redis 線程池不夠。剛開始設置的是:html
# 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
後來看代碼發現,判斷庫存用的是if(stock==0 ) 拋出異常。應該用stock<0,由於 若此時同時2個線程進來,就永遠小於0,後面的業務邏輯均可以執行。java
第一次壓力測試的時候,5000個線程,分別取不一樣的token(sessionId),同時訪問 秒殺這個接口,商品個數只放了20個。結果出現最後商品數量變負的問題。ajax
接口限流防刷的時候,經過計數器限流,若是超過某個閾值,向前端返回一個codeMsg對象用於顯示的時候,顯示的是String是亂碼的問題,以前因爲一直返回都是json 格式,都是封裝好在data裏。redis
此次返回是直接經過輸出流直接寫到response直接返回字節數組的,而不是spring controller 返回數據(springboot 默認utf-8),出現亂碼問題,用utf-8編碼,解決。算法
壓測是利用Jmeter壓測。(Apache開發的基於java的壓測工具)。spring
1.在數據庫中提早插入5000個用戶密碼(腳本 for循環 id是13000000+i),密碼統一爲「123456」,隨機鹽值也是固定的,方便操做。用JDBC存入數據庫。做爲5000個備用用戶。sql
2.而後寫了一個小腳本讓5000個用戶post請求個人登錄接口(login),生成sessionId並存入緩存,並改寫了一下login接口讓其換回sessionId。把這5000個用戶的id和對應sessionid寫到了一個TXT文件裏面。數據庫
3.最後利用jmeter 建立5000個線程,帳號每一個線程攜帶提早寫好的用戶token(sessionId),參數就是商品id和sessionid,商品id肯定我要買的票是哪一個,sessionid用來獲取用戶信息。(從緩存中拿)json
qps-126/s----靜態化-250/s---接口優化-860/s.
瓶頸主要是對數據庫的訪問。
1.數據庫讀取,寫入,處理請求的速度。
數據庫讀取寫入加上網絡IO速度很慢,減小對數據庫的訪問,在緩存這一端就屏蔽掉大部分訪問數據庫的請求(Redis預減庫存操做)
2.利用消息隊列,異步業務邏輯的處理速度慢,能夠先返回結果,讓其輪詢。
3.利用內存map,減小對Redis服務器的訪問,flag機制。
4.其餘想到的但還沒實現
服務器系統的負載均衡+集羣
數據庫數據達到1000W以上就很慢,分庫分表
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 做爲值,存入緩存(這裏爲分佈式緩存提供的基礎)。這時候跳轉到 搶票列表頁面,若是密碼不匹配,拋出異常,返回。
短期的大訪問量 網站服務器 同網站,不一樣項目部署,/獨立域名 避免對網站形成影響 高併發問題,不停刷新 數據庫 頁面靜態化
同網站,不一樣項目部署,/獨立域名 避免對網站形成影響 寬帶 同網站,不一樣項目部署,/獨立域名 避免對網站形成影響 不能提早下單 服務器 url動態化,+隨機數
下單以後的搶的問題 sql 樂觀鎖
1.Redis預減庫存減小數據庫訪問
2.map標記減小Redis訪問屏蔽必定的請求減輕緩存壓力
3.消息隊列異步處理
先給前端一個數據返回表示排隊中,再進行後續的業務處理,前端輪詢最後成功或者失敗在顯示業務結果
4.數據庫運行的問題,傳統的sql寫成存儲過程(直接調用),加速sql
5.數據庫裏鎖及惟一索引來處理搶的問題。
頁面靜態化,緩存在客戶端
CDN服務器
在上表中列出來的解決方案中看出,利用 頁面靜態化、數據靜態化,反向代理 等方法能夠避免 帶寬和sql壓力 ,可是隨之而來一個問題,頁面搶單按鈕也不會刷新了,能夠把 js 文件單獨放在js服務器上,由另一臺服務器寫 定時任務 來控制js 推送。
另外還有一個問題,js文件會被大部分瀏覽器緩存,咱們可使用xxx.js?v=隨機數 的方式來避免js被緩存
更爲激進的緩存方式(以前能夠用將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);