谷粒商城所學知識點整理總結


環境搭建

Mysql

docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7

配置utf-8編碼/mydata/conf/my.cnf(新建),重啟生效

[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve

進入容器:docker exec -it mysql /bin/bash

 

Redis

先創建對應的文件,然后執行代碼,不然會將 redis.conf 創建為一個目錄

1、mkdir -p /mydata/redis/conf
2、touch /mydata/redis/conf/redis.conf

3、

docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \
-v/mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf

4、開啟redis 持久化:vi /mydata/redis/conf/redis.conf

添加 appendonly yes

重啟生效

進入容器:docker exec -it redis redis-cli

 

ES

1、mkdir -p /mydata/elasticsearch/config

2、mkdir -p /mydata/elasticsearch/data

3、設置該配置文件可以被遠程任何機器訪問:echo "http.host: 0.0.0.0" >>/mydata/elasticsearch/config/elasticsearch.yml

4、授予所有權限:chmod -R 777 /mydata/elasticsearch/

5、創建容器並掛載

docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e  "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v  /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2 

# 設置開機啟動elasticsearch

docker update elasticsearch --restart=always

配置IK分詞器

1、前往ik分詞器網站https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v7.8.1,找到es對應版本的ik分詞器,下載。再通過 xftp 將ik 分詞器解壓包添加到掛載目錄plugin下

2、前往es里,docker exec -it id /bin/bash ,執行

elasticsearch-plugin  list,如果出現ik說明安裝成功。

 

配置ik分詞

1、進入 ik 目錄下的config目錄,vi IKAnalyzer.cfg.xml,打開自定義的分詞庫設置,設置自定義分詞庫地址

2、保存退出,重啟nginx。

3、登陸 kibana,使用自定義分詞作為語句來測試,查看結果

 

Nginx

1、docker run -p80:80 --name nginx -d nginx:1.10  

2、創建目錄:

mkdir -p /mydata/nginx/html

mkdir -p /mydata/nginx/logs

mkdir -p /mydata/nginx/conf

3、拷貝配置文件到掛載目錄:

docker container cp nginx:/etc/nginx /mydata/nginx/conf

4、因為會多出一個目錄,所以需要對目錄進行調整

1)mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/

2)rm -rf /mydata/nginx/conf/nginx

5、停止運行中的nginx,然后刪除

Docker stop nginx

Docker rm nginx

6、重新創建一個nginx:

docker run -p 80:80 --name nginx \
 -v /mydata/nginx/html:/usr/share/nginx/html \
 -v /mydata/nginx/logs:/var/log/nginx \
 -v /mydata/nginx/conf/:/etc/nginx \
 -d nginx:1.10

配置文件動靜分離和反向代理:

 

Kibana

1、創建容器

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601 -d kibana:7.4.2

2、開機啟動:docker update kibana  --restart=always

 

RabbitMQ

docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

 

阿里雲免密登陸

 

Cloud與AlibabaCloud組件、SpringBoot版本關系

見 版本說明

 

預檢請求與跨域解決

預檢請求:對那些可能對服務器產生副作用的 HTTP 請求方法,瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求,從而獲知服務器端是否允許該跨域請求。

跨域:是瀏覽器對 JavaScript 施加的安全限制,使瀏覽器不能執行其他網站的腳本。

 

解決:

1、使用Nginx反向代理請求來解決

2、配置跨域放行。如在Controller 上添加@CrossOrigin注解。或者配置放行請求

@Configuration
public class MyCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();

        //1.配置跨域
        corsConfiguration.addAllowedHeader("*");        //
        corsConfiguration.addAllowedMethod("*");        //請求方式
        corsConfiguration.addAllowedOrigin("*");        //請求來源
        corsConfiguration.setAllowCredentials(true);    //是否允許攜帶cookie

        source.registerCorsConfiguration("/**", corsConfiguration);

        return new CorsWebFilter(source);
    }
}

 

配置中心

1、系統會默認讀取nacos配置文件中的名字為“當前應用名.properties”的文件

2、在Controller層上添加@RefreshScope注解,實現nacos配置中心修改動態刷新。

具體配置:

1、配置中心可以根據模塊來讀取不同的配置文件,此時就可以指定指定的命名空間了,配置就會讀取指定的命名空間下的所有配置文件(也會讀取指定命名空間下默認會讀取的配置文件)。指定命名空間的參數是spring.cloud.nacos.config.namespace=命名空間id,默認是public

2、在命名空間下還可以指定分組名,來讀取指定命令空間下指定分組的所有配置文件(也會讀取指定命名空間下默認會讀取的配置文件)。spring.cloud.nacos.config.group=分組名

3、當配置文件較多時,還可以將不同的配置划分為各自的文件,然后指定讀取相應的文件,此時可以不再指定分組名(也會讀取指定命名空間下默認會讀取的配置文件)

4、對於以上三種,,如果沒有指定分組會讀取default group下的默認命名配置文件,如果沒有指定命名空間,那么會從public下讀取相應的默認命名配置文件。

 

本地和nacos服務器上的優先使用服務器上的配置

 

 

知識點

遠程調用

feign接口的方法名和URL必須和調用接口的對應上。而參數類型可以不是同一個Class類,只要屬性能夠對上就可以。

Feign遠程調用會丟失請求頭

如 Cookie,如果調用的模塊有 Cookie 信息的驗證,那么這條請求就會被攔截,此時可以使用裝飾者模式添加配置,在 feign 請求生成時將之前請求攜帶的 Cookie 添加到 feign 請求中。

@Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 1、獲取開始請求
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if(requestAttributes != null){
                    HttpServletRequest request = requestAttributes.getRequest();
                    // 2、將開始的請求頭Cookie數據設置到新請求中去
                    String cookie = request.getHeader("Cookie");
                    requestTemplate.header("Cookie",cookie);
                }
            }
        };
    }

異步編程丟失請求頭

在異步線程創建新的線程請求時,也會丟失之前請求攜帶的請求頭數據,解決原理和 feign 一樣。

在不同的異步線程中都可以使用RequestContextHolder.setRequestAttributes,原理是RequestContextHolder內部的 RequestAttributes 是使用ThreadLocal存儲的。

 

JSR303數據校驗

常用注解:

JSR303驗證注解

@NULL,是否為null

@NotNull,是否不為null

@NotBlank,是否不為null且長度至少為1(trim()之后不能為0)

@NotEmpty,是否不為null並且長度不為0,不能支持數字類型的

@Min,大於指定的值

@Pattern,正則表達式,用於String類型

@Max,小於指定的值

@Past,日期必須早於現在的時間

@Url,必須為URL格式

@Range,指定min與max之間的數

 

1、屬性上加判斷注解,可以指定注解返回的異常信息內容

2、請求的Controller層的方法參數添加@Valid注解,可以在方法參數后面加一個BindingResult result參數,可以獲得校驗的結果,然后在Controller層里編寫異常處理的代碼

 

優化1:統一異常處理類:將Controller層的異常代碼統一管理,先自定義異常處理類,然后在類上標注

@RestControllerAdvice(basePackages="com.gulimall.product.controller")注解,然后該方法就可以處理指定包下的所有方法異常

優化2:類中編寫方法進行處理,方法上@ExceptionHandler(value=)來指定處理的異常類型。

優化3:可以在公共類中定義枚舉來包含異常的code與信息內容,然后來調用

@RestControllerAdvice(basePackages = "com.gulimall.product.app")
public class GulimallExceptionControllerAdvice {

    // JSR303數據校驗異常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handlerException(MethodArgumentNotValidException e) {
        log.error("數據校驗出現問題{},異常類型:{}", e.getMessage(), e.getClass());
        e.printStackTrace();
        BindingResult bindResult = e.getBindingResult();

        Map<String, String> resultMap = new HashMap<>();
        bindResult.getFieldErrors().forEach((fieldError) -> {
            resultMap.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
//        return R.error(400,"數據校驗出現問題").put("data",resultMap);
        return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(), BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data", resultMap);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handlerException(Throwable throwable) {
        throwable.printStackTrace();
        return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
    }
}

優化4:分組。要先創建幾個代表分組的接口,在entity的異常注解中設置group來讓不同的方法啟用不同的注解異常@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})

,然后再修改@Valid注解為@Validated({AddGroup.class})指定分組

注意:使用@Validated({})指定了分組 那么屬性上的校驗注解要生效就必須指定分組且分組必須為這里@Validated({})里面存在的分組,不然校驗不起作用

//      但是如果只使用@Validated(),沒有指定組名,那么指定組名的校驗注解就不會生效,沒有指定組名的就會生效。

 

Mybatis-Plus 實體類相關

邏輯刪除

1、

mybatis-plus:
  global-config:
    db-config:
      id-type: auto     # 主鍵自增
      logic-delete-value: 1 #邏輯刪除值
      logic-not-delete-value: 0 #邏輯未刪除值

2、在屬性上添加@TableLogic(value = "1", delval = "0")注解,如果括號里沒有配置就使用配置文件配置的,如果配置了就使用括號里的配置。

屬性為空時不顯示此屬性

 

 主鍵為自己輸入,非自增

使用@TableId默認是自增的,這時候自己設置ID插入時會拋異常,可以指定為自己輸入。@TableId(type = IdType.INPUT)

時間屬性設置為自動填充

1、添加注解

2、添加配置類

 

域名原理

1、當在網址輸入域名時,會首先在本地 hosts文件查看有沒有對應域名設置,如果有,直接就去訪問域名關聯的ip地址。

2、如果本地沒有配置,那么就去本地配置DNS 地址上尋找域名配置,DNS相當於公共的 hosts 配置,找到后再訪問 ip 地址

 

OSS對象存儲

在后台配置好公匙和密匙,以及OSS的域名,使用官網提供的方法編寫返回返回的 Policy方法,前端通過后端返回的 Policy 直接訪問OSS服務器存儲對象數據。

 

異步線程

使用

1、創建異步線程。

2、執行完添加回調方法。

whenComplete 回調同步方法

whenCompleteAsync 回調方法交給線程池執行(一般是異步,也有可能是會再次交給同一個線程執行)

exceptionally:捕獲返回值和異常,並對返回值進行修改。(在異常發生時才會觸發)

handle:捕獲返回值異常,並對返回值進行修改(是否發生異常都會觸發)

 

 3、線程串行化

thenRun系列的,在上一個線程執行完后直接執行,不需要獲取其返回值。

thenAccept系列,在上一個線程執行完后獲取其返回值執行,但是和thenRun系列一樣,執行完后不會有返回值。

thenApply系列的,是在上一個線程執行完,得到其返回值來執行,執行完后再將這個方法返回值返回出供下一個線程繼續使用。

 

其他的帶Async就是交給線程池執行,否則就是跟隨上一個線程同步執行。

 

 

 4、兩任務同時執行完再繼續執行

 

 

和上面的規律一樣,

runAfterBoth 對應 thenRun,兩個任務執行完立刻執行,

thenAcceptBoth 對應 thenAccept,兩個任務執行完獲取其結果,再執行

thenCombine 對應 thenApply,兩個任務執行完獲取其結果再執行,最后會有返回值

 

 

 

 5、兩任務單個完成就繼續執行

 

 

 

 6、多任務全部執行完 / 執行完一個就繼續執行

 

配置

1、編寫配置屬性的配置類,並將其加入容器

2、這樣就可以直接在配置文件中定義配置的屬性

3、編寫線程池的配置,由於上面將配置屬性的配置類加入了容器,所以可以直接獲取

 

 這個可以對比SpringCache 的配置,那里CacheProperties 配置類默認沒有加入容器,所以需要額外聲明,這里則不需要。

 

使用案例

 

密碼加密

密碼加密是為了防止數據庫外泄導致密碼泄漏。常用的加密算法是 MD5,其特點是

1、長度一致。不管原密碼長度多少,加密后的長度都是一樣的。

2、不可逆。不支持將加密后的密碼轉回加密前的原密碼。

3、易計算。計算效率高。

但是也存在缺點,會被暴力破解。所以 SpringSecurity 在使用 BCryptPasswordEncoder 編寫加密規則時,引入了加鹽概念。也就是規定一個數據(鹽值)和傳來的密碼按規則進行拼接,這樣在破解時由於不知道這個鹽值和拼接規則,破解的難度就會加大。具體實現是通過方法生成一個隨機的鹽值,然后通過 hash 加密將這個鹽值與原密碼進行加密(會對鹽值和原密碼再進行處理),生成最終加密的密碼。在驗證密碼時,由於加密過程是不可逆的,所以驗證時需要將原密碼進行加密與數據庫的密碼進行比較,如果相同通過。

加密:encode

驗證:matches

每次調用加密方法生成的密碼因為每次都使用不同的鹽值而是不同的,而使用 matches 可以驗證的原理就是在加密后的密碼中保存了鹽值,同一個鹽值加上原密碼就可以推出最終密碼,從而驗證密碼是否一致

 

OAuth2.0

OAuth2.0 是一種廣泛用於社交登陸的安全、開發的用戶資源授權協議。任何服務提供商都可以實現自身的OAuth協議,來兼容不同的平台、語言。社交登陸過程用到的服務器主要分為授權服務器和資源服務器,授權服務器用於驗證用戶信息,通過返回訪問令牌,然后就可以通過訪問令牌去資源服務器訪問相應的信息資源。

令牌種類:

1、授權碼模式:最常用的,也是最安全的一種,首先通過第三方應用驗證登陸,授權服務器返回 code,再通過 code 搭配 app_id、app_secret 來從授權服務器獲取授權令牌,最終通過授權令牌訪問資源服務器。優點是訪問令牌token存儲在服務器上,不易被截取,同時code是一次性,使用后失效,更安全。同時支持更新 token,避免了 token 過期問題。應用於第三方平台的登陸。

2、簡化模式:省去了獲取code 的步驟,直接從認證服務器獲取 token,是基於瀏覽器的應用,所以容易被截取 Token,這點可以通過減小 Token 的存活時間解決,但是不支持刷新 token,所以 token 過期后又無法使用。優點就是過程簡單。

3、密碼模式:與簡化模式大致一樣,只不過是直接輸入用戶名、密碼訪問認證服務器來獲取 token,因為是直接輸入密碼,所以需要認證的平台和當前平台有高度的信任。這種一般用於一家企業的兩個產品平台上。

4、客戶端憑證模式:在一次驗證通過后,返回的 token 無平台限制,可以訪問任意平台的資源。這種模式安全隱患也是最大的。

微博開通使用的就是授權碼模式:

先在微博開放平台提交進行身份認證,然后添加應用,填寫必要的消息,以及高級消息的授權回調頁、取消回調頁。然后根據文檔中 OAuth2.0 中找到 “Web網站的授權”,將URL添加到點擊微博圖標后跳轉的URL,client_id 為基本信息的 App Key,redirect_uri 為填寫登陸通過后跳轉的URL。

 

 

Session數據共享

在默認情況下,集群項目中,一個服務器保存的Session數據,另一個服務器是訪問不到的,但是可以通過一些方法來實現。

Session復制(不推薦)

優點:配置簡單。

缺點:在數據量大或者集群服務器多時影響帶寬、降低系統性能。同時由於每台服務器都需要存儲所有的session數據,會浪費大量空間。

 

客戶端存儲(不推薦)

將session 存儲在 Cookie 里。

優點:節省服務器空間。

缺點:Cookie不安全,可能會造成數據泄漏。Cookie 長度限制,數據量太大會發不出去。每次發送都會攜帶數據,浪費帶寬。

 

Hash一致性

同一個會話發送到同一個服務器中。可以使用 nginx 使用 ip_hash 的負載均衡策略。

優點:配置簡單,不會浪費帶寬、空間資源

缺點:服務器重啟后 session 可能會丟失。當集群拓展時 session 會重新分布,這樣也會導致一些用戶丟失原本的 session 數據。

 

將數據存入中間件(推薦)

如 Redis。當前項目就是使用 Redis 來存儲 session 數據,具體就是使用 SpringSession 來實現的。並通過配置增加了Cookie 作用的域名范圍。

@Configuration
public class SessionConfig {


    @Bean
    public CookieSerializer cookieSerializer() {

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

        //放大作用域
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        cookieSerializer.setSameSite(null);
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

缺點:由於是通過 Cookie 實現的,所以 Cookie 作用的范圍也是這一因素的限制條件,最多只能在同一個域名下作用。如果想要實現不同域名間的訪問,可以通過單點登陸。

 

購物車

在購物車模塊添加一個攔截器,在請求進來時根據是否登陸來創建一個當前用戶(臨時用戶)購物車信息對象,將其存入 ThreadLocal 以及 Redis 中(Redis 中的 key 就是 " 固定前綴 + 用戶ID(如果是臨時用戶就是隨機生成的UUID)"),並創建一個 Cookie 保存購物車用戶數據(生成的UUID)。下次查詢時就先判斷是否已登陸狀態,如果是將通過用戶ID查出其登陸下的購物車數據,以及 Cookie 攜帶的UUID對應的 Redis 購物車數據,進行合並,最后刪除 UUID 對應的 Redis 數據。

public class CartInterceptor implements HandlerInterceptor {

    public static final ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    /**
     * 執行前攔截,將請求攜帶的cookie存入 threadLocal
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MemberTo memberTo = (MemberTo) request.getSession().getAttribute(LoginConstant.LOGIN_USERNAME);
        UserInfoTo userInfoTo = new UserInfoTo();
        // 如果已登錄將ID加入屬性
        if(memberTo != null){
            userInfoTo.setUserId(memberTo.getId());
        }else{
            userInfoTo.setTempUser(true);
        }

        Cookie[] cookies = request.getCookies();
        if(cookies != null && cookies.length>0){
            for (Cookie cookie : cookies) {
                if(cookie.getName().equals(CartConstant.CART_COOKIE_NAME)){
                    userInfoTo.setUserKey(cookie.getValue());
                }
            }
        }

        if(StringUtils.isEmpty(userInfoTo.getUserKey())){
            // 如果為空,說明沒有傳來對應的 Cookie,那么就創建一個UUID的Cookie
            userInfoTo.setUserKey(UUID.randomUUID().toString());
        }
        threadLocal.set(userInfoTo);
        return true;
    }


    /**
     * 返回請求前執行,將對應的user_key 的Cookie返回回去
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();

        boolean isEmpty = true;
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals(CartConstant.CART_COOKIE_NAME)){
                isEmpty = false;
            }
        }

        // 如果不是臨時用戶或是第一次綁定的臨時用戶,那么就返回user_key 的Cookie
        if(!userInfoTo.getTempUser() || isEmpty){
            Cookie cookie = new Cookie(CartConstant.CART_COOKIE_NAME,userInfoTo.getUserKey());
            // 如果不是臨時用戶,那么就延長有效時間
            cookie.setDomain("cart.gulimall.com");
            cookie.setMaxAge(CartConstant.CART_COOKIE_LIVE_TIME);
            response.addCookie(cookie);
        }
    }

}

userKey 存儲的是購物車的隨機碼,userId 就是已登陸的用戶ID,tempUser 表示當前用戶是否是臨時用戶,用於來查詢購物車數據時是否進行數據合並。

 

消息隊列

特點

1、一個隊列可以被多個客戶端監聽,但一個消息只能被一個客戶端所接收。一個客戶端處理完一個消息后才會去接收下一個消息

2、接收消息時需要添加注解@EnableRabbit,如果不需要監聽隊列就不需要添加這個注解

3、在定義接收消息的方法時,在方法參數上可以使用 Message 作為消息的原生結構,此時可以使用 getBody來獲取攜帶的數據,getMessageProperties 來獲取頭屬性消息;也可以直接定義消息數據的類型,直接獲取攜帶的數據。

4、@RabbitListener表示監聽某個隊列,可以標注在方法上或類上。標注在類上就可以搭配@RabbitHandler 注解表示某個方法是來監聽某個隊列的消息,沒有@RabbitHandler注解的就不是監聽方法。這樣做是可以定義接收多個類型對象數據消息的接收接口。

消息隊列的實體類要用同一個

 

消息可靠性

1、生產者到服務器 Broker。發送成功回調。

1)開啟發送端確認。

2)添加回調方法

 

2、服務器到消息隊列,失敗回調。

1)開啟配置

spring.rabbitmq.template.mandatory=true:隊列未收到消息,就回調錯誤消息給發送者

2)添加失敗回調方法

 

3、消息隊列到生產者。手動 ACK。

1)添加配置,默認是auto,自動確認。

2)接收方法里進行手動確認

 

deleveryTag 是一個自增的消息唯一標識

此外,如果發生異常,可以取消這次確認,並選擇是否重新加入隊列。拒絕確認有兩種方式。一種Nack,一種是 Reject。區別是 Nack 會將當前消息之前的所有未確認的消息也取消確認,而 Reject 只針對於當前消息。(未確認/取消確認的消息會被標記為 unacked 狀態,即使宕機也不會丟失)。

 

4、每條發送的消息都進行持久化存儲,進行定時檢查,對失敗的消息進行重發。

 

5、開啟RabbitMQ的事務(效率低,不推薦)

channel.txSelect()聲明啟動事務模式;

channel.txComment()提交事務;

channel.txRollback()回滾事務。

 

消息冪等性

在使用手動確認時,會存在一個問題,如果在消息執行完后還未手動確認,發生了斷點,那么下次就會被檢測到未確認,重新發送,這就導致客戶端對一條消息消費了多次。所以應該將消費端設計為冪等性的(如果把手動確認放在開始,那么就和自動確認沒區別了,甚至還意識不到丟失了消息,更嚴重)。

可以將單次操作與一個唯一值綁定,在每次執行前先判斷是否重復,如果未重復才執行。

就比如當前項目中的冪等性處理:庫存在解鎖時先判斷工作單狀態(解鎖會伴隨着工作單的修改,改成已解鎖狀態),是未解鎖狀態才會進行解鎖。並且將解鎖整個修改的操作設為一個大事務。避免只執行了部分操作就發生異常。

 

除此之外,還可以判斷當前是否是第一次接收此消息來進行判斷。判斷方式可以從記錄消息隊列的表中查詢;或者直接使用 message.getMessageProperties().getRedelivered(); 返回的 Boolean ,其就是反應此次消息是否是非第一次傳來的。

 

消息積壓

場景:客戶端宕機、並發量太大

解決:

1、限制消息發送,將額外的消息拒收,提示稍后重試

2、添加更多的消費者。

3、將多余的消息記錄先存儲到數據庫,然后慢慢來處理

 

延時隊列

延時隊列適用於一些需要定時自動執行的操作。比如當前項目中的庫存解鎖,就是用於訂單超時自動取消帶來的解鎖庫存操作。

實現方式

方式1、為消息設置TTL,弊端,因為是先進先出,所以必須等待先進的消息過期才能拿到里面的消息,即使后進的先過期也不行。所以效率不高

方式2、為隊列設置過期,等到消息進入后到達時間后直接被死信隊列獲取到。這也是當前項目使用的。

延時隊列的定義:

   /**
     * 延遲隊列(死信隊列)
     */
    @Bean
    public Queue orderDelayQueue() {
        /*
            Queue(String name,  隊列名字
            boolean durable,  是否持久化
            boolean exclusive,  是否排他
            boolean autoDelete, 是否自動刪除
            Map<String, Object> arguments) 屬性【TTL、死信路由、死信路由鍵】
         */
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "order-event-exchange");// 死信路由
        arguments.put("x-dead-letter-routing-key", "order.release.order");// 死信路由鍵
        arguments.put("x-message-ttl", 60000); // 消息過期時間 1分鍾
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);

        return queue;
    }
    /**
     * 交換機與延遲隊列的綁定
     */
    @Bean
    public Binding orderCreateBinding() {
        /*
         * String destination, 目的地(隊列名或者交換機名字)
         * DestinationType destinationType, 目的地類型(Queue、Exhcange)
         * String exchange,
         * String routingKey,
         * Map<String, Object> arguments
         * */
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

其他操作都與普通的消息發送一致

 

分布式事務

分布式事務是指一個業務涉及到多個服務的操作,那么普通的本地事務是無法約束遠程的方法,也就導致了本地的方法或遠程的方法某一個發生了異常,整個操作不能全部回滾的問題。

2PC模式(剛性事務)

如果需要強一致性,也就是修改的數據必須保證實時性。最簡單的解決方式就是利用 MySQL 的2PC模式(也叫XA模式),其原理是在第一階段將遠程調用的方法事務不進行提交,繼續向下執行。等到所有的操作都執行完畢,進入第二階段,提交之前所有的事務。這種方式的優點就是配置簡單,缺點就是效率低。因為在處於就緒狀態涉及到的數據表,因為事務還未提交,所以占用着這些數據的鎖,同時還可能會涉及到表鎖、間隙鎖等問題,造成其他數據也不能使用,這樣其他相關的操作就會阻塞,所以在高並發下效率很低。

 

Seata

seata 就是 2PC模式的改進版。改良的地方主要是在第一階段直接提交事務,並且記錄操作,在發生異常后按照記錄的日志進行逆操作回滾數據。

而seata主要分為三種模式:

AT模式:將分支事務執行過程分為了兩個階段。

第一階段是分支事務提交,並將回滾日志記錄在回滾表中。

二階段是主業務的提交帶動分支事務的提交,隨后刪除日志表中對應的回滾記錄。或者是發生異常導致主事務回滾,帶動分支事務進行回滾。

TCC模式:與AT模式不同之處就是將第一階段的提交執行邏輯與第二階段回滾(回滾數據庫處理)、提交全部自定義

Saga模式:適用於長事務,分支事務異步執行,可以由多個參與者參與,編寫分支事務以及補償服務(回滾操作),第一階段不會加鎖,執行效率高

 

配置

//1.2.0 Seata + 1.2.1 Nacos 配置
1、添加依賴
<!--分布式事務解決-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.2.0</version>
        </dependency>
2、添加數據庫seata,然后加表,將相關數據保存在db中
drop table if exists `global_table`;
create table `global_table` (
  `xid` varchar(128)  not null,
  `transaction_id` bigint,
  `status` tinyint not null,
  `application_id` varchar(32),
  `transaction_service_group` varchar(32),
  `transaction_name` varchar(128),
  `timeout` int,
  `begin_time` bigint,
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`xid`),
  key `idx_gmt_modified_status` (`gmt_modified`, `status`),
  key `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
  `branch_id` bigint not null,
  `xid` varchar(128) not null,
  `transaction_id` bigint ,
  `resource_group_id` varchar(32),
  `resource_id` varchar(256) ,
  `lock_key` varchar(128) ,
  `branch_type` varchar(8) ,
  `status` tinyint,
  `client_id` varchar(64),
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`branch_id`),
  key `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(36) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);
3、在執行需要執行分布式事務的庫中添加 undo_log表

drop table `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

4、    修改conf/file.conf 和 file.conf.example 文件,將 store的mode改為db,並且設置數據庫的庫名、地址、密碼;修改 registry.conf,將 registry的type 改成 nacos,serverAddr改成 localhost:8848
5、    配置文件添加seata的配置
seata:
  application-id: ${spring.application.name}
  tx-service-group: default
  service:
    vgroupMapping:
      default: default
    grouplist:
      default: 127.0.0.1:8091

6、在主業務方法上添加@GlobalTransactional注解,在調用的分布式事務上添加@Transactional 注解
View Code

 

缺點:說到底還是2PC模式的改良版,在高並發場景效率還是不夠高。但是適用於數據一致性要求高的場景。

 

最終一致性(柔性事務)

在數據一致性要求不高的場景,可以使用最終一致性來解決,也就是在短時間可能造成數據不一致的情況,但是隨着時間推移,數據最終會一致。

當前項目中使用的就是這個。具體實現就是在訂單業務中,調用了遠程鎖庫存方法,這樣如果只使用本地事務,那么發生異常會有兩種情況。

1、遠程鎖庫存執行時發生異常,但是由於事務回滾,而主業務在收到結果后也回滾,無損失。

2、遠程鎖庫存正常執行完畢,但是主業務隨后的某個位置發生異常,那么已執行過的鎖庫存就無法回滾了。

針對於第二種,在鎖庫存后發送一個消息給延時隊列,保證消息到達消費者時鎖庫存一定是已經完成了,接下來判斷當前訂單狀態是否是取消(訂單已經完成而解鎖了庫存)並且庫存工作單是否已解鎖(防止訂單取消時已經解鎖了造成重復解鎖)。這樣就可以保證庫存數據最終一致性了。

 

支付

加密算法

對稱加密:發送方和接收方解密和加密使用的四把鑰匙是同一種鑰匙,安全系數低,接收方和發送方破解了一個鑰匙,就可以進行自由的接收和發送,實現完整地通信。

非對稱加密:發送方和接收方加密和解密使用的鑰匙都不一樣,即使破解了一個,也無法實現自由通信。支付寶使用的就是非對稱加密。

1、支付寶在和商戶進行通信時,發送的每條請求都會現有發送方使用私匙,私匙是本地隱藏的,不對外公開,公鑰是約定好解析的鑰匙,也就是公開的鑰匙。

2、支付寶和商戶在發送請求時除了要操作的數據外,還會攜帶一個密匙,接收時就會先判斷密匙是否對的上,如果對不上就不處理。

 

收單

因為訂單消息會發送一個定時的訂單取消消息,所以如果在支付界面等到訂單取消執行后再支付,那么由於訂單已取消且庫存已解鎖,即使通過可以修改訂單狀態,但是庫存可能會被其他用戶所搶占完,那么這個訂單就會發生異常。所以解決方案就是在訂單取消后立刻調用支付寶的收單方法,並且限制單次訂單的最大時間,超時后也會自動收單。

1、設置單從訂單的自動收單時間

2、主動關閉訂單

 

定時任務

Cron表達式

語法:秒 分 時 日 月 周 年 (spring 不支持年,所以可以不寫)

 

特殊字符:

,:枚舉;

(cron="7,9,23****?"):任意時刻的7,9,23秒啟動這個任務;

-:范圍:

(cron="7-20****?""):任意時刻的7-20秒之間,每秒啟動一次

*:任意;

指定位置的任意時刻都可以

/:步長;

(cron="7/5****?"):第7秒啟動,每5秒一次;

(cron="*/5****?"):任意秒啟動,每5秒一次;

 

? :(出現在日和周幾的位置):為了防止日和周沖突,在周和日上如果要寫通配符使用?

(cron="***1*?"):每月的1號,而且必須是周二然后啟動這個任務;

 

L:(出現在日和周的位置)”,

last:最后一個

(cron="***?*3L"):每月的最后一個周二

 

W:Work Day:工作日

(cron="***W*?"):每個月的工作日觸發

(cron="***LW*?"):每個月的最后一個工作日觸發

#:第幾個

(cron="***?*5#2"):每個月的 第2個周4

 

使用

1、@EnableScheduling 開啟定時任務

2、@Scheduled開啟一個定時任務

3、自動配置類TaskSchedulingAutoConfiguration

 

 

 Spring 中的特點

1)spring周一都周天就是1-7

2)沒有年,只有6個參數

3)默認是阻塞的,也就是在執行過程中如果發生阻塞,定時任務也被阻塞,比如

 

雖然是周五每秒鍾執行一次,但是卻需要等待上一個方法執行完才能執行下一個定時任務。

 

阻塞解決

1、  將業務使用異步線程池進行處理

CompletableFuture.runAsync(() -> {

     },execute);

2、  SpringBoot內部支持了定時任務的線程池,可以讓每個任務各分配一個線程執行。配置文件:TaskSchedulingProperties。

配置:spring.task.scheduling.pool.size: 5

但是由於版本bug,有的版本可以實現,有的版本還是會阻塞

3、引入異步任務。

 

異步任務

配置

1、@EnableAsync:開啟異步任務

2、@Async:給希望異步執行的方法標注

3、自動配置類TaskExecutionAutoConfiguration

 

異步線程本身也是維護了一個線程池來實現的。

線程池的配置:

 

 

Sentinel 熔斷降級限流

使用配置

spring.cloud.sentinel.transport.dashboard=localhost:8333

spring.cloud.sentinel.transport.port=8719

 

實時監控、界面可視化

1、引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>      

 

2、添加暴露路徑配置

management.endpoints.web.exposure.include=*

 

 

流控

QPS:單秒訪問數

 

單機均值:限制單台機器同時的訪問量

總體閥值:限制所有機器同時的訪問量

 

流控模式:直接:限制的是當前接口

鏈路:限制的是通過遠程調用鏈式調用當前請求的請求,可以指定特定調用當前請求的URL

關聯:限制關聯的資源,如數據庫數據,另一個服務用到這個請求所涉及的數據,那么也會被限制

限制效果:直接拒絕:顧名思義

           Warm up:在預熱時間慢慢執行

           排隊等待:進入隊列,排隊執行

 

流控搭配的是限流信息,當流控達到設置的閥值時,后面的請求就會執行前面的限流提示方法,如果沒有設置自定義限流方法,那么就顯示默認的提示信息

 

 

自定義默認的限流提示信息(對整個請求進行限流)
@Component
public class SentinelPigBlockDataConfig implements BlockExceptionHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
        R error = R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), BizCodeEnum.TO_MANY_REQUEST.getMsg());
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().write(JSON.toJSONString(error));
    }
}

效果:

 

 自定義限制流量的資源
代碼塊

1、使用try catch包圍要限制的代碼塊

2、對請求下的資源進行限流

使用這個方式(包括下面的限流整個方法方式)不會觸發上面設置的自定義限流方法,而是執行catch(BlockException e)里的操作,然后繼續向后執行

 

方法

1、  在限制的方法上添加@SentinelResource 注解,並指定資源名

@SentinelResource(value = "resourceName1")

    這樣再配置資源的限流,在超過時就會拋出異常

2、  可以添加阻塞方法

 

 

這樣在超過限流后就會執行這個方法,作為原本方法執行的替代。需要注意的是處理方法的返回值以及可見性要和限流方法一致,同時需要添加BlockException的異常參數。

除了BlockHanlder 還可以指定 fallback 函數,fallback 指定的函數是在所有異常觸發時都會執行,而 BlockHanlder 是限流方法觸發,如果fallback函數在其他類,需要再額外指定 fallbackClass 參數,表示該方法在哪個類中,方法也必須是static

 

網關限流

可以將請求直接在網關層進行限流處理,進一步減小各個服務的壓力

在網關服務里添加依賴

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

 

 配置

 

API名稱:就是配置的網關ID

間隔:設置統計間隔(如2s就是每2s統計一次,那么第一秒統計完第二秒就不統計了到第三秒再統計)

Burst size:在達到閥值后額外再允許的請求數

 

同時也可以勾選針對請求屬性來對攜帶特定屬性的請求進行處理,並且可以添加API分組來一次性設置多個API

 

限流提示

默認的提示

 

自定義提示信息和調用方法

方法一(優先級更高):添加配置

 

 

 

 方法二:添加組件(和上面普通模塊自定義一樣)

 

 

降級

降級策略

RT:在一秒內,如果有五個請求執行的時間超過了設置RT時間后(單位毫秒),那么在后面的時間窗口內的時間會對進來的請求直接進行處理(如果是普通方法那么就走限流邏輯,如果是feign遠程方法就走Feign熔斷邏輯)。超過窗口時間后會重新計算。

 

Feign熔斷

配置

1、添加配置,打開feign的sentinel配置

feign.sentinel.enable=true

2、添加feign的熔斷回調方法(在服務宕機時調用)

 

隨后配合前面的降級配置,在2秒中有五次響應時間超過大於規定的時間,后面的請求就會直接執行這個熔斷回調方法(如果沒有配置熔斷回調方法就直接返回錯誤)。

 

注:設置降級的操作必須在調用的模塊中對feign調用方法資源進行配置,不能在被調用模塊資源里設置

 

總結

1、降級針對於所有資源;熔斷只針對於不同模塊間的遠程調用。

2、降級是一種對系統整體上的各個資源的主動控制;而熔斷是針對於具體的某個遠程調用接口,往往是被動觸發。

3、熔斷本質上是屬於降級的一種策略。遠程資源如果達到了降級規定的條件那么就會被動觸發熔斷。

 

Sleuth 鏈路追蹤

基礎配置

引入依賴

配置打印

 

Zipkin可視化配置

1、安裝服務器:docker run -d -p 9411:9411 openzipkin/zipkin

2、添加依賴:

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

(內部引入了Sleuth的基礎依賴)

3、  添加配置

#鏈路調用可視化配置
#服務追蹤
spring.zipkin.base-url=http://192.168.77.130:9411/
#關閉服務發現
spring.zipkin.discovery-client-enabled=false
spring.zipkin.sender.type=web
#配置采樣器
spring.sleuth.sampler.probability=1 

4、 在主類上移除TraceRedisAutoConfiguration類(視頻里不需要移除,測試版本需要)

5、到此為止,如果某個服務中引入了seata,也就是spring-cloud-starter-alibaba-seata,因為其內部通過SeataFeignClientAutoConfiguration集成了feign,所以在使用feign遠程調用時就會沖突導致連接不上,發生錯誤。

此時可以使用spring.sleuth.feign.enabled=false ,或在主類上移除SeataFeignClientAutoConfiguration來關閉,但是在使用Zipkin查看鏈路時不能查看到調用情況。所以還需要添加配置類

/**
 * @Author Rookie-6688
 * @Description 解決整合調用鏈zipkin時在移除 SeataFeignClientAutoConfiguration 后不能查看調用鏈的問題
 * @Create 2021-05-19 23:25
 */
@Component
@ConditionalOnClass({RequestInterceptor.class, GlobalTransactional.class})
public class SetSeataInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {

        String currentXid = RootContext.getXID();
        if (!StringUtils.isEmpty(currentXid)) {
            template.header(RootContext.KEY_XID, currentXid);
        }
    }
}

來將主業務的全局事務的XID,透傳到全局事務的各個子服務(feign遠程調用的各個事務)中。

 

鏈路數據持久化

將鏈路數據保存到Mysql、ES中

 

其他

手動發送Post請求

HttpResponse httpResponse = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());

  

Model、RedirectAttributes 傳值

1、redirectAttributes 存儲的數據可以在重定向后的頁面使用。Model存儲的數據只能用於請求轉發

2、redirectAttributes.addFlashAttribute("errors",errorMap); // 原理是將數據保存在cookie里重定向

3、model.addAttribute、redirectAttributes.addAttribute發出的請求會將數據放在URL的?后面,可以直接通過@RequestParam 獲取(重定向,請求轉發)。

 

將return 的數據作為Html數據展示

pay:

 

項目注意

1、  單一模塊:因為秒殺往往伴隨着高並發,所以應該涉及為一個單獨的模塊,這樣即使流量過大導致服務器癱瘓也只不會影響其他模塊。同時秒殺的商品消息,庫存都已經存儲在redis,避免高並發訪問數據庫

2、  鏈接加密:對秒殺請求的鏈接加密,防止惡意攻擊(項目使用了秒殺隨機碼)

3、  庫存預熱:避免進行數據庫操作,防止數據庫崩潰,使用redis的信號量替代庫存

4、  動靜分離:靜態資源不需要訪問后台服務器,減少服務器壓力

5、  惡意請求攔截:對於某個ip同時發送的上千次請求直接進行攔截,不進行處理

6、  流量錯峰:將流量分擔到更大的時間段上,比如增加驗證碼驗證

7、  限流、熔斷、降級:限流。前端點擊一次搶購后需要等待幾秒才能點第二次,后端在並發量達到一定次數后對一定比率的請求進行處理。熔斷。某一部分發生異常后不再繼續執行,直接返回失敗消息如遠程調用,如果一秒內有多個請求都請求超時,那么隨后的請求執行到這就直接返回錯誤消息,不再執行遠程調用(是一種主動的方式)。降級。將一部分請求降級,等待一段時間后才去處理(或者直接返回異常)

8、  隊伍削峰。將一些不需要強一致性的數據操作放入隊列中處理。如商品庫存的扣減,先通過信號量來扣除,然后又隊列慢慢去數據庫修改庫存

 

LocalDate 使用

 

UUID與雪花算法生成ID、自增ID

UUID:優點:1、簡單,生成性能快,2、唯一。數據庫合表可以不需要擔心主鍵沖突問題

                  缺點:沒有順序,數據庫新增、查詢、遍歷效率低

雪花算法:優點:1、生成效率高。2、同時根據時間生成ID,所以是遞增,查詢、插入、遍歷效率也高3、同時是唯一,在合表也不需要擔心主鍵沖突

                          缺點:因為是通過時間來生成的,當時間回溯時,可能會生成同一個主鍵

自增ID:優點:簡單

                  缺點:1、當生成的並發量大時,對數據庫壓力會變高 2、在合表時會比較麻煩 3、強依賴數據庫,當數據庫宕機后就無法實現

 

啟動Jar 包

java -jar jar包名 --server.port=端口號

 

數據庫性能

1、多張表查詢時盡量不要使用 in ,效率很低。

2、大表之間,如果查詢的條件列有索引,那么沒必要進行聯表查詢,因為連接查詢會進行笛卡爾積次數的遍歷,效率反而沒有單表多次查詢效率高(前提是大表,且有索引)。

 

遠程調用轉成想要的類型

1、在使用feign返回的Result數據里獲取傳過來的數據時,不要強轉(目前已知Map<Long,Integer>在強轉時會將Long轉成String類型),正確做法應該使用阿里的TypeReference:

如果存儲的是自定義類對象

 

非json格式的請求體數據不能使用requestBody來獲取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM