文章目錄
- 前言
- 頁面緩存+URL緩存+對象緩存
- 頁面緩存
- 取緩存
- 手動渲染
- URL緩存
- 對象緩存
- 頁面靜態化,前后端分離(常用)
- GET POST區別
- 如何解決超賣?重復賣?(簡單版)
- 靜態資源優化
- 多個JS/CSS組合,減少連接數
- CDN優化
- 總結
前言
本篇重點學習頁面緩存,頁面靜態化,剩下的了解即可。
頁面緩存+URL緩存+對象緩存
業務的瓶頸就是數據庫,而這些頁面(大粒度),對象(小粒度)都需要查詢數據庫 所以最大力度加緩存。
頁面緩存
什么是頁面緩存?我們訪問一個頁面的時候,不是讓服務端渲染而是從緩存里邊去,如果找到返回給客戶端,如果沒有再讓服務端渲染并返回給客戶端,同時保存到redis中。
與之前沒有頁面緩存相比,省時省力在哪?
首先,之前的方法是我們每次請求一個頁面,服務端都需要從數據庫中查詢相關數據并渲染到頁面中然后返回給客戶端
頁面緩存如何改進的? 直接從緩存中獲取頁面的信息并直接返回給客戶端。 省去查詢數據庫和渲染頁面。什么是渲染頁面?在服務端渲染頁面時從接受請求開始,查詢數據庫,然后加載JS/CSS文件再將動態數據添加到頁面中,最后返回一個完整的html頁面的過程
以商品列表為例
如何實現?1取緩存 2緩存未命中的話手動渲染頁面并返回
取緩存
//如果緩存中有頁面且TTL沒有過期
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);if(!StringUtils.isEmpty(html)) {return html;}
手動渲染
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);if(!StringUtils.isEmpty(html)) {return html;}//如果沒有先查詢數據庫List<GoodsVo> goodsList = goodsService.listGoodsVo();//添加到頁面中model.addAttribute("goodsList", goodsList);SpringWebContext ctx = new SpringWebContext(request,response,request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );//手動渲染,goods_list是具體html文件,要不然thymeleafViewResolver也不知道頁面長啥樣啊html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);//存儲到緩存中if(!StringUtils.isEmpty(html)) {redisService.set(GoodsKey.getGoodsList, "", html);}return html;
這里需要注意的是,我們將頁面緩存到redis時都會設置有效期,過了有效期我們就需要重新從數據庫中加載
URL緩存
URL緩存本質上就是頁面緩存的一種,只不過需要在地址欄中傳參數。區別就是get和set方法都要有路徑的參數
@RequestMapping(value="/to_detail2/{goodsId}",produces="text/html")@ResponseBodypublic String detail2(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,@PathVariable("goodsId")long goodsId) {model.addAttribute("user", user);//取緩存String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);if(!StringUtils.isEmpty(html)) {return html;}//手動渲染GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);model.addAttribute("goods", goods);long startAt = goods.getStartDate().getTime();long endAt = goods.getEndDate().getTime();long now = System.currentTimeMillis();int miaoshaStatus = 0;int remainSeconds = 0;if(now < startAt ) {//秒殺還沒開始,倒計時miaoshaStatus = 0;remainSeconds = (int)((startAt - now )/1000);}else if(now > endAt){//秒殺已經結束miaoshaStatus = 2;remainSeconds = -1;}else {//秒殺進行中miaoshaStatus = 1;remainSeconds = 0;}model.addAttribute("miaoshaStatus", miaoshaStatus);model.addAttribute("remainSeconds", remainSeconds);
// return "goods_detail";SpringWebContext ctx = new SpringWebContext(request,response,request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);if(!StringUtils.isEmpty(html)) {redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);}return html;}
對象緩存
像我們之前緩存user一樣,每次從緩存中取出對應token的user。但我們這里的有效期是動態的,也就是用戶在有效期沒過期之前登錄了就會自動延長有效期(具體就是從緩存里邊查詢出來不為空,就延長)。和之前的靜態有效期有區別。
頁面靜態化,前后端分離(常用)
為什么有這個呢?
你如果只做了頁面緩存,會發現渲染頁面其實本質上還是服務端做
但你做了頁面靜態化,瀏覽器會將html頁面緩存在客戶端,這樣頁面數據渲染時客戶端做,遇到動態的數據再去請求服務端。
前端:對于動態數據我們需要獲取,添加以下方法
function render(detail){//獲取相關參數var miaoshaStatus = detail.miaoshaStatus;var remainSeconds = detail.remainSeconds;var goods = detail.goods;var user = detail.user;if(user){$("#userTip").hide();}$("#goodsName").text(goods.goodsName);$("#goodsImg").attr("src", goods.goodsImg);$("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"));$("#remainSeconds").val(remainSeconds);$("#goodsId").val(goods.id);$("#goodsPrice").text(goods.goodsPrice);$("#miaoshaPrice").text(goods.miaoshaPrice);$("#stockCount").text(goods.stockCount);countDown();
}
$(function(){//countDown();getDetail();
});function getDetail(){var goodsId = g_getQueryString("goodsId");$.ajax({url:"/goods/detail/"+goodsId,type:"GET",success:function(data){if(data.code == 0){render(data.data);}else{layer.msg(data.msg);}},error:function(){layer.msg("客戶端請求有誤");}});
}function countDown(){
//倒計時函數var remainSeconds = $("#remainSeconds").val();var timeout;if(remainSeconds > 0){//秒殺還沒開始,倒計時$("#buyButton").attr("disabled", true);$("#miaoshaTip").html("秒殺倒計時:"+remainSeconds+"秒");timeout = setTimeout(function(){$("#countDown").text(remainSeconds - 1);$("#remainSeconds").val(remainSeconds - 1);countDown();},1000);}else if(remainSeconds == 0){//秒殺進行中$("#buyButton").attr("disabled", false);if(timeout){clearTimeout(timeout);}$("#miaoshaTip").html("秒殺進行中");}else{//秒殺已經結束$("#buyButton").attr("disabled", true);$("#miaoshaTip").html("秒殺已經結束");}
}
后端:需要取出相關數據并返回,這里注意方法要加@ResponseBody 因為是返回數據,不是返回頁面了。
@RequestMapping(value="/detail/{goodsId}")@ResponseBodypublic Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,@PathVariable("goodsId")long goodsId) {//查詢數據庫GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);long startAt = goods.getStartDate().getTime();long endAt = goods.getEndDate().getTime();long now = System.currentTimeMillis();int miaoshaStatus = 0;int remainSeconds = 0;if(now < startAt ) {//秒殺還沒開始,倒計時miaoshaStatus = 0;remainSeconds = (int)((startAt - now )/1000);}else if(now > endAt){//秒殺已經結束miaoshaStatus = 2;remainSeconds = -1;}else {//秒殺進行中miaoshaStatus = 1;remainSeconds = 0;}GoodsDetailVo vo = new GoodsDetailVo();vo.setGoods(goods);vo.setUser(user);vo.setRemainSeconds(remainSeconds);vo.setMiaoshaStatus(miaoshaStatus);返回數據return Result.success(vo);}
這樣是不是感覺比頁面緩存還慢呀,因為每次訪問時確實是瀏覽器解析頁面,但動態數據都需要去加載動態數據,而加載動態數據就需要查詢數據庫。而頁面緩存直接可以從緩存中取。其實這里還會有個配置,當沒過期時,瀏覽器直接加載緩存中的靜態頁面。無需請求服務端,當過期時,則需要重新訪問服務器重新加載動態數據。若在過期時間內,我們更新了數據,客戶端不會自動收到提示,我們需要主動提示客戶端我們更新數據了(這部分具體實現,之后會說,大家先了解這些原理)。
spring.resources.add-mappings=true
//過期時間
spring.resources.cache-period= 3600
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.gzipped=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/
當我們點擊秒殺時前端:
function doMiaosha(){$.ajax({url:"/miaosha/do_miaosha",type:"POST",data:{goodsId:$("#goodsId").val(),},success:function(data){if(data.code == 0){window.location.href="/order_detail.htm?orderId="+data.data.id;}else{layer.msg(data.msg);}},error:function(){layer.msg("客戶端請求有誤");}});}
需要將商品id傳遞過去 ,這里為什么是post(重點)
GET POST區別
這道題面試會經常問,網上答案很多,最重要的區別是以下兩點(不要再說查詢數據和請求數據了,這倆本質上都能查詢和請求)
GET 冪等 無論你get多少次,服務端數據不會變化,且狀態不會改變
POST 顯然不冪等 你提交數據(新增,添加,修改,刪除),服務端數據可能會變化,會改變狀態。
改變狀態怎么理解?比如登錄方法 客戶端請求方式為POST ,服務端數據不會發生改變,但是登錄完了之后會創建session啊,生成token等等一系列操作。
為什么 <a href="/delete?id=xxx">
是很嚴重的錯誤?
1 首先a標簽是超鏈接 它是get方法,服務端數據不應該發生變化。
2 其次用戶很容偽造一些get方法來刪除數據,因為get方法主要通過路徑參數來刪除數據。
改為post就安全了嘛? 是的 post方法通過請求體傳參,而請求體里邊可以包含token,我們更容易通過token先校驗然后再取數據操作。
如何解決超賣?重復賣?(簡單版)
解決超賣
本節課解決超賣的方式很簡單,就是將更新數據庫時的SQL語句加個判斷條件,如果庫存大于0則執行更新。為什么在這里加可以,因為數據庫系統 會采用MVCC機制(主頁里講過),修改操作(增,刪,改)sql語句會加鎖,所以多線程并發訪問時,一個表只會有一個線程修改,所以我們可以直接在SQL語句中加上判斷條件。
解決重復賣?
什么是重復賣?一個用戶對于秒殺商品購買了兩次
如何解決 為相應的字段的添加唯一索引。比如秒殺商品表中有用戶id和商品id字段我們為這兩個字段加上唯一索引。這樣用戶id和商品id的字段值在此表中只能有一個,重復秒殺會報錯。
唯一索引并不是指“這張表中只有一個索引”,而是指“這個索引要求字段值唯一”。一張表可以有多個索引,包括多個普通索引和多個唯一索引。
普通索引:允許表中的字段值重復。例如,在 users 表的 username 字段上創建普通索引,可以快速查找 username,但同一個 username 可以出現多次。
唯一索引:要求表中的字段值必須唯一。如果嘗試插入或更新違反唯一性約束的數據,數據庫會拋出錯誤。
靜態資源優化
這些在我們代碼中體現不出來,所以自行search吧 。面試中也不是特別重要
多個JS/CSS組合,減少連接數
CDN優化
總結
并發大的瓶頸 是在于數據庫
如何解決?
加緩存,首先用戶發起請求,瀏覽器通過頁面靜態化,可以直接把頁面緩存到瀏覽器,請求到達服務端之前請求會首先訪問CDN節點。通過CDN,繼續到服務端之前 還可以加ngix緩存,ngix沒有就是服務端的頁面緩存,然后在細粒度是對象緩存,最后才是數據庫。