本文寫的較為零散,對沒有基礎的同學不太友好,
一、秒殺系統專案總結(基礎版)
classpath
在.properties中時常需要讀取資源,定位檔案地址時經常用到classpath
類路徑指的是src/main/java,或者是src/main/resource下的路徑,例如:resource 下的 classpath:mapping/*.xml,經常用于Mybatis中配置mapping檔案地址,
Mybatis-generator
在寫專案中可以利用mybatis-generator進行一些機械性作業(在pom中引入),這里將組態檔中的一部分進行展示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!--資料庫鏈接地址賬號密碼-->
<jdbcConnection driver connectionURL="jdbc:mysql://127.0.0.1:3306/庫名" userId="sql_id" password="sql_password">
</jdbcConnection>
<!--生成DataObject類存放位置-->
<javaModelGenerator targetPackage="com.imooc.miaoshaproject.dataobject" targetProject="src/main/java">
<property name="enableSubPackages" value="https://www.cnblogs.com/gaoyuan206/p/true"/>
<property name="trimStrings" value="https://www.cnblogs.com/gaoyuan206/p/true"/>
</javaModelGenerator>
<!--生成映射檔案存放位置-->
<sqlMapGenerator targetPackage="mapping" targetProject="src/main/resources">
<property name="enableSubPackages" value="https://www.cnblogs.com/gaoyuan206/p/true"/>
</sqlMapGenerator>
<!--生成Dao類存放位置-->
<!-- 客戶端代碼,生成易于使用的針對Model物件和XML組態檔 的代碼
type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper物件
type="MIXEDMAPPER",生成基于注解的Java Model 和相應的Mapper物件
type="XMLMAPPER",生成SQLMap XML檔案和獨立的Mapper介面
-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.imooc.miaoshaproject.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="https://www.cnblogs.com/gaoyuan206/p/true"/>
</javaClientGenerator>
<!--生成對應表及類名-->
<!--
<table tableName="user_info" domainObjectName="UserDO" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>
<table tableName="user_password" domainObjectName="UserPasswordDO" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>
-->
<table tableName="promo" domainObjectName="PromoDO" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>
</context>
</generatorConfiguration>
在使用mybatis-generator之后要注意檢查mapping中的檔案,進行適當修改,比如Insert操作中宣告自增和主鍵,
Spring例外攔截:
- 如果對Spring程式沒有進行例外處理,則遇到特定的例外會自動映射為指定的HTTP狀態碼,部分如下:
表中的例外一般會由Spring自身拋出,作為DispatcherServlet處理程序中或執行校驗時出現問題的結果,如果DispatcherServlet無法找到適合處理請求的控制器方法,那么將會拋出NoSuchRequestHandlingMethodException例外,最終的結果就是產生404狀態碼的回應(Not Found),
- 通過使用@ResponseStatus注解能將例外映射為特定的狀態碼:
//定義exceptionhandler解決未被controller層吸收的exception
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Object handlerException(HttpServletRequest request, Exception ex){
Map<String,Object> responseData = https://www.cnblogs.com/gaoyuan206/p/new HashMap<>();
if( ex instanceof BusinessException){
BusinessException businessException = (BusinessException)ex;
responseData.put("errCode",businessException.getErrCode());
responseData.put("errMsg",businessException.getErrMsg());
}else{
responseData.put("errCode", EmBusinessError.UNKNOWN_ERROR.getErrCode());
responseData.put("errMsg",EmBusinessError.UNKNOWN_ERROR.getErrMsg());
}
return CommonReturnType.create(responseData,"fail");
}
這里將回應200(OK)狀態碼,但是大多數時候,我們需要知道這個例外的具體資訊,這就需要如上代碼所示,加上 @ExceptionHandler(Exception.class),一旦捕捉到例外,則按handler流程運行, 如果需要一個contrller具有該例外處理,可以建立一個基類進行繼承,不然需要每個controller都寫一遍,這種方式較為麻煩,
一個Controller下多個@ExceptionHandler上的例外型別不能出現一樣的,否則運行時拋例外.
-
@ControllerAdvice+@ExceptionHandler攔截例外并統一處理
@ExceptionHandler的作用主要在于宣告一個或多個型別的例外,當符合條件的Controller拋出這些例外之后將會對這些例外進行捕獲,然后按照其標注的方法的邏輯進行處理,從而改變回傳的視圖資訊,
@ControllerAdvice public class GlobalExceptionHandler{ @ExceptionHandler(Exception.class) @ResponseBody public CommonReturnType doError(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Exception ex) { ex.printStackTrace(); Map<String,Object> responseData = https://www.cnblogs.com/gaoyuan206/p/new HashMap<>(); if( ex instanceof BusinessException){ BusinessException businessException = (BusinessException)ex; responseData.put("errCode",businessException.getErrCode()); //自定義的例外類 responseData.put("errMsg",businessException.getErrMsg()); }else if(ex instanceof ServletRequestBindingException){ responseData.put("errCode",EmBusinessError.UNKNOWN_ERROR.getErrCode()); responseData.put("errMsg","url系結路由問題"); }else if(ex instanceof NoHandlerFoundException){ responseData.put("errCode",EmBusinessError.UNKNOWN_ERROR.getErrCode()); //自定義的列舉類 responseData.put("errMsg","沒有找到對應的訪問路徑"); }else{ responseData.put("errCode", EmBusinessError.UNKNOWN_ERROR.getErrCode()); responseData.put("errMsg",EmBusinessError.UNKNOWN_ERROR.getErrMsg()); } return CommonReturnType.create(responseData,"fail"); } }這樣,當訪問任何controller的時候,如果在該controller中拋出了Exception,那么理論上這里的例外捕獲器就會捕獲該例外,判斷情況,然后回傳我們定義的例外視圖(默認的error視圖),
在資料庫設計層面需要注意的有:例如商品價格屬性在后臺設定為
BigDecimal,但是mysql中是沒有這個關鍵字的,我們可以在表中設計為double屬性,包括商品的DO物件也為double,但是在商品的model物件中屬性為BigDecimal,需要進行型別轉換,不用double的原因為后端傳送給前端后,可能會出現一些錯誤,例如1.9傳過去之后可能為1.99999...建議將價格等對數位敏感的資料在后臺處理為BigDecimal,
在資料結構設計層面建立了3種資料物件,視圖層中的VO物件,這是為了將用戶需要的資料進行呈現,避免將一些用戶不需要感知的資料進行前后端互動,dao層的DO物件,這是為了和資料庫真正進行互動,Service層的Model物件,這是為了后臺整體邏輯統一,例如用戶的資料和用戶的密碼在本專案中分兩個表存,肯定有兩個DO物件,而在后臺設計時,每次都呼叫兩個DO屬性較為麻煩,直接建立一個用戶的邏輯物件,將用戶相關的所有資料放在一個物件中,方便操作,
基礎知識
前端
在撰寫前端頁面的時候,通常使用一些框架,比如本專案使用的Metronic,之前也稍微用過element-ui這些,一般邏輯為:首先<head> </head>中引入樣式和.js資源,然后在<body> </body>中通過呼叫"class"即可直接完成頁面的美化,在處理動態邏輯的時候,需要用ajax進行click等動作的判定,以及請求的發送,
對于前端我只了解一點點,可能說的不對,不過稍微理解概念后即可在模板上進行修修改改,
Java 8 stream api
在代碼中經常使用.stream()有利于簡化代碼結構,效率高一點,舉例:
//使用stream apiJ將list內的itemModel轉化為ITEMVO;
List<ItemVO> itemVOList = itemModelList.stream().map(itemModel -> {
ItemVO itemVO = this.convertVOFromModel(itemModel);
return itemVO;
}).collect(Collectors.toList());
這一段即為將一個Model結構的list,利用stream api轉成VO結構的list,
MD5加密
資料庫中通常不存明文密碼(防止資料庫資料泄露,密碼被公開),這時候我們需要一種加密方式,大多數采用MD5加密,在Java原生包中 MD5Encoder 只支持16位長度,這樣的話不方便業務實作,
md5是不可逆的,也就是沒有對應的演算法,從生產的md5值逆向得到原始資料,但是如果使用暴力破解,那就另說了,
簡單實作方式:
public String EncodeByMd5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
//確定計算方法
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64en = new BASE64Encoder();
//加密字串
String newstr = base64en.encode(md5.digest(str.getBytes("utf-8")));
return newstr;
}
MD5的幾個特點:
1.長度固定:
不管多長的字串,加密后長度都是一樣長
作用:方便平時資訊的統計和管理
2.易計算:
字串和檔案加密的程序是容易的.
作用: 開發者很容易理解和做出加密工具
3.細微性
一個檔案,不管多大,小到幾k,大到幾G,你只要改變里面某個字符,那么都會導致MD5值改變.
作用:很多軟體和應用在網站提供下載資源,其中包含了對檔案的MD5碼,用戶下載后只需要用工具測一下下載好的檔案,通過對比就知道該檔案是否有過更改變動.
4.不可逆性
你明明知道密文和加密方式,你卻無法反向計算出原密碼.
作用:基于這個特點,很多安全的加密方式都是用到.大大提高了資料的安全性
交易模型
交易模型流程:
//1.校驗下單狀態,下單的商品是否存在,用戶是否合法,購買數量是否正確,校驗活動資訊
//2.落單減庫存(下單時刻即減少庫存,但是如果用戶取消交易需要將庫存還原,適用于后臺備貨比顯示多的情況),還有一種交易減庫存,這是只有當成功交易才會減少庫存,適用于顯示的庫存為真實庫存,會讓用戶有一定的交易緊迫感
//3.訂單入庫,生成交易流水號,訂單號,加上商品的銷量
//4.回傳前端
設計訂單號:(訂單號顯示是具有一定意義的,簡單的自增ID無法滿足需求)
設計訂單號為16位:前8位為時間資訊(年月日)方便在資料庫資料量過大時候,可以洗掉幾個月前的無用訂單資料,中間6位為自增序列,如果每天的訂單量超過6位數,則需要擴增,最后兩位為分庫分表位,區分在哪個庫哪張表,這是訂單號的一個簡單設計,
秒殺環節的簡單思考:
秒殺通常與商品活動掛鉤,因此必然有一個活動開始時間,活動結束時間,以及活動開始倒計時,在增加秒殺活動的程序中,我們就需要對商品模型資料結構進行修改,可以增加一個促銷模型屬性,而促銷模型進行分層設計,設計其service等等,在前端進行一定的頁面修改,顯示時間,顯示促銷價格等等,同時對訂單模型進行修改,增加是否促銷屬性,如果促銷,則訂單入庫時需要以促銷價格入庫,這些地方需要注意,
至于后端訂單介面如何識別是否在活動呢?
//1.通過前端url上傳過來秒殺活動id,然后下單介面內校驗對應id是否屬于對應商品且活動已開始
//2.直接在下單介面內判斷對應的商品是否存在秒殺活動,若存在進行中的則以秒殺價格下單
顯然,使用2的話,在非促銷商品的下單環節會增加不必要的運行,
前端設計:
下單時,將promo_id傳進去
jQuery(document).ready(function(){
$("#createorder").on("click",function(){
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://localhost:8090/order/createorder",
data:{
"itemId":g_itemVO.id,
"amount":1,
"promoId":g_itemVO.promoId
},
xhrFields:{withCredentials:true},
success:function(data){
if(data.status == "success"){
alert("下單成功");
window.location.reload();
}else{
alert("下單失敗,原因為"+data.data.errMsg);
if(data.data.errCode == 20003){
window.location.href="https://www.cnblogs.com/gaoyuan206/p/login.html";
}
}
},
error:function(data){
alert("下單失敗,原因為"+data.responseText);
}
});
});
后臺下單:
//封裝下單請求
@RequestMapping(value = "https://www.cnblogs.com/createorder",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType createOrder(@RequestParam(name="itemId")Integer itemId,
@RequestParam(name="amount")Integer amount,
@RequestParam(name="promoId",required = false)Integer promoId) throws BusinessException {
Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
if(isLogin == null || !isLogin.booleanValue()){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用戶還未登陸,不能下單");
}
//獲取用戶的登陸資訊
UserModel userModel = (UserModel)httpServletRequest.getSession().getAttribute("LOGIN_USER");
OrderModel orderModel = orderService.createOrder(userModel.getId(),itemId,promoId,amount);
return CommonReturnType.create(null);
}
部署
本人是直接利用寶塔linux面板進行環境部署,在運行專案是采用外掛配置:
nohup java -jar "目標jar" --spring.config.additon-location=/外掛配置地址
//nohup可掛在后臺運行jar包
并且外掛配置優先級高于默認配置
二、JMETER性能測驗

JMETER實際上就是在本地開一個執行緒組,自己規定執行緒組的規模,向服務器發出HTTP請求,進行性能壓測,一般需要配置HTTP請求,查看結果樹,聚合報告這三項,

這是一個GET請求的示例,設定20個執行緒,ramp-up時間設為10秒,即jmeter用10秒啟動20個執行緒并運行,(改動了執行緒組的設定)

觀測結果,即平均58ms回應,90%的為64ms內回應,99%的為110ms內回應,TPS為2.1,
TPS 即Transactions Per Second的縮寫,每秒處理的事務數目,一個事務是指一個客戶機向服務器發送請求然后服務器做出反應的程序(完整處理,即客戶端發起請求到得到回應),客戶機在發送請求時開始計時,收到服務器回應后結束計時,以此來計算使用的時間和完成的事務個數,最終利用這些資訊作出的評估分,一個事務可能對應多個請求,可以參考下資料庫的事務操作,
在服務器上查看tomcat當前維護的執行緒樹:

可知當前共維護28個執行緒,1422為java運行埠,
因為測驗服務器是單核2G記憶體,當測驗5000個執行緒,10秒開啟,回圈10次時,就會出現大量錯誤請求,
內嵌tomcat配置
SpringBoot內嵌了tomcat容器,配置如下(部分):
{
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties",
"defaultValue": 8080, //tomcat埠設定
"name": "server.port",
"description": "Server HTTP port.",
"type": "java.lang.Integer"
},
{
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat",
"defaultValue": 100, //tomcat執行緒池佇列超過100后,請求將被拒絕
"name": "server.tomcat.accept-count",
"description": "Maximum queue length for incoming connection requests when all possible request processing threads are in use.",
"type": "java.lang.Integer"
},
{
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat",
"defaultValue": 10, //執行緒池的最小執行緒數量,可以理解為corePoolSize
"name": "server.tomcat.min-spare-threads",
"description": "Minimum number of worker threads.",
"type": "java.lang.Integer"
},
{
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat",
"defaultValue": 10000, //tomcat支持最大連接數
"name": "server.tomcat.max-connections",
"description": "Maximum number of connections that the server accepts and processes at any given time. Once the limit has been reached, the operating system may still accept connections based on the \"acceptCount\" property.",
"type": "java.lang.Integer"
},
{
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat",
"defaultValue": 200, //tomcat支持最大執行緒數,可認為maximumPoolSize
"name": "server.tomcat.max-threads",
"description": "Maximum number of worker threads.",
"type": "java.lang.Integer"
},
測驗4000個執行緒,15秒內啟動,回圈100次,觀察:

可以看到java行程的執行緒數在不斷上升,
而jmeter開始觀察到錯誤請求,


關于SpringBoot中內嵌tomcat默認配置如下:

接下來修改默認配置:

一般經驗上,在4核8G的服務器上,最大執行緒數可設為800,但是本服務器為單核2G,暫設為200,

重啟程式,可以看到,最小執行緒數較之前已有較大提升,
之前測驗過高直接導致服務器卡死,重新設定,200執行緒,15秒啟動,回圈50次,

可見比之前幾十個執行緒,已經多了很多,
keep-alive設定
關于keepalive,如何設定連接斷開時間或者該請求訪問多少次之后斷開連接,在內嵌tomcat的配置json中是沒有的,這時候需要更改代碼:
增加config package:
//當Spring容器內沒有TomcatEmbeddedServletContainerFactory這個bean時,會吧此bean加載進spring容器中
@Component
public class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory configurableWebServerFactory) {
//使用對應工廠類提供給我們的介面定制化我們的tomcat connector
((TomcatServletWebServerFactory)configurableWebServerFactory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
@Override
public void customize(Connector connector) {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
//定制化keepalivetimeout,設定30秒內沒有請求則服務端自動斷開keepalive鏈接
protocol.setKeepAliveTimeout(30000);
//當客戶端發送超過10000個請求則自動斷開keepalive鏈接
protocol.setMaxKeepAliveRequests(10000);
}
});
}
}
這樣配置之后當springboot加載tomcat容器時,會掃描該定制類,加載設定,
容量問題優化方向
在jmeter壓測程序中,通過top -H命令是可以看到行程占用情況的,可以看到mysql是主要占據記憶體的應用,因為每個請求實際上都是到資料庫進行查詢,
關于資料庫QPS可以參考上圖,
三、分布式擴展
原專案性能壓測:

TPS在200左右,接下來考慮優化:通過nginx反向代理負載均衡進行水平擴展,
思路為:一臺nginx代理服務器,兩臺java程式運行服務器,一臺mysql服務器,
首先在資料庫服務器開放遠程埠:
需要開放權限,本文是進行內網訪問,可參考該篇博客:https://blog.csdn.net/zhazhagu/article/details/81064406
nginx
作為web服務器
Nginx架構,通過修改nginx.conf來實作這個架構
location /resources/ {
alias /usr/local/openresty/nginx/html/resources/;
index index.html index.htm;
}
表明當訪問路徑命中了/resources之后,就把/resources/替換成
/usr/local/openresty/nginx/html/resources/
并將 /resources/后面的html資源拼接在后面
將所有的前端檔案和static檔案都移動到resources檔案夾中
因為修改了組態檔,所以要重啟nginx,nginx提供了無縫平滑重啟(用戶不會感知):
動靜分離服務器
將conf/nginx.conf進行配置:
upstream backend_server{
server 172.27.65.183 weight=1;
server 172.16.162.179 weight=1; #兩個應用服務器,權重均為1,則為輪詢方式進行訪問
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location /resources/ {
alias /usr/local/openresty/nginx/html/resources/;
index index.html index.htm;
}
#新增
location / {
proxy_pass http://backend_server; #當訪問/路徑時,將反向代理到backend_server上
proxy_set_header Host $http_host:$proxy_port; #host和port進行拼接,發送到應用服務器
proxy_set_header X-Real-IP $remote_addr; #真正的ip地址是遠端的地址,否則將會拿到nginx服務器的地址
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #設定這個頭表明nginx只是轉發請求
}
效果如下:

請求轉發到了應用服務器上,且回應正確,
通過開啟tomcat access_log進行觀察請求是否進入應用服務器:
通過修改專案application.properties
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.directory=/www/SpringBoot/tomcat
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D
# %h遠端host %l通常為- %u用戶 %t請求時間 %r對應的HTTP請求的第一行,請求的URL等資訊 %s回傳狀態碼 %b請求回傳大小(位元組) %D處理請求的時長(毫秒)
日志輸出如下:
172.27.65.182 - - [03/Mar/2022:00:26:19 +0800] "GET /item/get?id=6 HTTP/1.0" 200 303 1156
注意因為nginx代理給兩個應用服務器,所以沒重繪兩次頁面,才有一個請求被分配給這個列印日志的服務器,
目前負載均衡策略:請求以輪詢方式分給兩臺應用服務器,
JMETER性能壓測
代理服務器帶寬為3M,應用服務器帶寬為1M,資料庫服務器帶寬為1M,
測驗引數設定:700執行緒,10秒內啟動,30次回圈
對代理服務器發送請求:

可以看到TPS已經上升到了490左右,峰值600左右,由于執行緒開啟過多的話,TOP工具將會非常卡,所以對更高引數不作測驗,
觀察資料庫服務器:

面對這樣的請求,資料庫服務器還是較為輕松,
觀察水平擴展后的應用服務器:(這里JVM的記憶體設定為1G,服務器記憶體為2G)

對比
對于單機進行測驗:

從top工具可知,單個服務器負載面對同樣的情況非常高,已經開始拒絕請求,可見,水平擴展的效果是比較好的,

目前優化后的系統架構:
優化nginx服務器
目前nginx服務器與兩臺應用服務器不是長連接,需要從nginx.conf中進行設定,
#更改兩處
upstream backend_server{
server 172.27.65.183 weight=1;
server 172.16.162.179 weight=1;
keepalive 30;
}
location / {
proxy_pass http://backend_server;
proxy_set_header Host $http_host:$proxy_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1; #修改header
proxy_set_header Connection ""; #將Connection欄位置空,Connection為空就使用KeepAlive
}
配置了之后,Nginx和應用服務器之間就不會有頻繁的建立釋放連接的程序.訪問平均回應時間會快很多
這樣處理之后,處理TIME_WAIT狀態的行程數就會少很多,
nginx高性能的原因
epoll多路復用
select和epoll的區別可以理解為:一個需要遍歷查找哪個發生變更,而epoll是不需要的,因此epoll更快,且監聽更多,
master-worker行程模型
master和worker是父子行程,下圖第二行顯示,

因此,master行程可以管理worker行程,worker行程為真正連接客戶端的行程,client發送socket連接請求時(TCP),master并不會進行accept處理,而是發送信號給worker進行accept動作,本質上是多個worker去搶占鎖,搶到的進行accept連接,后續send和recv均由連接的worker負責,
nginx平滑重啟的原因是什么呢?
不論是worker掛了,還是管理員發出重啟命令,master是不能掛的,對應的master行程會將死亡的worker行程所有的socket句柄交給master管理,這是master會Load所有的組態檔去new一個新的worker,并將所有句柄交給他,
每個worker中只有一個執行緒,這些執行緒基于epoll模型,理論上worker的執行緒是不阻塞的,因此非常快,
協程機制
協程的模型:一個執行緒有多個協程,依附于執行緒,只調記憶體開銷,開銷比較小,
協程程式遇到阻塞,自動將協程權限剝奪,調出不阻塞協程執行,
不需要加鎖,不是執行緒要搶奪鎖資源效率會比較高,
分布式會話
//將OTP驗證碼同對應用戶的手機號關聯,使用httpsession的方式系結他的手機號與OTPCODE
httpServletRequest.getSession().setAttribute(telphone,otpCode);
//在驗證之后,將成功標識加入session中作為登錄憑證
第一種方式,之前的方式只適用于單體應用,因為session_id存盤于spring內嵌的tomcat容器中,如果有多臺服務器,攜帶的session_id只能對應其中一臺應用服務器的登陸憑證,
將session存盤在redis服務器上
第一種方式在分布式應用上的實作,需要遷移到redis上,
引入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
新建類:設定Redis的session過期時間為3600秒-一小時
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600) //將httpsession放入redis內
public class RedisConfigure {
}
本地windows安裝redis.下載zip包解壓即可:redis-server.exe redis.windows.conf
redis-cli.exe -h 127.0.0.1 -p 6379啟動redis
在IDEA配置:redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=10
#spring.redis.password=
#設定jedis連接池
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.min-idle=20
那么現在session資訊的存盤就是默認存盤在Redis上
但是存盤在Redis上的物件要可序列化,實作Serizaliable介面(也可以不實作,修改redis的序列化方式,這里介紹序列化方式,直接在需要存在redis上的資料結構上implements Serializable,使用java默認的序列化方式)
而redis需要部署在資料庫服務器上,因為假如分別部署到兩個應用服務器上,各自存各自的登錄憑證,和之前的cookie存盤session是一樣的,并不能實作分布式會話登錄,
注意修改資料庫服務器上redis的組態檔,系結本機內網地址,(4臺服務器內網相連),修改jar包組態檔,
#配置springboot對redis的依賴
spring.redis.host=127.0.0.1 #這里為redis服務器內網地址
spring.redis.port=6379
spring.redis.database=10
#spring.redis.password= #默認是沒有密碼的
基于token實作分布式會話
修改usercontroller中的/login
//用戶登陸服務,用來校驗用戶登陸是否合法
UserModel userModel = userService.validateLogin(telphone,this.EncodeByMd5(password));
//將登陸憑證加入到用戶登陸成功的session內
//修改成若用戶登錄驗證成功后將對應的登錄資訊和登錄憑證一起存入redis中
//生成登錄憑證token,UUID
String uuidToken = UUID.randomUUID().toString();
uuidToken = uuidToken.replace("-","");
//建議token和用戶登陸態之間的聯系
redisTemplate.opsForValue().set(uuidToken,userModel); //通過RedisTempate可以操作springboot中內嵌的redis的bean
redisTemplate.expire(uuidToken,1, TimeUnit.HOURS);
// this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true);
// this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel);
//下發了token
return CommonReturnType.create(uuidToken);
修改ordercontroller中的下單介面,
String token = httpServletRequest.getParameterMap().get("token")[0];
if(StringUtils.isEmpty(token)){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用戶還未登陸,不能下單");
}
//獲取用戶的登陸資訊
UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
if(userModel == null){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用戶還未登陸,不能下單");
}
將相關介面修改之后即可實作分布式會話,
四、查詢性能優化
快取設計:1.用快速存取設備,用記憶體處理 2.將快取推到離用戶最近的地方 3.臟快取清理
多級快取的幾個策略:
1.redis快取
2.JVM本地快取
3.Nginx Proxy Cache
4.Nginx lua快取
現在使用的是單機版的redis,弊端是redis容量問題,單點故障問題,除了單機模式,可以有sentianal的哨兵模式:
連接哪個redis全部由sentinal決定,下圖sentinal通過心跳機制監測兩臺redis服務器,假設redis1掛掉,則啟用redis2,redis2成為master,redis1成為slave,并通知jar發生了改變,get/set操作通過訪問redis2進行,
除了哨兵模式之外,集群cluster模式,沒有集群模式之前:使用分片機制:
客戶端通過哨兵得知有兩臺redis master,通過哈希將資料路由到兩臺redis服務器上,根據哈希對相應redis進行get/set操作,這種分片方式導致資料遷移和客戶端操作比較復雜,
cluster集群模式:
集群中所有redis都有所有集群成員的關系表,客戶端連接任意一個redis即可,假設redis1-4,4臺服務器,其中redis3掛掉,則redis集群進行rehash保持資料同步以及資料分塊,客戶端自己會維護一個路由表,當redis集群發生改變,第一時間,客戶端的路由表并未變化,所以會按照原來的方式進行訪問,假如說訪問redis2,這時候redis2會回傳一個reask更新客戶端中的路由表,
Jedis已經集成了這三種模式的管理,
快取商品詳情頁接入
將商品資訊首先在快取中查詢,如果查詢不到則進入資料庫,
//商品詳情頁瀏覽
@RequestMapping(value = "https://www.cnblogs.com/get",method = {RequestMethod.GET})
@ResponseBody
public CommonReturnType getItem(@RequestParam(name = "id")Integer id){
ItemModel itemModel = null;
//先取本地快取
itemModel = (ItemModel) cacheService.getFromCommonCache("item_"+id);
if(itemModel == null){
//根據商品的id到redis內獲取
itemModel = (ItemModel) redisTemplate.opsForValue().get("item_"+id);
//若redis內不存在對應的itemModel,則訪問下游service
if(itemModel == null){
itemModel = itemService.getItemById(id);
//設定itemModel到redis內
redisTemplate.opsForValue().set("item_"+id,itemModel);
redisTemplate.expire("item_"+id,10, TimeUnit.MINUTES); //設定過期時間
}
//填充本地快取
cacheService.setCommonCache("item_"+id,itemModel); //本地熱點資料快取,存在JVM中
}
ItemVO itemVO = convertVOFromModel(itemModel);
return CommonReturnType.create(itemVO);
}
注意:這里的itemModel因為沒有對應序列化方式,程式會報錯,需要對itemModel和promoModel(item中包含)進行序列化,注意默認使用java序列化,redis中存盤的key-value直接查詢將是一組亂碼,
為了在redis中查詢的更方便直接,對redisTemplate進行配置:
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//首先解決key的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
//解決value的序列化方式
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule(); //對序列化作定制
simpleModule.addSerializer(DateTime.class,new JodaDateTimeJsonSerializer());
simpleModule.addDeserializer(DateTime.class,new JodaDateTimeJsonDeserializer());
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);//需要加上這行配置在redis中加入類資訊,不然無法反序列化
objectMapper.registerModule(simpleModule);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
注意,itemmodel中含有DateTime屬性(jodatime),因此需要單獨對此序列化,因為redis默認對datetime的解讀不友好,舉例如下:
public class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> {
@Override
public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
}
}
本地熱點資料快取(JVM記憶體)
滿足:1.熱點資料 2.臟讀非常不敏感 3.記憶體可控
實際上是實作一個滿足并發讀寫的HashMap結構,存盤key-value在應用服務器上即可,但是快取資料還需要設定失效時間,可利用Guava cache(可控制大小和超時時間,可配置LRU策略,執行緒安全),
相應實作類:
@Service
public class CacheServiceImpl implements CacheService {
private Cache<String,Object> commonCache = null;
@PostConstruct
public void init(){
commonCache = CacheBuilder.newBuilder()
//設定快取容器的初始容量為10
.initialCapacity(10)
//設定快取中最大可以存盤100個KEY,超過100個之后會按照LRU的策略移除快取項
.maximumSize(100)
//設定寫快取后多少秒過期
.expireAfterWrite(60, TimeUnit.SECONDS).build();
}
@Override
public void setCommonCache(String key, Object value) {
commonCache.put(key,value);
}
@Override
public Object getFromCommonCache(String key) {
return commonCache.getIfPresent(key);
}
}
Nginx Proxy Cache快取(拓展)
在nginx.conf中兩個地方配置:
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;
//tmp_cache快取存放檔案夾,levels=1:2分子目錄,tmp_cache記憶體空間:100兆大小,過期時間7天,最大大小10g
proxy_cache tmp_cache;
proxy_cache_key $uri; //使用傳遞進來的uri作為key
proxy_cache_valid 200 206 304 302 7d;
但是,nginx的快取是存在檔案磁盤中,io會限制快取速度,所以這種方式較少使用
本文只對高并發性能優化作出以上方向的擴展,實際上還有很多種技術可以利用:靜態資源CDN引入,對于交易模塊的優化還有進行,這都是可以繼續提高的一部分,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/437875.html
標籤:Java
上一篇:【JavaWeb】互聯網通信流程 --- 互聯網通信模型;B/S 通信模型;共享資源檔案;開發人員在互聯網通信流程中擔負的職責
下一篇:【JavaWeb】Http網路協議包 --- 網路協議包概述;Http請求協議包內部空間;Http回應協議包內部結構;第二版互聯網通信流程圖
