java秒殺系列(2)- 頁面靜態化技術

  • 2019 年 10 月 3 日
  • 筆記

前言

通過程式碼片段分別介紹服務端渲染、客戶端渲染、對象快取三種方式的寫法。
程式碼片段僅供參考,具體實現需要根據業務場景自行適配,但思想都是一樣。

一、服務端渲染方式

1、介面返回html頁面的設置

@Autowired  ThymeleafViewResolver thymeleafViewResolver;  @Autowired  ApplicationContext applicationContext;    @RequestMapping(value="/to_list", produces="text/html")  @ResponseBody  public String goodsList() {      // 業務邏輯  }

2、先從快取中取,有就返回。

//取快取  String html = redisService.get(GoodsKey.getGoodsList, "", String.class);  if(!StringUtils.isEmpty(html)) {      return html;  }

3、快取中沒有,就手動渲染。

springboot1.5.x的寫法:

List<GoodsVo> goodsList = goodsService.listGoodsVo();  model.addAttribute("goodsList", goodsList);  SpringWebContext ctx = new SpringWebContext(request,response, request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );  //手動渲染  html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);

springboot2.x的寫法:

WebContext ctx = new WebContext(request, response, request.getServletContext(),  request.getLocale(), model.asMap());  //手動渲染  html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);

4、最後再將渲染內容加入到redis

if(!StringUtils.isEmpty(html)) {      redisService.set(GoodsKey.getGoodsList, "", html);  }

二、客戶端渲染方式(商品詳情頁)

1、找到跳轉到商品詳情頁的路徑,修改為一個靜態html的路徑。

在商品列表頁,找到跳轉到詳情頁的動態路徑,直接修改為一個靜態路徑,後綴為htm或shtml,總之不要是html即可,因為application.properties中一般會配置後綴為html的都訪問templates文件夾下的。
注意程式碼中詳情的鏈接,指向一個靜態頁面goods_detail.htm:

<body>    <div class="panel panel-default" style="height:100%;background-color:rgba(222,222,222,0.8)">    <div class="panel-heading">秒殺商品列表</div>    <table class="table" id="goodslist">      <tr><td>商品名稱</td><td>商品圖片</td><td>商品原價</td><td>秒殺價</td><td>庫存數量</td><td>詳情</td></tr>      <tr  th:each="goods,goodsStat : ${goodsList}">                  <td th:text="${goods.goodsName}"></td>                  <td ><img th:src="@{${goods.goodsImg}}" width="100" height="100" /></td>                  <td th:text="${goods.goodsPrice}"></td>                  <td th:text="${goods.miaoshaPrice}"></td>                  <td th:text="${goods.stockCount}"></td>                  <td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">詳情</a></td>       </tr>    </table>  </div>  </body>

2、根據1的路徑,在static目錄下新建一個goods_detail.htm文件,原本動態頁面上的模板語言都去掉,換成id=「xx」,然後使用ajax來渲染靜態文件中的數據即可。

原始動態頁面:

<div class="panel panel-default">    <div class="panel-heading">秒殺商品詳情</div>    <div class="panel-body">      <span th:if="${user eq null}"> 您還沒有登錄,請登陸後再操作<br/></span>      <span>沒有收貨地址的提示。。。</span>    </div>    <table class="table" id="goodslist">      <tr>          <td>商品名稱</td>          <td colspan="3" th:text="${goods.goodsName}"></td>       </tr>       <tr>          <td>商品圖片</td>          <td colspan="3"><img th:src="@{${goods.goodsImg}}" width="200" height="200" /></td>       </tr>       <tr>          <td>秒殺開始時間</td>          <td th:text="${#dates.format(goods.startDate, 'yyyy-MM-dd HH:mm:ss')}"></td>          <td id="miaoshaTip">              <input type="hidden" id="remainSeconds" th:value="${remainSeconds}" />              <span th:if="${miaoshaStatus eq 0}">秒殺倒計時:<span id="countDown" th:text="${remainSeconds}"></span>秒</span>              <span th:if="${miaoshaStatus eq 1}">秒殺進行中</span>              <span th:if="${miaoshaStatus eq 2}">秒殺已結束</span>          </td>          <td>              <form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">                  <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒殺</button>                  <input type="hidden" name="goodsId" th:value="${goods.id}" />              </form>          </td>       </tr>       <tr>          <td>商品原價</td>          <td colspan="3" th:text="${goods.goodsPrice}"></td>       </tr>        <tr>          <td>秒殺價</td>          <td colspan="3" th:text="${goods.miaoshaPrice}"></td>       </tr>       <tr>          <td>庫存數量</td>          <td colspan="3" th:text="${goods.stockCount}"></td>       </tr>    </table>  </div>

靜態化之後的頁面:可以看到,動態模板語言都去掉了,直接通過JS來賦值。

<div class="panel panel-default" style="height:100%;background-color:rgba(222,222,222,0.8)" >    <div class="panel-heading">秒殺商品詳情</div>    <div class="panel-body">      <span id="userTip"> 您還沒有登錄,請登陸後再操作<br/></span>      <span>沒有收貨地址的提示。。。</span>    </div>    <table class="table" id="goodslist">      <tr>          <td>商品名稱</td>          <td colspan="3" id="goodsName"></td>       </tr>       <tr>          <td>商品圖片</td>          <td colspan="3"><img  id="goodsImg" width="200" height="200" /></td>       </tr>       <tr>          <td>秒殺開始時間</td>          <td id="startTime"></td>          <td >              <input type="hidden" id="remainSeconds" />              <span id="miaoshaTip"></span>          </td>          <td>          <!--              <form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">                  <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒殺</button>                  <input type="hidden" name="goodsId"  id="goodsId" />              </form>-->              <div class="row">                  <div class="form-inline">                      <img id="verifyCodeImg" width="80" height="32"  style="display:none" onclick="refreshVerifyCode()"/>                      <input id="verifyCode"  class="form-control" style="display:none"/>                      <button class="btn btn-primary" type="button" id="buyButton"onclick="getMiaoshaPath()">立即秒殺</button>                  </div>              </div>              <input type="hidden" name="goodsId"  id="goodsId" />          </td>       </tr>       <tr>          <td>商品原價</td>          <td colspan="3" id="goodsPrice"></td>       </tr>        <tr>          <td>秒殺價</td>          <td colspan="3"  id="miaoshaPrice"></td>       </tr>       <tr>          <td>庫存數量</td>          <td colspan="3"  id="stockCount"></td>       </tr>    </table>  </div>

核心js程式碼:

<script>    $(function(){     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 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(){      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("秒殺進行中");          $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());          $("#verifyCodeImg").show();          $("#verifyCode").show();      }else{//秒殺已經結束          $("#buyButton").attr("disabled", true);          $("#miaoshaTip").html("秒殺已經結束");          $("#verifyCodeImg").hide();          $("#verifyCode").hide();      }  }    </script>

3、記得服務端返回json格式數據,給靜態頁面做數據綁定。

三、客戶端渲染方式(秒殺介面)

和第二點的操作基本一樣,也是去除動態模板語言,改為ajax渲染。
不同的地方:
1)、多了一些springboot的配置;
2)、GET和POST的區別,這裡一定要用POST,有一些場景比如刪除操作,如果用了GET比如delete?id=XX這樣的寫法,那麼搜索引擎掃描到會自動幫你刪除了,所以一定要寫清楚類型。

1、springboot-1.5.x的配置

spring.resources.add-mappings=true #是否啟用默認資源處理  spring.resources.cache-period= 3600 #快取時間  spring.resources.chain.cache=true #是否在資源鏈中啟用快取  spring.resources.chain.enabled=true #是否啟用Spring資源處理鏈。默認情況下,禁用,除非至少啟用了一個策略。  spring.resources.chain.gzipped=true #是否對快取壓縮  spring.resources.chain.html-application-cache=true #是否啟用HTML5應用程式快取清單重寫  spring.resources.static-locations=classpath:/static/ #靜態資源的位置

2、springboot2.1.1的官方配置

spring.resources.add-mappings=true # 是否啟用默認資源處理  spring.resources.cache.cachecontrol.cache-private= # 表示響應消息是針對單個用戶的,不能由共享快取存儲。  spring.resources.cache.cachecontrol.cache-public= # 表示任何快取都可以存儲響應  spring.resources.cache.cachecontrol.max-age= # 響應被快取的最大時間,如果沒有指定持續時間後綴,以秒為單位。  spring.resources.cache.cachecontrol.must-revalidate= # 表明,一旦快取過期,在未與伺服器重新驗證之前,快取不能使用響應。  spring.resources.cache.cachecontrol.no-cache= # 表示快取的響應只有在伺服器重新驗證時才能重用  spring.resources.cache.cachecontrol.no-store= # 表示在任何情況下都不快取響應  spring.resources.cache.cachecontrol.no-transform= # 指示中介(快取和其他)它們不應該轉換響應內容  spring.resources.cache.cachecontrol.proxy-revalidate= # 與「must-revalidate」指令的含義相同,只是它不適用於私有快取。  spring.resources.cache.cachecontrol.s-max-age= # 響應被共享快取快取的最大時間,如果沒有指定持續時間後綴,以秒為單位。  spring.resources.cache.cachecontrol.stale-if-error= # 當遇到錯誤時,響應可能使用的最大時間,如果沒有指定持續時間後綴,以秒為單位。  spring.resources.cache.cachecontrol.stale-while-revalidate= # 如果沒有指定持續時間後綴,則響應在過期後可以提供的最長時間(以秒為單位)。  spring.resources.cache.period= # 資源處理程式提供的資源的快取周期。如果沒有指定持續時間後綴,將使用秒。  spring.resources.chain.cache=true # 是否在資源鏈中啟用快取。  spring.resources.chain.compressed=false # 是否啟用已壓縮資源(gzip, brotli)的解析。  spring.resources.chain.enabled= # 是否啟用Spring資源處理鏈。默認情況下,禁用,除非至少啟用了一個策略。  spring.resources.chain.html-application-cache=false # 是否啟用HTML5應用快取清單重寫。  spring.resources.chain.strategy.content.enabled=false # 是否啟用內容版本策略。  spring.resources.chain.strategy.content.paths=/** # 應用於內容版本策略的以逗號分隔的模式列表。  spring.resources.chain.strategy.fixed.enabled=false # 是否啟用固定版本策略。  spring.resources.chain.strategy.fixed.paths=/** # 用於固定版本策略的以逗號分隔的模式列表。  spring.resources.chain.strategy.fixed.version= # 用於固定版本策略的版本字元串。  spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/ # 靜態資源的位置。

3、第二點在點擊<立即秒殺>按鈕時調用的JS方法getMiaoshaPath()如下

<script>    function getMiaoshaPath(){      var goodsId = $("#goodsId").val();      g_showLoading();      $.ajax({          url:"/miaosha/path",          type:"GET",          data:{              goodsId:goodsId,              verifyCode:$("#verifyCode").val()          },          success:function(data){              if(data.code == 0){                  var path = data.data;                  doMiaosha(path);              }else{                  layer.msg(data.msg);              }          },          error:function(){              layer.msg("客戶端請求有誤");          }      });  }    function doMiaosha(path){      $.ajax({          url:"/miaosha/"+path+"/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;                  getMiaoshaResult($("#goodsId").val());              }else{                  layer.msg(data.msg);              }          },          error:function(){              layer.msg("客戶端請求有誤");          }      });    }    function getMiaoshaResult(goodsId){      g_showLoading();      $.ajax({          url:"/miaosha/result",          type:"GET",          data:{              goodsId:$("#goodsId").val(),          },          success:function(data){              if(data.code == 0){                  var result = data.data;                  if(result < 0){                      layer.msg("對不起,秒殺失敗");                  }else if(result == 0){//繼續輪詢                      setTimeout(function(){                          getMiaoshaResult(goodsId);                      }, 200);                  }else{                      layer.confirm("恭喜你,秒殺成功!查看訂單?", {btn:["確定","取消"]},                              function(){                                  window.location.href="/order_detail.htm?orderId="+result;                              },                              function(){                                  layer.closeAll();                              });                  }              }else{                  layer.msg(data.msg);              }          },          error:function(){              layer.msg("客戶端請求有誤");          }      });  }    </script>

四、對象快取

最基本最常用的快取處理邏輯:

  1. 失效:應用程式先從cache取數據,沒有得到,則從資料庫中取數據,成功後,放到快取中。
  2. 命中:應用程式從cache中取數據,取到後返回。
  3. 更新:先把數據存到資料庫中,成功後,再讓快取失效。

    參考程式碼:

// 先查快取,再查資料庫。  public MiaoshaUser getById(long id) {     //取快取     MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);     if(user != null) {        return user;     }     //取資料庫     user = miaoshaUserDao.getById(id);     if(user != null) {        redisService.set(MiaoshaUserKey.getById, ""+id, user);     }     return user;  }    // 更新資料庫後,快取也要做同步更新。  public boolean updatePassword(String token, long id, String formPass) {     //取user     MiaoshaUser user = getById(id);     if(user == null) {        throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);     }     //更新資料庫     MiaoshaUser toBeUpdate = new MiaoshaUser();     toBeUpdate.setId(id);     toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));     miaoshaUserDao.update(toBeUpdate);     //處理快取     redisService.delete(MiaoshaUserKey.getById, ""+id);     user.setPassword(toBeUpdate.getPassword());     redisService.set(MiaoshaUserKey.token, token, user);     return true;  }

總結

  • 服務端渲染:利用模板引擎技術生成靜態html頁面到指定目錄或者是redis等快取中間件中去,頁面上的訪問路徑直接指向到該目錄下的靜態html,這種方式實際上還是屬於服務端渲染的靜態化方式。
  • 客戶端渲染:將原本的模板語言渲染的html,改變為純靜態的htm/shtml,然後用js/ajax渲染數據,加上spring配置文件的相關配置指定靜態文件存放路徑,達到利用瀏覽器快取頁面的方式。推薦使用這種方式,這種屬於前後端分離的客戶端渲染方式,性能更好。
  • 對象快取:對象級快取主要是在service中對一些對象的快取處理,要按照合理的步驟,先取快取,再取資料庫,快取中有就返回快取對象,沒有就返回資料庫數據,最後將資料庫數據再放入快取中。

    如果對快取處理邏輯感興趣,可以參考這篇部落格:http://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/78693323