主頁 > 後端開發 > 商城秒殺系統總結(Java)

商城秒殺系統總結(Java)

2022-03-05 06:12:20 後端開發

本文寫的較為零散,對沒有基礎的同學不太友好,

一、秒殺系統專案總結(基礎版)

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例外攔截:

  1. 如果對Spring程式沒有進行例外處理,則遇到特定的例外會自動映射為指定的HTTP狀態碼,部分如下:
image-20220119235134663

表中的例外一般會由Spring自身拋出,作為DispatcherServlet處理程序中或執行校驗時出現問題的結果,如果DispatcherServlet無法找到適合處理請求的控制器方法,那么將會拋出NoSuchRequestHandlingMethodException例外,最終的結果就是產生404狀態碼的回應(Not Found),

  1. 通過使用@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上的例外型別不能出現一樣的,否則運行時拋例外.

  1. @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性能測驗

image-20220302170432864

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

image-20220302170708900

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

image-20220302171847971

觀測結果,即平均58ms回應,90%的為64ms內回應,99%的為110ms內回應,TPS為2.1,

TPS 即Transactions Per Second的縮寫,每秒處理的事務數目,一個事務是指一個客戶機向服務器發送請求然后服務器做出反應的程序(完整處理,即客戶端發起請求到得到回應),客戶機在發送請求時開始計時,收到服務器回應后結束計時,以此來計算使用的時間和完成的事務個數,最終利用這些資訊作出的評估分,一個事務可能對應多個請求,可以參考下資料庫的事務操作,

在服務器上查看tomcat當前維護的執行緒樹:

image-20220302172638385

可知當前共維護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次,觀察:

image-20220302175908933

可以看到java行程的執行緒數在不斷上升,

而jmeter開始觀察到錯誤請求,

image-20220302175812733

image-20220302175822195

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

image-20220302180152582

接下來修改默認配置:

image-20220302181707281

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

image-20220302181935791

重啟程式,可以看到,最小執行緒數較之前已有較大提升,

之前測驗過高直接導致服務器卡死,重新設定,200執行緒,15秒啟動,回圈50次,

image-20220302184844757

可見比之前幾十個執行緒,已經多了很多,

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容器時,會掃描該定制類,加載設定,

容量問題優化方向

image-20220302192007866

在jmeter壓測程序中,通過top -H命令是可以看到行程占用情況的,可以看到mysql是主要占據記憶體的應用,因為每個請求實際上都是到資料庫進行查詢,

image-20220302192827115

關于資料庫QPS可以參考上圖,

三、分布式擴展

原專案性能壓測:

image-20220302193853956

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提供了無縫平滑重啟(用戶不會感知):

image-20220302233615792

動靜分離服務器

image-20220302235624928

將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只是轉發請求
        } 

效果如下:

image-20220303001426126

請求轉發到了應用服務器上,且回應正確,

通過開啟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次回圈

對代理服務器發送請求:

image-20220303004726612

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

觀察資料庫服務器:

image-20220303005633257

面對這樣的請求,資料庫服務器還是較為輕松,

觀察水平擴展后的應用服務器:(這里JVM的記憶體設定為1G,服務器記憶體為2G)

image-20220303005825990

對比

對于單機進行測驗:

image-20220303005111170

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

image-20220303005307874

目前優化后的系統架構:

image-20220303005947174

優化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多路復用

image-20220303011418494

select和epoll的區別可以理解為:一個需要遍歷查找哪個發生變更,而epoll是不需要的,因此epoll更快,且監聽更多,

master-worker行程模型

image-20220303012114559

master和worker是父子行程,下圖第二行顯示,

image-20220303012253315

因此,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的執行緒是不阻塞的,因此非常快,

協程機制

image-20220303013315430

協程的模型:一個執行緒有多個協程,依附于執行緒,只調記憶體開銷,開銷比較小,
協程程式遇到阻塞,自動將協程權限剝奪,調出不阻塞協程執行,
不需要加鎖,不是執行緒要搶奪鎖資源效率會比較高,

分布式會話

image-20220303013743278
//將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進行,

image-20220304181038548

除了哨兵模式之外,集群cluster模式,沒有集群模式之前:使用分片機制:

image-20220304181624194

客戶端通過哨兵得知有兩臺redis master,通過哈希將資料路由到兩臺redis服務器上,根據哈希對相應redis進行get/set操作,這種分片方式導致資料遷移和客戶端操作比較復雜,

cluster集群模式:

image-20220304182201740

集群中所有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回應協議包內部結構;第二版互聯網通信流程圖

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more