Springboot 系列(十六)你真的了解 Swagger 文檔嗎?

前言

目前來說,在 Java 領域使用 Springboot 構建微服務是比較流行的,在構建微服務時,我們大多數會選擇暴漏一個 REST API 以供調用。又或者公司採用前後端分離的開發模式,讓前端和後端的工作由完全不同的工程師進行開發完成。不管是微服務還是這種前後端分離開發,維持一份完整的及時更新的 REST API 文檔,會極大的提高我們的工作效率。而傳統的文檔更新方式(如手動編寫),很難保證文檔的及時性,經常會年久失修,失去應有的意義。因此選擇一種新的 API 文檔維護方式很有必要,這也是這篇文章要介紹的內容。

1. OpenAPI 規範介紹

OpenAPI Specification 簡稱 OAS,中文也稱 OpenAPI 描述規範,使用 OpenAPI 文件可以描述整個 API,它制定了一套的適合通用的與語言無關的 REST API 描述規範,如 API 路徑規範、請求方法規範、請求參數規範、返回格式規範等各種相關信息,使人類和計算機都可以不需要訪問源代碼就可以理解和使用服務的功能。

下面是 OpenAPI 規範中建議的 API 設計規範,基本路徑設計規範。

https://api.example.com/v1/users?role=admin&status=active
\________________________/\____/ \______________________/
         server URL       endpoint    query parameters
                            path

對於傳參的設計也有規範,可以像下面這樣:

  • , 例如 /users/{id}
  • , 例如 /users?role=未讀代碼
  • , 例如 X-MyHeader: Value
  • , 例如 Cookie: debug=0; csrftoken=BUSe35dohU3O1MZvDCU

OpenAPI 規範的東西遠遠不止這些,目前 OpenAPI 規範最新版本是 3.0.2,如果你想了解更多的 OpenAPI 規範,可以訪問下面的鏈接。

2. Swagger 介紹

很多人都以為 Swagger 只是一個接口文檔生成框架,其實並不是。 Swagger 是一個圍繞着 OpenAPI Specification(OAS,中文也稱 OpenAPI規範)構建的一組開源工具。可以幫助你從 API 的設計到 API 文檔的輸出再到 API 的測試,直至最後的 API 部署等整個 API 的開發周期提供相應的解決方案,是一個龐大的項目。 Swagger 不僅免費,而且開源,不管你是企業用戶還是個人玩家,都可以使用 Swagger 提供的方案構建令人驚艷的 REST API

Swagger 有幾個主要的產品。

  • – 一個基於瀏覽器的 Open API 規範編輯器。
  • – 一個將 OpenAPI 規範呈現為可交互在線文檔的工具。
  • – 一個根據 OpenAPI 生成調用代碼的工具。

如果你想了解更多信息,可以訪問 Swagger 官方網站 。

3. Springfox 介紹

源於 Java 中 Spring 框架的流行,讓一個叫做 Marrty Pitt 的老外有了為 SpringMVC 添加接口描述的想法,因此他創建了一個遵守 OpenAPI 規範(OAS)的項目,取名為 swagger-springmvc,這個項目可以讓 Spring 項目自動生成 JSON 格式的 OpenAPI 文檔。這個框架也仿照了 Spring 項目的開發習慣,使用註解來進行信息配置。

後來這個項目發展成為 Springfox,再後來擴展出 springfox-swagger2 ,為了讓 JSON 格式的 API 文檔更好的呈現,又出現了 springfox-swagger-ui 用來展示和測試生成的 OpenAPI 。這裏的 springfox-swagger-ui 其實就是上面介紹的 Swagger-ui,只是它被通過 webjar 的方式打包到 jar 包內,並通過 maven 的方式引入進來。

上面提到了 Springfox-swagger2 也是通過註解進行信息配置的,那麼是怎麼使用的呢?下面列舉常用的一些註解,這些註解在下面的 Springboot 整合 Swagger 中會用到。

註解 示例 描述
@ApiModel @ApiModel(value = “用戶對象”) 描述一個實體對象
@ApiModelProperty @ApiModelProperty(value = “用戶ID”, required = true, example = “1000”) 描述屬性信息,執行描述,是否必須,給出示例
@Api @Api(value = “用戶操作 API(v1)”, tags = “用戶操作接口”) 用在接口類上,為接口類添加描述
@ApiOperation @ApiOperation(value = “新增用戶”) 描述類的一個方法或者說一個接口
@ApiParam @ApiParam(value = “用戶名”, required = true) 描述單個參數

更多的 Springfox 介紹,可以訪問 Springfox 官方網站。

4. Springboot 整合 Swagger

就目前來說 ,Springboot 框架是非常流行的微服務框架,在微服務框架下,很多時候我們都是直接提供 REST API 的。REST API 如果沒有文檔的話,使用者就很頭疼了。不過不用擔心,上面說了有一位叫 Marrty Pitt 的老外已經創建了一個發展成為 Springfox 的項目,可以方便的提供 JSON 格式的 OpenAPI 規範和文檔支持。且擴展出了 springfox-swagger-ui 用於頁面的展示。

需要注意的是,這裏使用的所謂的 Swagger 其實和真正的 Swagger 並不是一個東西,這裏使用的是 Springfox 提供的 Swagger 實現。它們都是基於 OpenAPI 規範進行 API 構建。所以也都可以 Swagger-ui 進行 API 的頁面呈現。

4.1. 創建項目

如何創建一個 Springboot 項目這裏不提,你可以直接從 下載一個標準項目,也可以使用 idea 快速創建一個 Springboot 項目,也可以順便拷貝一個 Springboot 項目過來測試,總之,方式多種多樣,任你選擇。

下面演示如何在 Springboot 項目中使用 swagger2。

4.2. 引入依賴

這裏主要是引入了 springfox-swagger2,可以通過註解生成 JSON 格式的 OpenAPI 接口文檔,然後由於 Springfox 需要依賴 jackson,所以引入之。springfox-swagger-ui 可以把生成的 OpenAPI 接口文檔显示為頁面。Lombok 的引入可以通過註解為實體類生成 get/set 方法。

<dependencies> 
    <!-- Spring Boot web 開發整合 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-json</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入swagger2的依賴-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- jackson相關依賴 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.4</version>
    </dependency>

    <!-- Lombok 工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

4.3. 配置 Springfox-swagger

Springfox-swagger 的配置通過一個 Docket 來包裝,Docket 里的 apiInfo 方法可以傳入關於接口總體的描述信息。而 apis 方法可以指定要掃描的包的具體路徑。在類上添加 @Configuration 聲明這是一個配置類,最後使用 @EnableSwagger2 開啟 Springfox-swagger2。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * <p>
 * Springfox-swagger2 配置
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.codingme.boot.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("未讀代碼 API")
                .description("公眾號:未讀代碼(weidudaima) springboot-swagger2 在線借口文檔")
                .termsOfServiceUrl("https://www.codingme.net")
                .contact("達西呀")
                .version("1.0")
                .build();
    }
}

4.4. 代碼編寫

文章不會把所有代碼一一列出來,這沒有太大意義,所以只貼出主要代碼,完整代碼會上傳到 Github,並在文章底部附上 Github 鏈接。

參數實體類 User.java,使用 @ApiModel@ApiModelProperty 描述參數對象,使用 @NotNull 進行數據校驗,使用 @Data 為參數實體類自動生成 get/set 方法。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * <p>
 * 用戶實體類
 *
 * @Author niujinpeng
 * @Date 2018/12/19 17:13
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用戶對象")
public class User {

    /**
     * 用戶ID
     *
     * @Id 主鍵
     * @GeneratedValue 自增主鍵
     */
    @NotNull(message = "用戶 ID 不能為空")
    @ApiModelProperty(value = "用戶ID", required = true, example = "1000")
    private Integer id;

    /**
     * 用戶名
     */
    @NotNull(message = "用戶名不能為空")
    @ApiModelProperty(value = "用戶名", required = true)
    private String username;
    /**
     * 密碼
     */
    @NotNull(message = "密碼不能為空")
    @ApiModelProperty(value = "用戶密碼", required = true)
    private String password;
    /**
     * 年齡
     */
    @ApiModelProperty(value = "用戶年齡", example = "18")
    private Integer age;
    /**
     * 生日
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")
    @ApiModelProperty(value = "用戶生日")
    private Date birthday;
    /**
     * 技能
     */
    @ApiModelProperty(value = "用戶技能")
    private String skills;
}

編寫 Controller 層,使用 @Api 描述接口類,使用 @ApiOperation 描述接口,使用 @ApiParam 描述接口參數。代碼中在查詢用戶信息的兩個接口上都添加了 tags = "用戶查詢" 標記,這樣這兩個方法在生成 Swagger 接口文檔時候會分到一個共同的標籤組裡。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.domain.User;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 用戶操作
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */

@Slf4j
@RestController(value = "/v1")
@Api(value = "用戶操作 API(v1)", tags = "用戶操作接口")
public class UserController {

    @ApiOperation(value = "新增用戶")
    @PostMapping(value = "/user")
    public Response create(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            // 新增用戶信息 do something
            return ResponseUtill.success("用戶[" + user.getUsername() + "]信息已新增");
        }
    }

    @ApiOperation(value = "刪除用戶")
    @DeleteMapping(value = "/user/{username}")
    public Response delete(@PathVariable("username")
                           @ApiParam(value = "用戶名", required = true) String name) throws Exception {
        // 刪除用戶信息 do something
        return ResponseUtill.success("用戶[" + name + "]信息已刪除");
    }

    @ApiOperation(value = "修改用戶")
    @PutMapping(value = "/user")
    public Response update(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            String username = user.getUsername();
            return ResponseUtill.success("用戶[" + username + "]信息已修改");
        }
    }

    @ApiOperation(value = "獲取單個用戶信息", tags = "用戶查詢")
    @GetMapping(value = "/user/{username}")
    public Response get(@PathVariable("username")
                        @NotNull(message = "用戶名稱不能為空")
                        @ApiParam(value = "用戶名", required = true) String username) throws Exception {
        // 查詢用戶信息 do something
        User user = new User();
        user.setId(10000);
        user.setUsername(username);
        user.setAge(99);
        user.setSkills("cnp");
        return ResponseUtill.success(user);
    }

    @ApiOperation(value = "獲取用戶列表", tags = "用戶查詢")
    @GetMapping(value = "/user")
    public Response selectAll() throws Exception {
        // 查詢用戶信息列表 do something
        User user = new User();
        user.setId(10000);
        user.setUsername("未讀代碼");
        user.setAge(99);
        user.setSkills("cnp");
        List<User> userList = new ArrayList<>();
        userList.add(user);
        return ResponseUtill.success(userList);
    }
}

最後,為了讓代碼變得更加符合規範和好用,使用一個統一的類進行接口響應。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "響應信息")
public class Response {
    /**
     * 響應碼
     */
    @ApiModelProperty(value = "響應碼")
    private String code;
    /**
     * 響應信息
     */
    @ApiModelProperty(value = "響應信息")
    private String message;

    /**
     * 響應數據
     */
    @ApiModelProperty(value = "響應數據")
    private Collection content;
}

4.5. 運行訪問

直接啟動 Springboog 項目,可以看到控制台輸出掃描到的各個接口的訪問路徑,其中就有 /2/api-docs

這個也就是生成的 OpenAPI 規範的描述 JSON 訪問路徑,訪問可以看到。

因為上面我們在引入依賴時,也引入了 springfox-swagger-ui 包,所以還可以訪問 API 的頁面文檔。訪問路徑是 /swagger-ui.html,訪問看到的效果可以看下圖。

也可以看到用戶查詢的兩個方法會歸到了一起,原因就是這兩個方法的註解上使用相同的 tag 屬性。

4.7. 調用測試

springfox-swagger-ui 不僅是生成了 API 文檔,還提供了調用測試功能。下面是在頁面上測試獲取單個用戶信息的過程。

  1. 點擊接口 [/user/{username}] 獲取單個用戶信息。
  2. 點擊 **Try it out** 進入測試傳參頁面。
  3. 輸入參數,點擊 Execute 藍色按鈕執行調用。
  4. 查看返回信息。

下面是測試時的響應截圖。

5. 常見報錯

如果你在程序運行中經常發現像下面這樣的報錯。

java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_111]
    at java.lang.Long.parseLong(Long.java:601) ~[na:1.8.0_111]
    at java.lang.Long.valueOf(Long.java:803) ~[na:1.8.0_111]
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412) ~[swagger-models-1.5.20.jar:1.5.20]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:536) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:666) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:156) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:113) [jackson-databind-2.5.4.jar:2.5.4]

那麼你需要檢查使用了 @ApiModelProperty 註解且字段類型為数字類型的屬性上,@ApiModelProperty 註解是否設置了 example 值,如果沒有,那就需要設置一下,像下面這樣。

@NotNull(message = "用戶 ID 不能為空")
@ApiModelProperty(value = "用戶ID", required = true, example = "1000")
private Integer id;

文中代碼都已經上傳到

參考文檔

個人網站:
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試核心資料。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

【設計模式】單例模式的八種姿態寫法分析

目錄

前言
網上泛濫流傳單例模式的寫法種類,有說7種的,也有說6種的,當然也不排除說5種的,他們說的有錯嗎?其實沒有對與錯,刨根問底,寫法終究是寫法,其本質精髓大體一致!因此完全沒必要去追究寫法的多少,有這個時間還不如跟着宜春去網吧偷耳機、去田裡抓青蛙得了,一天天的….

言歸正傳…單例模式是最常用到的設計模式之一,熟悉設計模式的朋友對單例模式絕對不會陌生。同時單例模式也是比較簡單易理解的一種設計模式。

@

何謂單例模式?

專業術語

單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。

單例模式,簡單的說就是 一個類只能有一個實例,並且在整個項目中都能訪問到這個實例。

單例模式的優點

1、在內存中只有一個對象,節省內存空間。
2、避免頻繁的創建銷毀對象,可以提高性能。
3、避免對共享資源的多重佔用。
4、可以全局訪問。

單例模式實現整體思路流程

首先我們要清楚單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱)。

單例模式的常規實現思路大致相同為以下三個步驟:

1、私有構造方法
2、指向自己實例的私有靜態引用
3、以自己實例為返回值的靜態的公有的方法

當然也可以理解為
1、私有化構造方法,讓外部不能new。
2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
3、提供一個公有的static靜態方法(一般該方法使用getInstance這個名稱),返回實例對象。

將該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。

單例模式的適用場景

由於單例模式有很多獨特的優點,所以是編程中用的比較多的一種設計模式。我總結了一下我所知道的適合使用單例模式的場景:

1、需要頻繁實例化然後銷毀的對象。
2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
3、有狀態的工具類對象。
4、頻繁訪問數據庫或文件的對象。

在後面我將會講到JDK中的Runtime類就是使用的餓漢式單例!在Spring MVC框架中的controller 默認是單例模式的!

單例模式的八種姿態寫法

宜春強烈建議:如果是沒有接觸單例模式的讀者朋友強烈建議你們動手敲一遍,不要複製,不然沒效果!

還有一點就是,要真正輕而易舉的理解單例模式,JVM的類加載知識是不能少的,不然你只是會敲的層次,啥?不懂類加載?放心,宜春就是要你會,要你理解透徹。

其實上面的這篇文章特別重要,上面這篇文章的重要性懂的自然懂,不懂的希望能理解宜春的一片好意,去看一下吧,實在看不懂看不下去在回來看這篇文章就好了,再大不了就把博主一起按在馬桶蓋蓋上….

是不是心裏暖暖的?宜春也不多嗶嗶了,直接擼碼走起….

姿態一:餓漢式1(靜態變量)

package singletonPattern;
//餓漢式(靜態變量)

class Singleton{
    //1、私有化構造方法,讓外部不能new
    private Singleton(){

    }
    //2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
    private final static Singleton instance=new Singleton();

    //3、提供一個公有的static靜態方法,返回實例對象
    public static Singleton getInstance(){
        return instance;
    }
}
//以下是測試代碼=====================

public class SingletenDemo1 {
    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
        Singleton singleton2=Singleton.getInstance();
//驗證一:
        System.out.println(singleton==singleton2);
//驗證二:
        System.out.println(singleton.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

//運行結果:
//        true
//        460141958
//        460141958

/*
餓漢式(靜態變量)方法

優點:寫法簡單,在類加載的時候就完成了實例化,同時也就避免了線程同步問題,因此線程安全
缺點:由於是在類加載時就完成了實例化,沒有達到懶加載的效果。如果一直沒有使用過這個實例,就造成了內存的浪費!

總結:這種方式基於ClassLoader類加載機制避免了多線程的同步問題,只不過instance屬性在類加載就實例化,在單例模式中大多數都是調用getInstance方法,
     由於getInstance方法是static靜態的,調用它肯定會觸發類加載!但是觸發類加載的原因有很多,我們不能保證這個類會通過其他的方式觸發類加載(比如調用了其他的static方法)
     這個時候初始化instance就沒有達到lazy loading 懶加載的效果,可能造成內存的浪費!

     餓漢式(靜態變量)這種方式可以使用但是會造成內存的浪費!

     */

姿態二:餓漢式2(static靜態代碼塊)

package singletonPattern;
//餓漢式2(static靜態代碼塊)

class Singleton2{
    private Singleton2(){

    }

    private static Singleton2 instance;

    static{ //把創建單例對象的操作放進了static靜態代碼塊中==============
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}
//餓漢式2(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!
//唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中

姿態三:懶漢式1(線程不安全)

package singletonPattern;
//懶漢式1(線程不安全)
class Singleton3{
    private Singleton3(){

    }

    private static Singleton3 instance;

    public static Singleton3 getInstance(){
        if(instance == null){
            instance=new Singleton3();
        }
        return instance;
    }
}
/*
懶漢式(線程不安全)的這種方式起到了懶加載的效果,但只能在單線程下使用。
如果在多線程下,一個線程進入了if(singleton==null)判斷語句塊,還沒執行產生實例的句子,另一個線程
又進來了,這時會產生多個實例,所以不安全。

結語:懶漢式(線程不安全)在實際開發中,不要使用這種方式!!存在潛在危險
*/

姿態四:懶漢式2(線程安全)

package singletonPattern;
//懶漢式2(線程安全)
class Singleton4{
    private Singleton4(){

    }

    private static Singleton4 instance;

    public static synchronized Singleton4 getInstance(){
        if(instance == null){
            instance=new Singleton4();
        }
        return instance;
    }
}

/*
懶漢式2(線程安全)方式

優點:線程安全
缺點:效率太低,每次調用getInstance方法都要進行同步

結語:懶漢式2(線程安全)方式在開發中不推薦使用,主要是效率太低了*/

姿態五:餓漢式2(static靜態代碼塊)

package singletonPattern;
//懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,在多線程下依舊會有多個實例
class Singleton5{
    private Singleton5(){

    }

    private static Singleton5 instance;

    public static  Singleton5 getInstance(){
        if(instance == null){   //多線程情況下可能多個線程進入這個if塊
            synchronized (Singleton5.class){  //到這裏只會一個一個創建實例,雖然安全,但是就不再是單例了
                instance=new Singleton5();
            }
        }
        return instance;
    }
}
/*
懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,依舊會有多個實例

結語:懶漢式3 同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個單例設計的有點搞笑*/

姿態六:雙重檢查單例

package singletonPattern;
//雙重檢查應用實例方式
class Singleton6{
    private Singleton6(){}

    private static volatile Singleton6 singleton;

    public static Singleton6 getInstance(){
        if(singleton==null){
            synchronized(Singleton6.class){
                if(singleton == null){
                    singleton= new Singleton6();
                }
            }
        }
        return singleton;
    }
}
/*
雙重檢查應用實例方式:

線程安全、延遲加載、效率較高

結語:開發中推薦使用!
*/

這個時候博主就得嗶嗶幾句了,細心的童鞋會發現有一個Volatile關鍵字,完了,沒見過,小白童鞋慌了!

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。

這種實現方式既可以實現線程安全地創建實例,而又不會對性能造成太大的影響。它只是第一次創建實例的時候同步,以後就不需要同步了,從而加快了運行速度。

姿態七:靜態內部類單例

package singletonPattern;
//static靜態內部類單例

class Singleton7{
    private Singleton7(){}

    private static volatile Singleton7 instance;

    //寫一個static靜態內部類,給該類添加一個static靜態instance屬性
    private static class SingletonInstance{
        private static final Singleton7 SINGLETON_7=new Singleton7();
    }

    //
    public static synchronized Singleton7 getInstence(){
        return SingletonInstance.SINGLETON_7;
    }
}
/*
靜態內部類單例方式
        1、這種方式採用了類加載機制來保證初始化實例時只有一個線程
        2、巧妙的將實例化Singleton操作放進getInstance方法中,getInstance方法返回靜態內部類中實例化好的Singleton
        3、類的靜態屬性只會在第一次加載類的時候初始化,也就是只會初始化一次,在這裏,JVM幫我們保證了線程的安全,類在初始化時,別的線程無法進入。
       
        優點:線程安全、利用靜態內部類特點實現延遲加載、效率高
        開發中推薦使用這種靜態內部類單例方式!

static靜態內部特點:
1、外部類加載不會導致內部類加載,保證了其懶加載
*/

這個單例,宜春就不得不嗶嗶兩句了,要清楚這個單例,必須要明白static靜態內部特點,也就是外部類加載不會導致內部類加載!

姿態八:餓漢式2(static靜態代碼塊)

package singletonPattern;
//使用枚舉

import com.sun.xml.internal.bind.v2.runtime.unmarshaller.XsiNilLoader;

enum Singleton8{
    INSTANCE;
    public void methodName(){
        System.out.println("測試數據");
    }
}
/*

枚舉方式的枚舉:
推薦寫法,簡單高效。充分利用枚舉類的特性,只定義了一個實例,且枚舉類是天然支持多線程的。
藉助JDK1.5中添加的枚舉來實現單例模式優點:
         1、不僅能避免多線程同步問題 
         2、還能防止反序列化重新創建新的對象

枚舉方式單例是由Effective java作者Josh Bloch提倡的,結語:推薦使用!
*/

當然也可以測試一下

public class SingletonDemo8 {
    public static void main(String[] args) {
        Singleton8 instance = Singleton8.INSTANCE;
        Singleton8 instance2 = Singleton8.INSTANCE;
        System.out.println(instance==instance2);

        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());

        instance.methodName();
    }
}

運行結果:

true
460141958
460141958
測試數據

屬實沒毛病!

JDK源碼中單例模式的應用

先來看一段Runtime 的源碼吧,並分析一下其使用的是種單例模式!

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

這應該不難看出吧!如果看不出的話只能說明你真的還沒有理解單例模式,我其實想說單例模式其實是23種設計模式中最簡單的一個,只是寫法比較多而已!同時面試官一般都會問單例模式,它已經是很基礎的了,問的稍微有點水平就是問你單例模式在JDK中哪裡運用到了,顯然JDK中的Runtime其實它使用的就是餓漢式單例!正如註釋所說,每一個java應用程序都有一個Runtime實例。Runtime的單例模式是採用餓漢模式創建的,意思是當你加載這個類文件時,這個實例就已經存在了。

Runtime類可以取得JVM系統信息,或者使用gc()方法釋放掉垃圾空間,還可以使用此類運行本機的程序。

==還有就是spring Mvc 中的controller 默認是單例模式的,解析。==

單例模式總結

1、餓漢式(靜態變量)這種方式可以使用,但是沒有達到 lazy loading 懶加載的效果會造成內存的浪費!開發中不建議使用。
2、餓漢式(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中
3、懶漢式(線程不安全)起到了懶加載的效果,但只能在單線程下使用。在實際開發中,不要使用這種方式!!!
4、懶漢式2(線程安全)方式線程安全但是效率太低,每次調用getInstance方法都要進行同步。所以在開發中不推薦使用。 5、懶漢式3
同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個設計有點搞笑哈哈。
6、雙重檢查應用實例方式,線程安全、延遲加載、效率較高。因此開發中推薦使用!
7、靜態內部類單例方式線程安全、利用靜態內部類特點實現延遲加載、效率高。 開發中推薦使用這種靜態內部類單例方式!
8、藉助JDK1.5中添加的枚舉來實現單例模式不僅能避免多線程同步問題還能防止反序列化重新創建新的對象。枚舉方式單例是由Effective java作者Josh Bloch提倡的,開發中推薦使用!

單例模式必須考慮到在多線程的應用場合下的使用,畢竟現在的服務器基本上都是多核的了。

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

mybatis精講(三)–標籤及TypeHandler使用

目錄

話引

  • 前兩張我們分別介紹了Mybatis環境搭建及其組件的生命周期。這些都是我們Mybatis入門必備技能。有了前兩篇的鋪墊我們今天就來深入下Mybatis, 也為了填下之前埋下的坑。

XML配置標籤

概覽


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration> 
    <!--引入外部配置文件-->
    <properties resource=""/>
    <!--設置-->
    <settings/>
    <!--定義別名-->
    <typeAliases>
        <package name=""/>
    </typeAliases>
    <!--類型處理器-->
    <typeHandlers/>
    <!--對象工廠-->
    <objectFactory/>
    <!--插件-->
    <plugins/>
    <!--定義數據庫信息,默認使用development數據庫構建環境-->
    <environments default="development">
        <environment id="development">
            <!--jdbc事物管理-->
            <transactionManager type="JDBC"/>
            <!--配置數據庫連接信息-->
            <dataSource type="POOLED"/>
        </environment>
    </environments>
    <!--數據庫廠商標識-->
    <databaseIdProvider/>
    <mappers/>
</configuration>

  • 上面模板列出了所有xml可以配置的屬性。這裏plugins是一個讓人哭笑不得的東西。用的好是利器,用的不好就是埋坑。接下來我們來看看各個屬性的作用

properties

  • 該標籤的作用就是引入變量。和maven的properties一樣。在這裏定義的變量或者引入的變量,在下面我們是可以童工${}使用的。

子標籤property


<properties>
  <property name="zxhtom" value="jdbc:mysql://localhost:3306/mybatis"/>
</properties>

<dataSource type="POOLED">
<property name="driver" value="${zxhtom}"/>
<dataSource>
  • 上述的配置就可以直接使用zxhtom這個變量。

resource

  • 除了上述方法我們還可以通過引入其他properties文件,就可以使用文件里的配置變量了。

<properties resource="mybatis.properties"/>

程序注入

  • 最後還有一種我們在構建SqlSessionFactory的時候重新載入我們的Properties對象就可以了。另外三者的優先級是從低到高

settings


configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
  • 上面代碼是我們在XMLConfigBuilder解析settings標籤的代碼。從這段代碼中我們了解到settings子標籤。
參數 功能 可選值 默認值
autoMappingBehavior 指定Mybatis應如何自動映射列到字段上。
NONE : 表示取消自動映射
PARTIAL:只會自動映射沒有定義嵌套結果集映射的結果集
FULL : 自動映射任意複雜的結果集
NONE、PARTIAL、FULL PARTIAL
autoMappingUnknownColumnBehavior 指定識別到位置列或屬性的時間
NONE : 什麼都不做
WARNING:日誌會報警(前提是日誌設置了显示權限)
FAILING : 拋出異常。
NONE, WARNING, FAILING NONE
cacheEnabled 該配置影響的所有映射器中配置的緩存的全局開關 true|false true
proxyFactory 指定Mybatis創建具有延遲加載能力的對象所用到的代理工具未指定時將自動查找 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING not set
lazyLoadingEnabled 延時加載全局開關
開啟時:級聯對象會延時加載;級聯標籤中可以通過fetchType來定製覆蓋此選項
true|false false
aggressiveLazyLoading 啟用時:對任意延遲屬性的調用會使帶有延遲加載屬性的對象分層性質完整加載,反之按需加載 true|false true
multipleResultSetsEnabled 是否允許單一語句返回多結果集 true|false true
useColumnLabel 確切的說當映射找不到參數時會使用列標籤(數據庫列名)代替別名去映射 true|false true
useGeneratedKeys 允許 JDBC 支持自動生成主鍵,需要驅動兼容。 如果設置為 true 則這個設置強制使用自動生成主鍵,儘管一些驅動不能兼容但仍可正常工作(比如 Derby) true|false false
defaultExecutorType 配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 設置超時時間,決定驅動等待數據庫響應的秒數 整數 null
defaultFetchSize 設置數據庫resultSet讀取數據方式,默認全部加載進內存,設置該屬性可以設置一次性讀多少條數據進內存 整數 null
mapUnderscoreToCamelCase 是否開啟自動駝峰命名規則(camel case)映射,即從經典數據庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似映射。 true|false false
safeRowBoundsEnabled -允許在嵌套語句中使用分頁 true|false false
localCacheScope 一級緩存。mybatis默認對同一個sqlsession中數據是共享的。一個sqlsession調用兩次相同查詢實際只會查詢一次。就是因為該屬性為SESSION , STATEMENT則針對的是每一條sql SESSION|STATEMENT SESSION
jdbcTypeForNull 當沒有為參數提供特定的jdbc類型時,為空值則指定JDBC類型。在新增時我們沒有設置參數,這個時候就會根據此參數天長。加入設置VARCHAR,那麼我們新增的數據沒傳參數則為空字符 NULL|VARCHAR|OTHER OTHER
lazyLoadTriggerMethods 指定具體方法延時加載 方法 equals,clone,hashCode,toString
safeResultHandlerEnabled 允許在嵌套語句中使用分頁 true|false true
defaultScriptingLanguage 動態SQL生成的默認語言 org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
defaultEnumTypeHandler mybatis默認的枚舉處理類
callSettersOnNulls 指定當結果集中值為null的時候是否調用映射對象的setter(put)方法。
useActualParamName 允許使用他們的編譯后名稱來映射,3.4.2后默認true.在xml中#{0}則報錯。設置為false,則#{0}代表第一個參數#{n}第n個 true|false true
returnInstanceForEmptyRow 當返回行的所有列都是空時,MyBatis默認返回 null。 當開啟這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集 (如集合或關聯)。(新增於 3.4.2) true|false false
logPrefix 指定 MyBatis 增加到日誌名稱的前綴。

別名

  • 別名是mybatis為我們項目中類起的一個名字,類名往往會很長所以別名就方便我們平時的開發。Mybatis為我們內置了一些類的別名:byte、short、int、long、float、double、boolean、char等基礎類型的別名。還有其的封裝類型、String,Object,Map,List等等常用的類。
    org.apache.ibatis.type.TypeAliasRegistry這個類中幫我們內置了別名。可以看下。自定義別名也是通過這個類進行註冊的。我們可以通過settings中typeAliases配置的方式結合@Alias。或者掃描包也可以的。

TypeHandler

  • 這個接口就四個方法

public interface TypeHandler<T> {

  /**
   * 設置參數是用到的方法
   */
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}
  • 可以理解成攔截器。它主要攔截的是設置參數和獲取結果的兩個節點。這個類的作用就是將Java對象和jdbcType進行相互轉換的一個功能。同樣的在org.apache.ibatis.type.TypeHandlerRegistry這個類中mybatis為我們提供了內置的TypeHandler。基本上是對於基本數據和分裝對象的轉換。
  • 下面我們隨便看一個TypeHandler處理細節
public class BooleanTypeHandler extends BaseTypeHandler<Boolean> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setBoolean(i, parameter);
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    boolean result = rs.getBoolean(columnName);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    boolean result = rs.getBoolean(columnIndex);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    boolean result = cs.getBoolean(columnIndex);
    return !result && cs.wasNull() ? null : result;
  }
}
  • setParameter是PreparedStatement進行設置成boolean類型。getResult分別通過三種不同方式獲取。在這些方法里我們可以根據自己也無需求進行控制。常見的控制是枚舉的轉換。傳遞參數過程可能是枚舉的name,但是傳遞到數據庫中要枚舉的index.這種需求我們就可以在TypeHandler中實現。我們書寫的typeHandler之後並不能被識別,還需要我們在resultMap中的result標籤中通過typeHandler指定我們的自定義Handler.

自定義TypeHandler

  • 承接上文我們說道枚舉的轉換。下面我們還是已學生類為例。學生中性別之前是boolean類型。現在我們採用枚舉類型。但是數據庫中存的還是數據,01.

EnumTypeHandler

  • 在TypeHandlerRegister類中申明了默認的枚舉類處理器是private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
  String s = rs.getString(columnName);
  return s == null ? null : Enum.valueOf(type, s);
}
  • 我們通過這個方法可以看出,這個枚舉處理器適合已枚舉名稱存儲的方式

EnumOrdinalTypeHandler

  • 在Enum中還有一個屬性oridinal。這個表示枚舉中的索引。然後我們通過查看Mybatis提供的處理器發現有個叫EnumOrdinalTypeHandler。我們很容易聯想到的就是這個處理器是通過枚舉的所以作為數據庫內容的。在SexEnum中MALE存儲到數據庫中則為0.注意這個0不是我們的index.而是MALE的索引。如果將MALE和FEMAEL調換。那麼MALE索引則為1.
  • 因為默認的是EnumTypeHandler。所以想用EnumOrdinalTypeHandler的話我們要麼在resultMap中sex字段指定該處理器。要不就通過配置文件typeHandlers註冊進來。(將處理器與Java類進行綁定。mybatis遇到這個Java對象的時候就知道用什麼處理器處理)

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.github.zxhtom.enums.SexEnum"/>
</typeHandlers>

SexTypeHandler

  • 上面的不管是通過名稱存儲還是通過索引存儲都不太滿足我們的需求。我們想通過我們的index存儲。那麼這時候我們就得自定義處理邏輯了。Mybatis處理器都是繼承BaseTypeHandler。因為BaseTypeHandler實現了TypeHandler.所以我們這裏也就繼承BaseTypeHandler。

public class SexTypeHandler extends BaseTypeHandler<SexEnum> {


    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i,parameter.getIndex());
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int i = rs.getInt(columnName);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int i = rs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int i = cs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }
}

typeHandler注意點

  • 在編寫自定義處理器的時候我們得之處Javatype、jdbctype。兩者不是必填。但至少得有一個。正常我們默認javatype是必填的。
  • 填寫的方式有三種
    • 通過MappedTypes、MappedJdbcTypes分別指定javatype、jdbctype
    • 通過在mybatis-config.xml中配置typeHandlers進行註解。裏面也有這兩個屬性的配置。
    • 通過在mapper.xml的resultmap中再次指定某個字段的typehandler.
  • TypeHandler為我們提供了Java到jdbc數據的轉換橋樑。極大的方便了我們平時的開發。讓我們開發期間忽略數據的轉換這麼糟心的事情。

# 加入戰隊

微信公眾號

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

簡單的學習,實現,領域事件,事件存儲,事件溯源

為什麼寫這篇文章

自己以前都走了彎路,以為學習戰術設計就會DDD了,其實DDD的精華在戰略設計,但是對於我們菜鳥來說,學習一些技術概念也是挺好的
經常看到這些術語,概念太多,也想簡單學習一下,記憶力比較差記錄一下實現的細節

領域事件

1.領域事件是過去發生的與業務有關的事實,一但發生就不可更改,所以存儲事件時只能追加

3.領域事件具有時間點的特徵,所有事件連接起來會形成明顯的時間軸

4.領域事件會導致目標對象狀態的變化,聚合根的行為會產生領域事件,所以會改變聚合的狀態

在聚合根裏面維護一個領域事件的聚合,每一個事件對應一個Handle,通過反射維護一個數據字典,通過事件查找到指定的Handle

領域事件實現的方式:目前看到有3種方式,MediatR,消息隊列 ,發布訂閱模式

eShopOnContainers 中使用的是MediatR

ENode 中使用的是EQueue,EQueue是一個純C#寫的消息隊列

使用已經寫好的消息隊列Rabbitmq ,kafka

事件存儲,事件溯源,事件快照

事件存儲:存儲所有聚合根裏面發生過的事件

1.事件存儲中可以做併發的處理,比如Command 重複,領域事件的重複

2.領域事件的重複通過聚合根Id+版本號判斷,可以在數據庫中建立聯合唯一索引,在存儲事件時檢測重複,記錄重複的事件,根據業務做處理

3.這裏要保證存儲事件與發布領域事件的一致性

如何保證存儲事件與發布領域事件的一致性

先存儲事件然後在發布領域事件,如果發生異常,就一直重試,一直到成功為止,也可以做一定的處理,比如重試到一定的次數,就通知,進行人工處理

我選擇了CAP + Policy + Dapper

事件溯源:在事件存儲中記錄導致狀態變化的一系列領域事件。通過持久化記錄改變狀態的事件,通過重新播放獲得狀態改變的歷史。 事件回放可以返回系統到任何狀態

聚合快照:聚合的生命周期各有長短,有的聚合裏面有大量的事件,,事件越多加載事件以及重建聚合的執行效率就會越來越低,快照裏面存儲的是聚合

1.定時存儲整個聚合根:使用定時器每隔一段時間就存儲聚合到快照表中

2.定量存儲整個聚合根:根據事件存儲中的數量來存儲聚合到快照表中

事件溯源的實現方式

1.首先我們需要實現聚合In Memory,

2.在CommandHandler中訂閱 Command命令,

創建聚合時 ,在內存中維護一個數據字典,key為:聚合根的Id,value為:聚合

修改,刪除,聚合時,根據聚合根的Id,查詢出聚合

如果內存中聚合不存在時:根據聚合根的Id 從聚合快照表中查詢出聚合,然後根據聚合快照存儲的時間,聚合根Id,查詢事件存儲中的所有事件,然後回放事件,得到聚合最終的狀態

記錄遇到的問題

由於基礎非常的差,所以實現的方式都是以最簡單的方式來寫的,存在許多的問題,代碼中有問題的地方希望大家提出來,讓我學習一下

代碼的實現目前還沒有寫快照的部分,也沒有處理EventStorage中的命令重複與聚合根+版本號重複,具體的請看湯總的ENode,裏面有全部的實現

1.怎樣保證存儲事件,發布事件的最終一致性

2.怎麼解析EventStorage中的事件,回放事件

先存儲事件,當事件存儲成功之後,在發布事件

存儲事件失敗:就一直重試,發布事件失敗,使用的是CAP,CAP內部使用的是本地消息表的方式,如果發布事件失敗,也一直重試,如果服務器重啟了,Rabbitmq裏面消息為Ack,消息沒有丟,重連後會繼續執行

存儲事件,發布事件

    /// <summary>
    /// 存儲聚合根中的事件到EventStorage 發布事件
    /// </summary>
    /// <typeparam name="TAggregationRoot"></typeparam>
    /// <param name="event"></param>
    /// <returns></returns>
    public async Task AppendEventStoragePublishEventAsync<TAggregationRoot>(TAggregationRoot @event)
        where TAggregationRoot : IAggregationRoot
    {
        var domainEventList = @event.UncommittedEvents.ToList();
        if (domainEventList.Count == 0)
        {
            throw new Exception("請添加事件!");
        }

        await TryAppendEventStorageAsync(domainEventList).ContinueWith(async e =>
        {
            if (e.Result == (int)EventStorageStatus.Success)
            {
                await TryPublishDomainEventAsync(domainEventList).ConfigureAwait(false);
                @event.ClearEvents();
            }
        });
    }

    /// <summary>
    /// 發布領域事件
    /// </summary>
    /// <returns></returns>
    public async Task PublishDomainEventAsync(List<IDomainEvent> domainEventList)
    {
        using (var connection =
            new SqlConnection(ConnectionStr))
        {
            if (connection.State == ConnectionState.Closed)
            {
                await connection.OpenAsync().ConfigureAwait(false);
            }
            using (var transaction = await connection.BeginTransactionAsync().ConfigureAwait(false))
            {
                try
                {
                    if (domainEventList.Count > 0)
                    {
                        foreach (var domainEvent in domainEventList)
                        {
                            await _capPublisher.PublishAsync(domainEvent.GetRoutingKey(), domainEvent).ConfigureAwait(false);
                        }
                    }
                    await transaction.CommitAsync().ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    await transaction.RollbackAsync().ConfigureAwait(false);
                    throw;
                }
            }
        }
    }

    /// <summary>
    /// 發布領域事件重試
    /// </summary>
    /// <param name="domainEventList"></param>
    /// <returns></returns>
    public async Task TryPublishDomainEventAsync(List<IDomainEvent> domainEventList)
    {
        var policy = Policy.Handle<SocketException>().Or<IOException>().Or<Exception>()
            .RetryForeverAsync(onRetry: exception =>
            {
                Task.Factory.StartNew(() =>
                {
                    //記錄重試的信息
                    _loggerHelper.LogInfo("發布領域事件異常", exception.Message);
                });
            });
        await policy.ExecuteAsync(async () =>
        {
            await PublishDomainEventAsync(domainEventList).ConfigureAwait(false);
        });

    }

    /// <summary>
    /// 存儲聚合根中的事件到EventStorage中
    /// </summary>
    /// <returns></returns>
    public async Task<int> AppendEventStorageAsync(List<IDomainEvent> domainEventList)
    {
        if (domainEventList.Count == 0)
        {
            throw new Exception("請添加事件!");
        }
        var status = (int)EventStorageStatus.Failure;
        using (var connection = new SqlConnection(ConnectionStr))
        {
            try
            {
                if (connection.State == ConnectionState.Closed)
                {
                    await connection.OpenAsync().ConfigureAwait(false);
                }
                using (var transaction = await connection.BeginTransactionAsync().ConfigureAwait(false))
                {
                    try
                    {
                        if (domainEventList.Count > 0)
                        {
                            foreach (var domainEvent in domainEventList)
                            {
                                EventStorage eventStorage = new EventStorage
                                {
                                    Id = Guid.NewGuid(),
                                    AggregateRootId = domainEvent.AggregateRootId,
                                    AggregateRootType = domainEvent.AggregateRootType,
                                    CreateDateTime = domainEvent.CreateDateTime,
                                    Version = domainEvent.Version,
                                    EventData = Events(domainEvent)
                                };
                                var eventStorageSql =
                                    $"INSERT INTO EventStorageInfo(Id,AggregateRootId,AggregateRootType,CreateDateTime,Version,EventData) VALUES (@Id,@AggregateRootId,@AggregateRootType,@CreateDateTime,@Version,@EventData)";
                                await connection.ExecuteAsync(eventStorageSql, eventStorage, transaction).ConfigureAwait(false);
                            }
                        }
                        await transaction.CommitAsync().ConfigureAwait(false);
                        status = (int)EventStorageStatus.Success;
                    }
                    catch (Exception e)
                    {
                        await transaction.RollbackAsync().ConfigureAwait(false);
                        throw;
                    }
                }

            }
            catch (Exception e)
            {
                connection.Close();
                throw;
            }
        }
        return status;
    }

    /// <summary>
    /// AppendEventStorageAsync異常重試
    /// </summary>
    public async Task<int> TryAppendEventStorageAsync(List<IDomainEvent> domainEventList)
    {
        var policy = Policy.Handle<SocketException>().Or<IOException>().Or<Exception>()
            .RetryForeverAsync(onRetry: exception =>
            {
                Task.Factory.StartNew(() =>
                {
                    //記錄重試的信息
                    _loggerHelper.LogInfo("存儲事件異常", exception.Message);
                });
            });
        var result = await policy.ExecuteAsync(async () =>
          {
              var resulted = await AppendEventStorageAsync(domainEventList).ConfigureAwait(false);
              return resulted;
          });
        return result;
    }

    /// <summary>
    /// 根據DomainEvent序列化事件Json
    /// </summary>
    /// <param name="domainEvent"></param>
    /// <returns></returns>
    public string Events(IDomainEvent domainEvent)
    {
        ConcurrentDictionary<string, string> dictionary = new ConcurrentDictionary<string, string>();
        //獲取領域事件的類型(方便解析Json)
        var domainEventTypeName = domainEvent.GetType().Name;
        var domainEventStr = JsonConvert.SerializeObject(domainEvent);
        dictionary.GetOrAdd(domainEventTypeName, domainEventStr);
        var eventData = JsonConvert.SerializeObject(dictionary);
        return eventData;
    }

解析EventStorage中存儲的事件

    public async Task<List<IDomainEvent>> GetAggregateRootEventStorageById(Guid AggregateRootId)
    {
        try
        {
            using (var connection = new SqlConnection(ConnectionStr))
            {
                var eventStorageList = await connection.QueryAsync<EventStorage>($"SELECT * FROM dbo.EventStorageInfo WHERE AggregateRootId='{AggregateRootId}'");
                List<IDomainEvent> domainEventList = new List<IDomainEvent>();
                foreach (var item in eventStorageList)
                {
                    var dictionaryDomainEvent = JsonConvert.DeserializeObject<Dictionary<string, string>>(item.EventData);
                    foreach (var entry in dictionaryDomainEvent)
                    {
                        var domainEventType = TypeNameProvider.GetType(entry.Key);
                        if (domainEventType != null)
                        {
                            var domainEvent = JsonConvert.DeserializeObject(entry.Value, domainEventType) as IDomainEvent;
                            domainEventList.Add(domainEvent);
                        }
                    }
                }
                return domainEventList;
            }
        }
        catch (Exception ex)
        {
            throw;
        }

注意事項

1.事件沒持久化就代表事件還沒發生成功,事件存儲可能失敗,必須先存儲事件,在發布事件,保證存儲事件與發布事件一致性
1.使用事件驅動,必須要做好冥等的處理
2.如果業務場景中有狀態時:通過狀態來控制
3.新建一張表,用來記錄消費的信息,消費端的代碼裏面,根據唯一的標識,判斷是否處理過該事件
4.Q端的任何更新都應該把聚合根ID和事件版本號作為條件,Q端的更新不用遵循聚合的原則,可以使用最簡單的方式處理
5.倉儲是用來重建聚合的,它的行為和集合一樣只有Get ,Add ,Delete
6.DDD不是技術,是思想,核心在戰略模塊,戰術設計是實現的一種選擇,戰略設計,需要面向對象的分析能力,職責分配,深層次的分析業務

感謝

雖然學習DDD的時間不短了,感覺還是在入門階段,在學習的過程中有許多的不解,經常問ENode群裏面的大佬,也經常@湯總,謝謝大家的幫助與解惑。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Class文件結構全面解析(下)

接上回書

書接,分享了Class文件的主要構成,同時也詳細分析了魔數、次版本號、主版本號、常量池集合、訪問標誌的構造,接下來我們就繼續學習。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

類索引和父類索引

類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類全限定名。由於java語言不允許多重繼承,所以父類索引只有一個。

類索引和父類索引各自指向常量池中類型為CONSTANT_Class_info的類描述符,再通過類描述符中的索引值找到常量池中類型為CONSTANT_Utf8_info的字符串。再來看一下之前的Class文件例子:

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

結合之前javap分析出來的常量池內容:

   #3 = Class         #17        // OneMoreStudy
   #4 = Class         #18        // java/lang/Object
  #17 = Utf8          OneMoreStudy
  #18 = Utf8          java/lang/Object

類索引為0x0003,去常量池裡找索引為3的類描述符,類描述符中的索引為17,再去找索引為17的字符串,就是“OneMoreStudy”。

父類索引為0x0004,去常量池裡找索引為4的類描述符,類描述符中的索引為18,再去常量池裡找索引為18的字符串,就是“java/lang/Object”。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

接口索引集合

接口索引集合(interface)是一組u2類型的數據的集合,由於java語言允許實現多個接口,所以接口索引也有多個,它們按照implements語句后的接口順序從左到右依次排列在接口索引集合中。接口索引集合的第一項數據是接口集合計數值(interfaces_count),表示有多少接口索引。如果該類沒有實現任何接口,那麼該計數值為0,後面的接口索引表不佔任何字節。之前的例子OneMoreStudy類沒有實現任何接口,所以接口集合計數值就是0,如下圖:

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

字段表集合

字段表(field_info)是用來描述接口或類中聲明的變量。包括類級變量(靜態變量)和實例級變量(成員變量),但是不包括在方法內部聲明的局部變量。具體結構如下錶:

類型 名稱 數量 描述
u2 access_flags 1 字段的訪問標誌
u2 name_index 1 字段的簡單名稱索引
u2 descriptor_index 1 字段的描述符索引
u2 attributes_count 1 字段的屬性計數值
attribute_info attributes attributes_count 字段的屬性

字段表中的access_flags,和類的access_flags是非常類似的,但是標識和含義是不一樣的。具體如下錶:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否為final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由編譯器自動產生的
ACC_ENUM 0x4000 字段是否enum

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

這裏提到了簡單名稱、描述符,和全限定名有什麼區別呢?稍微說一下。

簡單名稱是沒有類型和參數修飾的方法或字段名稱,比如OneMoreStudy類中的number字段和plusOne()方法的簡單名稱分別是“number”和“plusOne”。

全限定名是把類全名中的“.”替換成“/”就可以了,比如java.lang.Object類的全限定名就是“java/lang/Object”。

描述符是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。基礎數據類型和無返回的void類型都有一個大寫字母表示,對象類型用字符L加對象的全限定名來表示,如下錶:

標識字符 含義
B 基本類型byte
C 基本類型char
D 基本類型double
F 基本類型float
I 基本類型int
J 基本類型long
S 基本類型short
Z 基本類型boolean
V 特殊類型void
L 對象類型 如 Ljava/lang/Object

對於數組類型,每一維度使用一個前置的“[”字符來描述,比如java.lang.Object[][]的二維數據,就是“[[Ljava/lang/Object”。在描述方法時,按照先參數列表,后返回值的順序描述,參數列表按照嚴格順序放在“()”值中,比如boolean equals(Object anObject),就是“(Ljava/lang/Object)B”。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

再來看一下之前的Class文件例子:

OneMoreStudy類中只有一個字段number,所以字段計數值為0x0001。字段number只被private修飾,沒有其他修飾,所以字段的訪問標誌位為0x0002。字段的簡單名稱索引是0x0005,去常量池中找索引為5的字符串,為“number”。字段的描述符索引為0x0006,去常量池中找索引為6的字符串,為“I”,是基本類型int。以下是常量池相關內容:

   #5 = Utf8          number
   #6 = Utf8          I

字段number的屬性計數值為0x0000,也就是沒有需要額外描述的信息。

字段表集合中不會列出從父類或者父接口中繼承而來的字段,但有可能列出原版Java代碼中沒有的字段,比如在內部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

方法表集合

方法表的結構和字段表的是一樣的,也是依次包括了訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)和屬性表集合(attributes)。具體如下錶:

類型 名稱 數量 描述
u2 access_flags 1 方法的訪問標誌
u2 name_index 1 方法的簡單名稱索引
u2 descriptor_index 1 方法的描述符索引
u2 attributes_count 1 方法的屬性計數值
attribute_info attributes attributes_count 方法的屬性

對於方法的訪問標誌,所有標誌位和取值如下錶:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 方法是否public
ACC_PRIVATE 0x0002 方法是否private
ACC_PROTECTED 0x0004 方法是否protected
ACC_STATIC 0x0008 方法是否static
ACC_FINAL 0x0010 方法是否為final
ACC_SYNCHRONIZED 0x0020 方法是否sychronized
ACC_BRIDGE 0x0040 方法是否是由編譯器產生的橋接方法
ACC_VARARGS 0x0080 方法是否接受不定參數
ACC_NATIVE 0x0100 方法是否為native
ACC_ABSTRACT 0x0400 方法是否為abstract
ACC_STRICT 0x0800 方法是否為strictfp
ACC_SYNTHETIC 0x1000 方法是否由編譯器自動產生

方法中的Java代碼,經過編譯器編程成字節碼指令后,放在方法屬性表集合中一個名為“Code”的屬性里,後面會有更多分享。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

再來看一下之前的Class文件例子:

方法計算值為0x0003,表示集合中有兩個方法(編譯器自動添加的無參構造方法和源碼中的plusOne方法)。第一個方法的訪問標誌是0x0001,表示只有ACC_PUBLIC標誌為true。

名稱索引為0x0007,在常量池中為索引為7的字符串為“ ”,這就是編譯器自動添加的無參構造方法。描述符索引為0x0008,在常量池中為索引為7的字符串為“()V”,方法的屬性計數值為0x0001,表示該方法有1個屬性,屬性名稱索引為0x0009,在常量池中為索引為7的字符串為“Code”。以下是常量池相關內容:

   #7 = Utf8          <init>
   #8 = Utf8          ()V
   #9 = Utf8          Code

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

屬性表集合

屬性表(attribute_info)在前面的分享中出現了幾次,在Class文件、字段表、方法表都可以有自己的屬性表集合,用來描述某些場景下特有的信息。

屬性表不在要求具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以寫入自己定義的屬性信息,Java虛擬機在運行時會忽略掉它不認識的屬性。

我總結了一些比較常見的屬性,如下錶:

屬性名稱 使用位置 含義
Code 方法表 Java代碼編譯成的字節碼指令
ConstantValue 字段表 final關鍵字定義的常量值
Exceptions 方法表 方法拋出的異常
InnerClasses 類文件 內部類列表
LineNumberTable Code屬性 Java源碼的行號與字節碼指定的對應關係
LocalVariableTable Code屬性 方法的局部變量描述
SourceFile 類文件 記錄源文件名稱

對於每個屬性,它的名稱都從常量池中引用一個CONSTANT_Utf8_info類型的常量,而屬性值的結構則是完全自定義的,只需要用一個u4類型來說明屬性值所佔的位數就可以了。具體結構如下:

類型 名稱 數量 含義
u2 attribute_name_index 1 屬性名稱索引
u2 attribute_length 1 屬性值所佔的位數
u1 info attribute_length 屬性值

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

總結

Class文件主要由魔數、次版本號、主版本號、常量池集合、訪問標誌、類索引、父類索引、接口索引集合、字段表集合、方法表集合和屬性表集合組成。隨着JDK版本的不斷升級,Class文件結構也在不斷更新,學習之路,永不止步。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※公開收購3c價格,不怕被賤賣!

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

JSON——IT技術人員都必須要了解的一種數據交換格式

JSON作為目前Web主流的數據交換格式,是每個IT技術人員都必須要了解的一種數據交換格式。尤其是在Ajax和REST技術的大行其道的當今,JSON無疑成為了數據交換格式的首選

今天大家就和豬哥一起來學習一下JSON的相關知識吧!

一、XML

在講JSON之前,我覺得有必要先帶大家了解一下XML(Extensible Markup Language 可擴展標記語言),因為JSON正在慢慢取代XML。

1.XML起源

早期Web發展和負載的數據量並不是很大,所以基本靠HTML(1989誕生)可以解決。但是隨着Web應用的不斷壯大,HTML的一些缺點也慢慢顯現,如:可讀性差、解析時間長、數據描述性差等。

1998年2月10日,W3C(World WideⅥiebConsortium,萬維網聯盟)公布XML 1.0標準,XML誕生了。

XML使用一個簡單而又靈活的標準格式,為基於Web的應用提供了一個描述數據和交換數據的有效手段。但是,XML並非是用來取代HTML的。HTML着重如何描述將文件显示在瀏覽器中,它着重描述如何將數據以結構化方式表示。

XML簡單易於在任何應用程序中讀/寫數據,這使XML很快成為數據交換的唯一公共語言,所以XML被廣泛應用。

注意: XML是一種數據交換的格式,並不是編程語言。而且他是跨語言的數據格式,目前絕大多數編程語言均支持XML。

2.XML實例

XML究竟怎麼用?是什麼樣子的?我們來舉一個簡單的例子吧!

A公司要和B公司業務對接(A公司要獲取B公司的用戶基本信息),B公司提供接口讓A公司調用,A、B公司對接的開發人員會提前溝通好這個接口的:URL、傳參、返回數據、異常等等。

但是也許兩個公司使用的技術棧並不相同,所以支持的據格式也可能不同。為了解決因技術棧不同帶來的數據格式不同問題,A、B公司的開發協商使用一種通用的數據格式來傳輸,於是他們想到了XML。

  1. 假設現在A公司需要名稱叫pig的用戶信息,於是A公司調用B公司的接口,並傳參數name=pig。
  2. 然後B公司接口收到請求后,將用戶信息從數據庫拿出來,然後封裝成下面的XML格式,然後再返回給A公司。
  3. 最後A公司收到返回后,使用XML庫解析數據即可
<?xml version="1.0" encoding="UTF-8"?>
<person>
  <name>pig</name>
  <age>18</age>
  <sex>man</sex>
  <hometown>
    <province>江西省</province>
    <city>撫州市</city>
    <county>崇仁縣</county>
  </hometown>
</person>

3.XML十字路口

雖然XML標準本身簡單,但與XML相關的標準卻種類繁多,W3C制定的相關標準就有二十多個,採用XML制定的重要的电子商務標準就有十多個。這給軟件開發工程師帶來了極大的麻煩!

隨着AJax(之前叫XMLHTTP,2005年後才叫Ajax)技術的流行,XML的弊端也越來越顯現:大家都知道XML實現是基於DOM樹實現的,而DOM在各種瀏覽器中的實現細節不盡相同,所以XML的跨瀏覽器兼容性並不好,所以急需一種新的數據負載格式集成到HTML頁面中以滿足Ajax的要求!

二、JSON

前面我們說了隨着Ajax的流行,而各種瀏覽器對DOM的實現細節不盡相同,所以會出現兼容性問題,這對前端開發同學來講真的是災難。因為一個功能可能需要用代碼去兼容各種不同的瀏覽器,還要調試,工作量巨大。

1.JSON誕生

如何才能將數據整合到HTML中又解決瀏覽器兼容性問題呢?答案就是:利用所有主流瀏覽器中的一種通用組件——JavaScript引擎。這樣只要創造一種JavaScript引擎能識別的數據格式就可以啦!

2001 年 4 月,首個 JSON 格式的消息被發送出來。此消息是從舊金山灣區某車庫的一台計算機發出的,這是計算機歷史上重要的的時刻。道格拉斯·克羅克福特(Douglas Crockford) 和 奇普·莫寧斯達(Chip Morningstar) 是一家名為 State Software 的技術諮詢公司的聯合創始人(後來都在雅虎任職),他們當時聚集在 Morningstar 的車庫里測試某個想法,發出了此消息。

document.domain = 'fudco'; 

parent.session.receive( 
    { to: "session", do: "test", text: "Hello world" } 
) 

熟悉js的同學是不是也很驚訝,第一個 JSON 消息它明顯就是 JavaScript!實際上,Crockford 自己也說過他不是第一個這樣做的人。網景(Netscape )公司的某人早在 1996 年就使用 JavaScript 數組字面量來交換信息。因為消息就是 JavaScript,其不需要任何特殊解析工作,JavaScript 解釋器就可搞定一切。

最初的 JSON 信息實際上與 JavaScript 解釋器發生了衝突。JavaScript 保留了大量的關鍵字(ECMAScript 6 版本就有 64 個保留字),Crockford 和 Morningstar 無意中在其 JSON 中使用了一個保留字:do。因為 JavaScript 使用的保留字太多了,所以Crockford決定:既然不可避免的要使用到這些保留字,那就要求所有的 JSON 鍵名都加上引號。被引起來的鍵名會被 JavaScript 解釋器識別成字符串。這就為什麼今天 JSON 鍵名都要用引號引起來的原因。

這種數據格式既然可以被JavaScript引擎識別,那就解決了XML帶來的各種瀏覽器兼容性問題,所以這種技術完全可以推廣出去,於是Crockford 和 Morningstar 想給其命名為 “JSML”,表示JavaScript 標記語言(JavaScript Markup Language)的意思,但發現這個縮寫已經被一個名為 Java Speech 標記語言的東西所使用了。所以他們決定採用 “JavaScript Object Notation”,縮寫為 JSON,至此JSON正式誕生。

2.JSON發展

2005 年,JSON 有了一次大爆發。那一年,一位名叫 Jesse James Garrett 的網頁設計師和開發者在博客文章中創造了 “AJAX” 一詞。他很謹慎地強調:AJAX 並不是新技術,而是 “好幾種蓬勃發展的技術以某種強大的新方式彙集在一起。” AJAX 是 Garrett 給這種正受到青睞的 Web 應用程序的新開發方法的命名。他的博客文章接着描述了開發人員如何利用 JavaScript 和 XMLHttpRequest 構建新型應用程序,這些應用程序比傳統的網頁更具響應性和狀態性。他還以 Gmail 和 Flickr 網站已經使用 AJAX 技術作為了例子。

當然了,“AJAX” 中的 “X” 代表 XML。但在隨後的問答帖子中,Garrett 指出,JSON 可以完全替代 XML。他寫道:“雖然 XML 是 AJAX 客戶端進行數據輸入、輸出的最完善的技術,但要實現同樣的效果,也可以使用像 JavaScript Object Notation(JSON)或任何類似的結構數據方法等技術。 ”

這時JSON便在國外的博客圈、技術圈慢慢流行起來!

2006 年,Dave Winer,一位高產的博主,他也是許多基於 XML 的技術(如 RSS 和 XML-RPC)背後的開發工程師,他抱怨到 JSON 毫無疑問的正在重新發明 XML。

Crockford 閱讀了 Winer 的這篇文章並留下了評論。為了回應 JSON 重新發明 XML 的指責,Crockford 寫到:“重造輪子的好處是可以得到一個更好的輪子”。

3.JSON實例

還是以上面A、B公司業務對接為例子,兩邊的開發人員協商一種通用的數據交換格式,現在有XML與JSON比較流行的兩種數據格式,於是開發人員又將用戶信息以JSON形式展現出來,然後比較兩種數據格式:

{
  "person": {
    "name": "pig",
    "age": "18",
    "sex": "man",
    "hometown": {
      "province": "江西省",
      "city": "撫州市",
      "county": "崇仁縣"
    }
  }
}

比較XML與JSON的數據格式之後,開發人員發現:JSON可閱讀性、簡易性更好而且相同數據負載JSON字符數更少,所以兩個開發人員一致同意使用JSON作為接口數據格式!

而且還有重要的一點,在編寫XML時,第一行需要定義XML的版本,而JSON不存在版本問題,格式永遠不變!

4.當今JSON地位

當今的JSON 已經佔領了全世界。絕大多數的應用程序彼此通過互聯網通信時,都在使用 JSON。它已被所有大型企業所採用:十大最受歡迎的 web API 接口列表中(主要由 Google、Facebook 和 Twitter 提供),僅僅只有一個 API 接口是以 XML 的格式開放數據的。

JSON 也在程序編碼級別和文件存儲上被廣泛採用:在 Stack Overflow上,關於JSON的問題越來越多,下圖是關於Stack Overflow上不同數據交換格式的問題數和時間的曲線關係圖。
從上圖我們可以看出在Stack Overflow上越來越多JSON的問題,從這裏也可以反映出JSON越來越流行!

更詳細的關於創造JSON的故事可閱讀:

3、總結

由於篇幅原因我們今天只學習了JSON的誕生和起源相關知識,知道了JSON的誕生是因為XML無法滿足Ajax對瀏覽器兼容性問題,所以就有人想創造一種瀏覽器通用組件:JavaScript引擎 能識別的數據格式,這樣就可以解決瀏覽器不兼容問題,所以就從Js數據格式中提取了一個子集,取名為JSON!

我們還知道了為什麼JSON鍵為什麼需要用雙引號引起來,是因為JS中存在許多的關鍵字和保留關鍵字,為了避免與JS關鍵字衝突,所以Crockford就要求在所有的鍵名上加上雙引號,這樣JS引擎會將其識別為字符串,就避免與JS中關鍵字衝突!

下期我們會詳細介紹JSON數據結構、JSON序列化、JSON在Python中的使用等知識。

了解技術誕生與發展背後的故事同樣重要,因為這些可以作為你吹逼的資本!

參考資料:
百度百科:XML
Daniel Rubio:JSON 簡介

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

J.U.C剖析與解讀1(Lock的實現)

J.U.C剖析與解讀1(Lock的實現)

前言

為了節省各位的時間,我簡單介紹一下這篇文章。這篇文章主要分為三塊:Lock的實現,AQS的由來(通過演變的方式),JUC三大工具類的使用與原理剖析。

  • Lock的實現:簡單介紹ReentrantLock,ReentrantReadWriteLock兩種JUC下經典Lock的實現,並通過手寫簡化版的ReentrantLock和ReentrantReadWriteLock,從而了解其實現原理。

  • AQS的由來:通過對兩個簡化版Lock的多次迭代,從而獲得AQS。並且最終的Lock實現了J.U.C下Lock接口,既可以使用我們演變出來的AQS,也可以對接JUC下的AQS。這樣一方面可以幫助大家理解AQS,另一方面大家可以從中了解,如何利用AQS實現自定義Lock。而這兒,對後續JUC下的三大Lock工具的理解有非常大的幫助。

  • JUC三大工具:經過前兩個部分的學習,這個部分不要太easy。可以很容易地理解CountDownLatch,Semaphore,CyclicBarrier的內部運行及實現原理。

不過,由於這三塊內容較多,所以我將它拆分為三篇子文章進行論述。

一,介紹

Lock

Lock接口位於J.U.C下locks包內,其定義了Lock應該具備的方法。

Lock 方法簽名:

  • void lock():獲取鎖(不死不休,拿不到就一直等)
  • boolean tryLock():獲取鎖(淺嘗輒止,拿不到就算了)
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:獲取鎖(過時不候,在一定時間內拿不到鎖,就算了)
  • void lockInterruptibly() throws InterruptedException:獲取鎖(任人擺布,xxx)
  • void unlock():釋放鎖
  • Condition newCondition():獲得Condition對象

ReentrantLock

簡介

ReentrantLock是一個可重入鎖,一個悲觀鎖,默認是非公平鎖(但是可以通過Constructor設置為公平鎖)。

Lock應用

ReentrantLock通過構造方法獲得lock對象。利用lock.lock()方法對當前線程進行加鎖操作,利用lock.unlock()方法對當前線程進行釋放鎖操作。

Condition應用

通過


    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

獲得Condition對象(Condition是J.U.C下locks包下的接口)。

通過Condition對象的.await(*),可以將當前線程的線程狀態切換到Waiting狀態(如果是有參,則是Time Waiting狀態)。而.signal(),.signalAll()等方法則正好相反,恢複線程狀態為Runnable狀態。

ReentrantReadWriteLock

簡介

ReentrantLock和Synchronized功能類似,更加靈活,當然,也更加手動了。

大家都知道,只有涉及資源的競爭時,採用同步的必要。寫操作自然屬於資源的競爭,但是讀操作並不屬於資源的競爭行為。簡單說,就是寫操作最多只能一個線程(因為寫操作涉及數據改變,多個線程同時寫,會產生資源同步問題),而讀操作可以有多個(因為不涉及數據改變)。

所以在讀多寫少的場景下,ReentrantLock就比較浪費資源了。這就需要一種能夠區分讀寫操作的鎖,那就是ReentrantReadWriteLock。通過ReentrantReadWriteLock,可以獲得讀鎖與寫鎖。當寫鎖存在時,有且只能有一個線程持有鎖。當寫鎖不存在時,可以有多個線程持有讀鎖(寫鎖,必須等待讀鎖釋放完,才可以持有鎖)。

Lock及Condition應用


        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        readLock.lock();
        readLock.unlock();

        readLock.newCondition();

        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        writeLock.lock();
        writeLock.unlock();

        writeLock.newCondition();

與之前ReentrantLock應用的區別,就是需要通過lock.readLock()與lock.writeLock()來獲取讀鎖,寫鎖,再進行加鎖,釋放鎖的操作,以及Condition的獲取操作。

二,手寫ReentrantLock

獲取需求

終於上大餐了。

首先第一步操作,我們需要確定我們要做什麼。

我們要做一個鎖,這裏姑且命名為JarryReentrantLock。

這個鎖,需要具備以下特性:可重入鎖,悲觀鎖。

另外,為了更加規範,以後更好地融入到AQS中,該鎖需要實現Lock接口。

而Lock的方法簽名,在文章一開始,就已經寫了,這裏不再贅述。

當然,我們這裏只是一個demo,所以就不實現Condition了。另外tryLock(long,TimeUnit)也不再實現,因為實現了整體后,這個實現其實並沒有想象中那麼困難。

JarryReentrantLock實現原理

既然需要已經確定,並且API也確定了。

那麼第二步操作,就是簡單思考一下,如何實現。

類成員方面:

  1. 首先,我們需要一個owner屬性,來保存持有鎖的線程對象。

  2. 其次,由於是可重入鎖,所以我們需要一個count來保存重入次數。

  3. 最後,我們需要一個waiters屬性,來保存那些競爭鎖失敗后,還在等待(不死不休型)的線程對象。

類方法方面:

  • tryLock:嘗試獲取鎖,成功返回true,失敗返回false。首先是獲取鎖的行為,可以通過CAS操作實現,或者更簡單一些,通過Atomic包實現(其底層也還是CAS)。另外,由於是可重入鎖,所以在嘗試獲取鎖時,需要判斷嘗試獲取鎖的線程是否為當前鎖的持有者線程。
  • lock:嘗試獲取鎖,直到成功獲得鎖。看到這種不成功便成仁的精神,我第一個想法是循環調用tryLock。但是這實在太浪費資源了(畢竟長時間的忙循環是非常消耗CPU資源的)。所以就是手動通過LockSupport.park()將當前線程掛起,然後置入等待隊列waiters中,直到釋放鎖操作來調用。
  • tryUnlock:嘗試解鎖,成功返回true,失敗返回false。首先就是在釋放鎖前,需要判斷嘗試解鎖的線程與鎖的持有者是否為同一個線程(總不能線程A把線程B持有的鎖給釋放了吧)。其次,需要判斷可重入次數count是否為0,從而決定是否將鎖的持有owner設置為null。最後,就是為了避免在count=0時,其他線程同時進行加鎖操作,造成的count>0,owner=null的情況,所以count必須是Atomic,並此處必須採用CAS操作(這裡有些難理解,可以看代碼,有相關註釋)。
  • unlock:解鎖操作。這裏嘗試進行解鎖,如果解鎖成功,需要從等待隊列waiters中喚醒一個線程(喚醒后的線程,由於在循環中,所以會繼續進行競爭鎖操作。但是切記該線程不一定競爭鎖成功,因為可能有新來的線程,搶先一步。那麼該線程會重新進入隊列。所以,此時的JarryReentrantLock只支持不公平鎖)。

JarryReentrantLock實現

那麼接下來,就根據之前的信息,進行編碼吧。


    package tech.jarry.learning.netease;
    
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description: 仿ReentrantLock,實現其基本功能及特性
     * @Author: jarry
     */
    public class JarryReentrantLock implements Lock {
    
        // 加鎖計數器
        private AtomicInteger count = new AtomicInteger(0);
        // 鎖持有者
        private AtomicReference<Thread> owner = new AtomicReference<>();
        // 等待池
        private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
    
    
        @Override
        public boolean tryLock() {
            // 判斷當前count是否為0
            int countValue = count.get();
            if (countValue != 0){
                // countValue不為0,意味着鎖被線程持有
                // 進而判斷鎖的持有者owner是否為當前線程
                if (Thread.currentThread() == owner.get()){
                    // 鎖的持有者為當前線程,那麼就重入加鎖
                    // 既然鎖已經被當前線程佔有,那麼就不用擔心count被其他線程修改,即不需要使用CAS
                    count.set(countValue+1);
                    // 執行重入鎖,表示當前線程獲得了鎖
                    return true;
                }else{
                    // 如果當前線程不是鎖的持有者,返回false(該方法是tryLock,即淺嘗輒止)
                    return false;
                }
            }else {
                // countValue為0,意味着當前鎖不被任何線程持有
                // 通過CAS操作將count修改為1
                if (count.compareAndSet(countValue,countValue+1)){
                    // count修改成功,意味着該線程獲得了鎖(只有一個CAS成功修改count,那麼這個CAS的線程就是鎖的持有者)
                    // 至於這裏為什麼不用擔心可見性,其實一開始我也比較擔心其發生類似doubleCheck中重排序造成的問題(tryUnlock是會設置null的)
                    // 看了下源碼,AtomicReference中的value是volatile的
                    owner.set(Thread.currentThread());
                    return true;
                } else {
                    // CAS操作失敗,表示當前線程沒有成功修改count,即獲取鎖失敗
                    return false;
                }
            }
        }
    
        @Override
        public void lock() {
            // lock()【不死不休型】就等於執行tryLock()失敗后,仍然不斷嘗試獲取鎖
            if (!tryLock()){
                // 嘗試獲取鎖失敗后,就只能進入等待隊列waiers,等待機會,繼續tryLock()
                waiters.offer(Thread.currentThread());
    
                // 通過自旋,不斷嘗試獲取鎖
                // 其實我一開始也不是很理解為什麼這樣寫,就可以確保每個執行lock()的線程就在一直競爭鎖。其實,想一想執行lock()的線程都有這個循環。
                // 每次unlock,都會將等待隊列的頭部喚醒(unpark),那麼處在等待隊列頭部的線程就會繼續嘗試獲取鎖,等待隊列的其它線程仍然,繼續阻塞(park)
                // 這也是為什麼需要在循環體中執行一個檢測當前線程是否為等待隊列頭元素等一系列操作。
                // 另外,還有就是:處於等待狀態的線程可能收到錯誤警報和偽喚醒,如果不在循環中檢測等待條件,程序就會在沒有滿足結束條件的情況下退出。反正最後無論那個分支,都return,結束方法了。
                // 即使沒有偽喚醒問題,while還是需要的,因為線程需要二次嘗試獲得鎖
                while (true){
                    // 獲取等待隊列waiters的頭元素(peek表示獲取頭元素,但不刪除。poll表示獲取頭元素,並刪除其在隊列中的位置)
                    Thread head = waiters.peek();
                    // 如果當前線程就是等待隊列中的頭元素head,說明當前等待隊列就剛剛加入的元素。
                    if (head == Thread.currentThread()){
                        // 嘗試再次獲得鎖
                        if (!tryLock()){
                            // 再次嘗試獲取鎖失敗,即將該線程(即當前線程)掛起,
                            LockSupport.park();
                        } else {
                            // 獲取鎖成功,即將該線程(等待隊列的頭元素)從等待隊列waiters中移除
                            waiters.poll();
                            return;
                        }
                    } else {
                        // 如果等待隊列的頭元素head,不是當前線程,表示等待隊列在當前線程加入前,就還有別的線程在等待
                        LockSupport.park();
                    }
                }
            }
        }
    
        private boolean tryUnlock() {
            // 首先確定當前線程是否為鎖持有者
            if (Thread.currentThread() != owner.get()){
                // 如果當前線程不是鎖的持有者,就拋出一個異常
                throw new IllegalMonitorStateException();
            } else {
                // 如果當前線程是鎖的持有者,就先count-1
                // 另外,同一時間執行解鎖的只可能是鎖的持有者線程,故不用擔心原子性問題(原子性問題只有在多線程情況下討論,才有意義)
                int countValue = count.get();
                int countNextValue = countValue - 1;
                count.compareAndSet(countValue,countNextValue);
                if (countNextValue == 0){
                    // 如果當前count為0,意味着鎖的持有者已經完全解鎖成功,故應當失去鎖的持有(即設置owner為null)
                    // 其實我一開始挺糾結的,這裏為什麼需要使用CAS操作呢。反正只有當前線程才可以走到程序這裏。
                    // 首先,為什麼使用CAS。由於count已經設置為0,其它線程已經可以修改count,修改owner了。所以不用CAS就可能將owner=otherThread設置為owner=null了,最終的結果就是徹底卡死
                    //TODO_FINISHED 但是unlock()中的unpark未執行,根本就不會有其它線程啊。囧
                    // 這裏代碼還是為了體現源碼的一些特性。實際源碼是將這些所的特性,抽象到了更高的層次,形成一個AQS。
                    // 雖然tryUnlock是由實現子類實現,但countNextValue是來自countValue(而放在JarryReadWriteLock中就是writeCount),在AQS源碼中,則是通過state實現
    
                    // 其次,有沒有ABA問題。由於ABA需要將CAS的expect值修改為currentThread,而當前線程只能單線程執行,所以不會。
                    // 最後,這裏owner設置為null的操作到底需不需要。實際源碼可能是需要的,但是這裏貌似真的不需要。
                    owner.compareAndSet(Thread.currentThread(),null);
                    // 解鎖成功
                    return true;
                } else {
                    // count不為0,解鎖尚未完全完成
                    return false;
                }
            }
        }
    
        @Override
        public void unlock() {
            if (tryUnlock()){
                // 如果當前線程成功tryUnlock,就表示當前鎖被空置出來了。那就需要從備胎中,啊呸,從waiters中“放“出來一個
                Thread head = waiters.peek();
                // 這裏需要做一個簡單的判斷,防止waiters為空時,拋出異常
                if (head != null){
                    LockSupport.unpark(head);
                }
            }
        }
    
    
        // 非核心功能就不實現了,起碼現在不實現了。
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
    
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }
    
        @Override
        public Condition newCondition() {
            return null;
        }
    }

這裏就不進行一些解釋了。因為需要的解釋,在註釋中都寫的很明確了,包括我踩的一些坑。

如果依舊有一些看不懂的地方,或者錯誤的地方,歡迎@我,或者私信我。

三,手寫ReentrantReadWriteLock

獲取需求

與ReentrantLock一樣,首先第一步操作,我們需要確定我們要做什麼。

我們要做一個鎖,這裏姑且命名為JarryReadWriteLock。

這個鎖,需要具備以下特性:讀寫鎖,可重入鎖,悲觀鎖。

一方面了為了更好理解(第一版本,重在理解基礎,不是嘛),另一方面也是為了更好地復用前面ReentrantLock的代碼(畢竟ReentrantLock其實就是讀寫鎖的寫鎖,不是嘛),這裏的JarryReadWriteLock的API不再與官方的ReentrantReadWriteLock相同,而是做了小小調整。直接調用相關讀鎖的加解鎖API,已經相關寫鎖的加解鎖API。具體看代碼部分。

JarryReadWriteLock實現原理

既然需要已經確定,並且API也確定了。

那麼第二步操作,就是簡單思考一下,如何實現。

類成員方面:

  1. 首先,我們需要一個owner屬性,來保存持有寫鎖的線程對象。

  2. 其次,由於寫鎖是可重入鎖,所以我們需要一個readCount來保存重入次數。

  3. 然後,由於讀鎖是可以有多個線程持有的,所以我們需要一個writeCount來保存讀鎖持有線程數。

  4. 最後,我們需要一個waiters屬性,來保存那些競爭鎖失敗后,還在等待(不死不休型)的線程對象。

自定義數據結構:

到這這裏,就不禁會有一個疑問。如何判斷嘗試獲取鎖的線程想要獲得的鎖是什麼類型的鎖。在API調用階段,我們可以根據API判斷。但是放入等待隊列后,我們如何判斷呢?如果還是如之前那樣,等待隊列只是保存競爭鎖的線程對象,是完全不夠的。

所以我們需要新建一個WaitNode的Class,用來保存等待隊列中線程對象及相關必要信息。所以,WaitNode會有如下屬性:

  • Thread thread:標識該等待者的線程。
  • int type:標識該線程對象希望競爭的鎖的類型。0表示寫鎖(獨佔鎖),1表示讀鎖(共享鎖)。
  • int arg:擴展參數。其實在手寫的簡易版,看不出來價值。但是實際AQS中的Node就是類似設計。不過AQS中,並不是採用queue保存Node,而是通過一個鏈表的方式保存Node。

類方法方面:

  • 獨佔鎖:
    • tryLock:與JarryReentrantLock類似,不過增加了兩點。一方面需要考量共享鎖是否被佔用。另一方面需要引入acquire參數(目前是固定值),呼應WaitNode的arg。
    • lock:與JarryReentrantLock類似,不過需要手動設置arg。
    • tryUnlock:與JarryReentrantLock類似,同樣需要引入release參數(目前是固定值),呼應WaitNode的arg。
    • unlock:與JarryReentrantLock類似,不過需要手動設置arg。
  • 共享鎖:
    • tryLockShared:嘗試獲取共享鎖,成功返回true,失敗返回false。其實和獨佔鎖的tryLock類似,只不過需要額外考慮獨佔鎖是否已經存在。另外為了實現鎖降級,如果獨佔鎖存在,需要判斷獨佔鎖的持有者與當前嘗試獲得共享鎖的線程是否一致。
    • lockShared:獲取共享鎖,直到成功。由於已經有了WaitNode.type,用於判斷鎖類型,所以共享鎖與獨佔鎖使用的是同一隊列。同樣的,這裏需要手動設置arg。其它方面與獨佔鎖的lock操作基本一致。
    • tryUnlockShared:嘗試釋放鎖,成功返回true,失敗返回false。類似於tryUnlock,只不過增加了release參數(固定值),呼應WaitNode的arg。
    • unlockShared:釋放鎖。類似unlock,不過需要手動設置arg。

JarryReentrantLock實現


    package tech.jarry.learning.netease;
    
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class JarryReadWriteLock {
    
        // 用於讀鎖(共享鎖)的鎖計數器   這裏真的有必要volatile嘛(Atomic中的value時volatile的),再看看後續代碼
        // 這裏確實不需要volatile,至於源碼,更過分,源碼是通過一個變量state的位運算實現readCount與writeCount
        volatile AtomicInteger readCount = new AtomicInteger(0);
        // 用於寫鎖(獨佔鎖)的鎖計數器   這裏之所以不用volatile是因為獨佔鎖,只有一個線程在改變writeCount(即使有緩存,也還是這個線程,所以不會因為緩存問題,導致問題)
        AtomicInteger writeCount = new AtomicInteger(0);
        // 用於保存鎖的持有者(這裏專指寫鎖(獨佔鎖)的鎖持有者)
        AtomicReference<Thread> owner = new AtomicReference<>();
        // 用於保存期望獲得鎖的線程(為了區分線程希望獲得的鎖的類型,這裏新建一個新的數據類型(通過內部類實現))
        public volatile LinkedBlockingQueue<WaitNode> waiters = new LinkedBlockingQueue<>();
    
        // 內部類實現等待隊列中的自定義數據類型
        class WaitNode{
            // 表示該等待者的線程
            Thread thread = null;
            // 表示希望爭取的鎖的類型。0表示寫鎖(獨佔鎖),1表示讀鎖(共享鎖)
            int type = 0;
            // 參數,acquire,狀態相關,再看看
            int arg = 0;
    
            public WaitNode(Thread thread, int type, int arg) {
                this.type = type;
                this.thread = thread;
                this.arg = arg;
            }
        }
    
        /**
         * 嘗試獲取獨佔鎖(針對獨佔鎖)
         * @param acquires 用於加鎖次數。一般傳入waitNode.arg(本代碼中就是1。為什麼不用一個常量1,就不知道了?)(可以更好的對接AQS)
         * @return
         */
        public boolean tryLock(int acquires){
            //TODO_FINISHED 這裏readCount的判斷,與修改writeCount的操作可以被割裂,並不是原子性的。不就有可能出現readCount與writeCount的值同時大於零的情況。
            // 該示例代碼,確實存在該問題,但實際源碼,writeCount與readCount是通過同一變量state實現的,所以可以很好地通過CAS確保原子性
    
            // readCount表示讀鎖(共享鎖)的上鎖次數
            if (readCount.get() == 0){
                // readCount的值為0,表示讀鎖(共享鎖)空置,所以當前線程是有可能獲得寫鎖(獨佔鎖)。
                // 接下來判斷寫鎖(獨佔鎖)是否被佔用
                int writeCountValue = writeCount.get();
                if (writeCountValue == 0){
                    // 寫鎖(獨佔鎖)的鎖次數為0,表示寫鎖(獨佔鎖)並沒未被任何線程持有
                    if (writeCount.compareAndSet(writeCountValue,writeCountValue+acquires)){
                        // 修改writeCount,來獲得鎖。該機制與ReentrantLock相同
                        // 設置獨享鎖的持有者owner
                        owner.set(Thread.currentThread());
                        // 至此,表示當前線程搶鎖成功
                        return true;
                    }
                } else {
                    // 寫鎖(獨佔鎖)的鎖次數不為0,表示寫鎖(獨佔鎖)已經被某線程持有
                    if (Thread.currentThread() == owner.get()){
                        // 如果持有鎖的線程為當前線程,那就進行鎖的重入操作
                        writeCount.set(writeCountValue+acquires);
                        // 重入鎖,表示當前線程是持有鎖的
                        return true;
                    }
                    // 讀鎖未被佔用,但寫鎖被佔用,且佔據寫鎖的線程不是當前線程
                }
            }
            // 讀鎖被佔據
            // 其它情況(1.讀鎖被佔據,2讀鎖未被佔用,但寫鎖被佔用,且佔據寫鎖的線程不是當前線程),都返回false
            return false;
        }
    
        /**
         * 獲取獨佔鎖(針對獨佔鎖)
         */
        public void lock(){
            // 設定waitNote中arg參數
            int arg = 1;
            // 嘗試獲取獨佔鎖。成功便退出方法,失敗,則進入“不死不休”邏輯
            if (!tryLock(arg)){
                // 需要將當前保存至等待隊列,在這之前,需要封裝當前線程為waitNote
                WaitNode waitNode = new WaitNode(Thread.currentThread(), 0, arg);
                // 將封裝好的waitNode放入等待隊列waiters中(offer方法會在隊列滿時,直接返回false。put則是阻塞。add則是拋出異常)
                waiters.offer(waitNode);
    
                // 如ReentrantLock一般,開始循環嘗試拿鎖
                while (true){
                    // 獲取隊列頭部元素
                    WaitNode headNote = waiters.peek();
                    // 如果等待隊列頭部元素headNote不為null(有可能是null嘛?),並且就是當前線程,那就嘗試獲取鎖
                    if (headNote !=null && headNote.thread == Thread.currentThread()){
                        // 如果再次嘗試獲取鎖失敗,那就只能掛起了
                        if (!tryLock(headNote.arg)){
                            LockSupport.park();
                        } else {
                            // 再次嘗試獲取鎖成功,那就將隊列頭部元素,踢出等待隊列waiters
                            waiters.poll();
                            return;
                        }
                    }else {
                        // 如果headNote不是當前線程的封裝,就直接掛起(這裏就沒處理headNote==null的情況)
                        LockSupport.park();
                    }
                }
            }
        }
    
        /**
         * 嘗試解鎖(針對獨佔鎖)
         * @param releases 用於設定解鎖次數。一般傳入waitNode.arg
         * @return
         */
        public boolean tryUnlock(int releases){
            // 首先判斷鎖的持有者是否為當前線程
            if (owner.get() != Thread.currentThread()){
                // 鎖的持有者不是當前線程(即使鎖的持有者為null,鎖的持有者是null,還解鎖,仍然是拋出異常)
                throw new IllegalMonitorStateException();
            }
            // 鎖的持有者就是當前線程
            // 首先按照releases進行解鎖(經過一番思考後,這裏不會出現類似DoubleCheck中的問題(Atomic中的value是volatile的),所以這個值同時只會有一個線程對其操作)
            int writeCountValue = writeCount.get();
            // 為writeCount設置新值
            writeCount.set(writeCountValue-releases);
            // 根據writeCount的新值,判斷鎖的持有者是否發生變化
            if (writeCount.get() == 0){
                // writeCount的值為0,表示當前線程已經完全解鎖,所以修改鎖的持有者為null
                owner.set(null);
                // 而這表示完全解鎖成功
                return true;
            } else {
                // writeCount的值不為0,表示當前線程尚未完全解鎖,故鎖的持有者未發生變化。即嘗試解鎖失敗
                return false;
            }
        }
    
        /**
         * 解鎖(針對獨佔鎖)
         */
        public void unlock(){
            // 設定tryUnlock的參數releases
            int arg = 1;
            // 先嘗試解鎖
            if (tryUnlock(arg)){
                // 獲得等待隊列的頭部元素
                WaitNode head = waiters.peek();
                // 檢測一下頭部元素head是否null(也許等待隊列根本就沒有元素)
                if (head == null){
                    // 如果頭部元素head為null,說明隊列為null,直接return
                    return;
                }
                // 解鎖成功,就要把等待隊列中的頭部元素喚醒(unpark)
                // 這裡有一點注意,即使隊列的頭元素head被喚醒了,也不一定就是這個頭元素head獲得鎖(詳見tryLock,新來的線程可能獲得鎖)
                // 如果這個頭元素無法獲得鎖,就會park(while循環嘛)。並且一次park,可以多次unpark(已實踐)
                LockSupport.unpark(head.thread);
            }
        }
    
        /**
         * 嘗試獲取共享鎖(針對共享鎖)
         * @param acquires
         * @return
         */
        public boolean tryLockShared(int acquires){
            // 判斷寫鎖(獨佔鎖)是否被別的線程持有(這個條件意味着:同一個線程可以同時持有讀鎖與寫鎖)
            // 該方法是為了進行  鎖降級******
            if (writeCount.get() == 0 || owner.get() == Thread.currentThread()){
                // 如果寫鎖(獨佔鎖)沒有別的被線程持有,就可以繼續嘗試獲取讀鎖(共享鎖)
                // 通過循環實現自旋,從而實現加鎖(避免加鎖失敗)
                while(true){
                    // 由於讀鎖(共享鎖)是共享的,不存在獨佔行為,故直接在writeCount增加當前線程加鎖行為的次數acquires
                    int writeCountValue = writeCount.get();
                    // 通過CAS進行共享鎖的次數的增加
                    if (writeCount.compareAndSet(writeCountValue, writeCountValue+acquires)){
                        break;
                    }
                }
            }
            // 寫鎖已經被別的線程持有,共享鎖獲取失敗
            return false;
        }
    
        /**
         * 獲取共享鎖(針對共享鎖)
         */
        public void lockShared(){
            // 設定waitNote中arg參數
            int arg = 1;
            // 判斷是否獲取共享鎖成功
            if (!tryLockShared(arg)){
                // 如果獲取共享鎖失敗,就進入等待隊列
                // 與獲取同步鎖操作一樣的,需要先對當前線程進行WaitNote的封裝
                WaitNode waitNode = new WaitNode(Thread.currentThread(),1,arg);
                // 將waitNote置入waiters(offer方法會在隊列滿時,直接返回false。put則是阻塞。add則是拋出異常)
                waiters.offer(waitNode);
    
                // 使用循環。一方面避免偽喚醒,另一方面便於二次嘗試獲取鎖
                while (true){
                    // 獲取等待隊列waiters的頭元素head
                    WaitNode head = waiters.peek();
                    // 校驗head是否為null,並判斷等待隊列的頭元素head是否為當前線程的封裝(也許head時當前線程的封裝,但並不意味着head就是剛剛放入waiters的元素)
                    if (head != null && head.thread == Thread.currentThread()){
                        // 如果校驗通過,並且等待隊列的頭元素head為當前線程的封裝,就再次嘗試獲取鎖
                        if (tryLockShared(head.arg)){
                            // 獲取共享鎖成功,就從當前隊列中移除head元素(poll()方法移除隊列頭部元素)
                            waiters.poll();
    
                            // 在此處就是與獨佔鎖不同的地方了,獨佔鎖意味着只可能有一個線程獲得鎖,而共享鎖是可以有多個線程獲得的
                            // 獲得等待隊列的新頭元素newHead
                            WaitNode newHead = waiters.peek();
                            // 校驗該元素是否為null,並判斷它的鎖類型是否為共享鎖
                            if (newHead != null && newHead.type == 1){
                                // 如果等待隊列的新頭元素是爭取共享鎖的,那麼就喚醒它(這是一個類似迭代的過程,剛喚醒的線程會會做出同樣的舉動)
                                //TODO_FINISHED 這裡有一點,我有些疑惑,那麼如果等待隊列是這樣的{共享鎖,共享鎖,獨佔鎖,共享鎖,共享鎖},共享鎖們被一個獨佔鎖隔開了。是不是就不能喚醒後面的共享鎖了。再看看後面的代碼
                                // 這個實際源碼,並不是這樣的。老師表示現有代碼是這樣的,不用理解那麼深入,後續有機會看看源碼
                                LockSupport.unpark(newHead.thread);
                            }
                        } else {
                            // 如果再次獲取共享鎖失敗,就掛起
                            LockSupport.park();
                        }
                    } else {
                        // 如果校驗未通過,或等待隊列的頭元素head不是當前線程的封裝,就掛起當前線程
                        LockSupport.park();
                    }
                }
            }
        }
    
        /**
         * 嘗試解鎖(針對共享鎖)
         * @param releases
         * @return
         */
        public boolean tryUnlockShared(int releases){
            // 通過CAS操作,減少共享鎖的鎖次數,即readCount的值(由於是共享鎖,所以是可能多個線程同時減少該值的,故採用CAS)
            while (true){
                // 獲取讀鎖(共享鎖)的值
                int readCountValue = readCount.get();
                int readCountNext = readCountValue - releases;
                // 只有成功修改值,才可以跳出
                if (readCount.compareAndSet(readCountValue,readCountNext)){
                    // 用於表明共享鎖完全解鎖成功
                    return readCountNext == 0;
                }
            }
            // 由於讀鎖沒有owner,所以不用進行有關owner的操作
        }
    
        /**
         * 解鎖(針對共享鎖)
         */
        public boolean unlockShared(){
            // 設定tryUnlockShared的參數releases
            int arg = 1;
            // 判斷是否嘗試解鎖成功
            if (tryUnlockShared(arg)){
                // 如果嘗試解鎖成功,就需要喚醒等待隊列的頭元素head的線程
                WaitNode head = waiters.peek();
                // 校驗head是否為null,畢竟可能等待隊列為null
                if (head != null){
                    // 喚醒等待隊列的頭元素head的線程
                    LockSupport.unpark(head.thread);
                }
                //TODO_FINISHED 嘗試共享鎖解鎖成功后,就應當返回true(雖然有些不大理解作用)
                // 用於對應源碼
                return true;
            }
            //TODO_FINISHED 嘗試共享鎖解鎖失敗后,就應當返回false(雖然有些不大理解作用)
            // 用於對應源碼
            return false;
        }
    }

這裏同樣不進行相關解釋了。因為需要的解釋,在註釋中都寫的很明確了,包括我踩的一些坑。

如果依舊有一些看不懂的地方,或者錯誤的地方,歡迎@我,或者私信我。

四,總結

技術

  • CAS:通過CAS實現鎖持有數量等的原子性操作,從而完成鎖的競爭操作。
  • Atomic:為了簡化操作(避免自己獲取Unsafe,offset等),通過Atomic實現CAS 操作。
  • volatile:為了避免多線程下的可見性問題,採用了volatile的no cache特性。
  • transient:可以避免對應變量序列化,源碼中有採用。不過考慮后,並沒有使用。
  • while:一方面通過while避免偽喚醒問題,另一方面,通過while推動流程(這個需要看代碼)。
  • LinkedBlockingQueue:實現線程等待隊列。實際的AQS是通過Node構成鏈表結構的。
  • LockSupport:通過LockSupport實現線程的掛起,喚醒等操作。
  • IllegalMonitorStateException:就是一個異常類型,仿Synchronized的,起碼看起來更明確,還不用自己實現新的Exception類型。

方案

其實,這兩個demo有兩個重要的方面。一方面是可以親自感受,一個鎖是怎麼實現的,它的方案是怎樣的。另一方面就是去思量,其中有關原子性,以及可見性的思量與設計。

你們可以嘗試改動一些東西,然後去考慮,這樣改動后,是否存在線程安全問題。這樣的考慮對自己在線程安全方面的提升是巨大的。反正我當時那一周,就不斷的改來改去。甚至有些改動,根本調試不出來問題,然後諮詢了別人,才知道其中的一些坑。當然也有一些改動是可以的。

後言

如果有問題,可以@我,或者私信我。

如果覺得這篇文章不錯的話,請點擊推薦。這對我,以及那些需要的人,很重要。

謝謝。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

從零開始入門 | Kubernetes 中的服務發現與負載均衡

作者 | 阿里巴巴技術專家  溪恆

一、需求來源

為什麼需要服務發現

在 K8s 集群裏面會通過 pod 去部署應用,與傳統的應用部署不同,傳統應用部署在給定的機器上面去部署,我們知道怎麼去調用別的機器的 IP 地址。但是在 K8s 集群裏面應用是通過 pod 去部署的, 而 pod 生命周期是短暫的。在 pod 的生命周期過程中,比如它創建或銷毀,它的 IP 地址都會發生變化,這樣就不能使用傳統的部署方式,不能指定 IP 去訪問指定的應用。

另外在 K8s 的應用部署里,之前雖然學習了 deployment 的應用部署模式,但還是需要創建一個 pod 組,然後這些 pod 組需要提供一個統一的訪問入口,以及怎麼去控制流量負載均衡到這個組裡面。比如說測試環境、預發環境和線上環境,其實在部署的過程中需要保持同樣的一個部署模板以及訪問方式。因為這樣就可以用同一套應用的模板在不同的環境中直接發布。

Service:Kubernetes 中的服務發現與負載均衡

最後應用服務需要暴露到外部去訪問,需要提供給外部的用戶去調用的。我們上節了解到 pod 的網絡跟機器不是同一個段的網絡,那怎麼讓 pod 網絡暴露到去給外部訪問呢?這時就需要服務發現。

在 K8s 裏面,服務發現與負載均衡就是 K8s Service。上圖就是在 K8s 里 Service 的架構,K8s Service 向上提供了外部網絡以及 pod 網絡的訪問,即外部網絡可以通過 service 去訪問,pod 網絡也可以通過 K8s Service 去訪問。

向下,K8s 對接了另外一組 pod,即可以通過 K8s Service 的方式去負載均衡到一組 pod 上面去,這樣相當於解決了前面所說的複發性問題,或者提供了統一的訪問入口去做服務發現,然後又可以給外部網絡訪問,解決不同的 pod 之間的訪問,提供統一的訪問地址。

二、用例解讀

下面進行實際的一個用例解讀,看 pod K8s 的 service 要怎麼去聲明、怎麼去使用?

Service 語法

首先來看 K8s Service 的一個語法,上圖實際就是 K8s 的一個聲明結構。這個結構里有很多語法,跟之前所介紹的 K8s 的一些標準對象有很多相似之處。比如說標籤 label 去做一些選擇、selector 去做一些選擇、label 去聲明它的一些 label 標籤等。

這裡有一個新的知識點,就是定義了用於 K8s Service 服務發現的一個協議以及端口。繼續來看這個模板,聲明了一個名叫 my-service 的一個 K8s Service,它有一個 app:my-service 的 label,它選擇了 app:MyApp 這樣一個 label 的 pod 作為它的後端。

最後是定義的服務發現的協議以及端口,這個示例中我們定義的是 TCP 協議,端口是 80,目的端口是 9376,效果是訪問到這個 service 80 端口會被路由到後端的 targetPort,就是只要訪問到這個 service 80 端口的都會負載均衡到後端 app:MyApp 這種 label 的 pod 的 9376 端口。

創建和查看 Service

如何去創建剛才聲明的這個 service 對象,以及它創建之後是什麼樣的效果呢?通過簡單的命令:

  • kubectl apply -f service.yaml

或者是

  • kubectl created -f service.yaml

上面的命令可以簡單地去創建這樣一個 service。創建好之後,可以通過:

  • kubectl discribe service

去查看 service 創建之後的一個結果。

service 創建好之後,你可以看到它的名字是 my-service。Namespace、Label、Selector 這些都跟我們之前聲明的一樣,這裏聲明完之後會生成一個 IP 地址,這個 IP 地址就是 service 的 IP 地址,這個 IP 地址在集群裏面可以被其它 pod 所訪問,相當於通過這個 IP 地址提供了統一的一個 pod 的訪問入口,以及服務發現。

這裏還有一個 Endpoints 的屬性,就是我們通過 Endpoints 可以看到:通過前面所聲明的 selector 去選擇了哪些 pod?以及這些 pod 都是什麼樣一個狀態?比如說通過 selector,我們看到它選擇了這些 pod 的一個 IP,以及這些 pod 所聲明的 targetPort 的一個端口。

實際的架構如上圖所示。在 service 創建之後,它會在集群裏面創建一個虛擬的 IP 地址以及端口,在集群里,所有的 pod 和 node 都可以通過這樣一個 IP 地址和端口去訪問到這個 service。這個 service 會把它選擇的 pod 及其 IP 地址都掛載到後端。這樣通過 service 的 IP 地址訪問時,就可以負載均衡到後端這些 pod 上面去。

當 pod 的生命周期有變化時,比如說其中一個 pod 銷毀,service 就會自動從後端摘除這個 pod。這樣實現了:就算 pod 的生命周期有變化,它訪問的端點是不會發生變化的。

集群內訪問 Service

在集群裏面,其他 pod 要怎麼訪問到我們所創建的這個 service 呢?有三種方式:

  • 首先我們可以通過 service 的虛擬 IP 去訪問,比如說剛創建的 my-service 這個服務,通過 kubectl get svc 或者 kubectl discribe service 都可以看到它的虛擬 IP 地址是 172.29.3.27,端口是 80,然後就可以通過這個虛擬 IP 及端口在 pod 裏面直接訪問到這個 service 的地址。

  • 第二種方式直接訪問服務名,依靠 DNS 解析,就是同一個 namespace 里 pod 可以直接通過 service 的名字去訪問到剛才所聲明的這個 service。不同的 namespace 裏面,我們可以通過 service 名字加“.”,然後加 service 所在的哪個 namespace 去訪問這個 service,例如我們直接用 curl 去訪問,就是 my- 就可以訪問到這個 service。

  • 第三種是通過環境變量訪問,在同一個 namespace 里的 pod 啟動時,K8s 會把 service 的一些 IP 地址、端口,以及一些簡單的配置,通過環境變量的方式放到 K8s 的 pod 裏面。在 K8s pod 的容器啟動之後,通過讀取系統的環境變量比讀取到 namespace 裏面其他 service 配置的一個地址,或者是它的端口號等等。比如在集群的某一個 pod 裏面,可以直接通過 curl $ 取到一個環境變量的值,比如取到 MY_SERVICE_SERVICE_HOST 就是它的一個 IP 地址,MY_SERVICE 就是剛才我們聲明的 MY_SERVICE,SERVICE_PORT 就是它的端口號,這樣也可以請求到集群裏面的 MY_SERVICE 這個 service。

Headless Service

service 有一個特別的形態就是 Headless Service。service 創建的時候可以指定 clusterIP:None,告訴 K8s 說我不需要 clusterIP(就是剛才所說的集群裏面的一個虛擬 IP),然後 K8s 就不會分配給這個 service 一個虛擬 IP 地址,它沒有虛擬 IP 地址怎麼做到負載均衡以及統一的訪問入口呢?

它是這樣來操作的:pod 可以直接通過 service_name 用 DNS 的方式解析到所有後端 pod 的 IP 地址,通過 DNS 的 A 記錄的方式會解析到所有後端的 Pod 的地址,由客戶端選擇一個後端的 IP 地址,這個 A 記錄會隨着 pod 的生命周期變化,返回的 A 記錄列表也發生變化,這樣就要求客戶端應用要從 A 記錄把所有 DNS 返回到 A 記錄的列表裡面 IP 地址中,客戶端自己去選擇一個合適的地址去訪問 pod。

可以從上圖看一下跟剛才我們聲明的模板的區別,就是在中間加了一個 clusterIP:None,即表明不需要虛擬 IP。實際效果就是集群的 pod 訪問 my-service 時,會直接解析到所有的 service 對應 pod 的 IP 地址,返回給 pod,然後 pod 裏面自己去選擇一個 IP 地址去直接訪問。

向集群外暴露 Service

前面介紹的都是在集群裏面 node 或者 pod 去訪問 service,service 怎麼去向外暴露呢?怎麼把應用實際暴露給公網去訪問呢?這裏 service 也有兩種類型去解決這個問題,一個是 NodePort,一個是 LoadBalancer。

  • NodePort 的方式就是在集群的 node 上面(即集群的節點的宿主機上面)去暴露節點上的一個端口,這樣相當於在節點的一個端口上面訪問到之後就會再去做一層轉發,轉發到虛擬的 IP 地址上面,就是剛剛宿主機上面 service 虛擬 IP 地址。

  • LoadBalancer 類型就是在 NodePort 上面又做了一層轉換,剛才所說的 NodePort 其實是集群裏面每個節點上面一個端口,LoadBalancer 是在所有的節點前又掛一個負載均衡。比如在阿里雲上掛一個 SLB,這個負載均衡會提供一個統一的入口,並把所有它接觸到的流量負載均衡到每一個集群節點的 node pod 上面去。然後 node pod 再轉化成 ClusterIP,去訪問到實際的 pod 上面。

三、操作演示

下面進行實際操作演示,在阿里雲的容器服務上面進去體驗一下如何使用 K8s Service。

創建 Service

我們已經創建好了一個阿里雲的容器集群,然後並且配置好本地終端到阿里雲容器集群的一個連接。

首先可以通過 kubectl get cs ,可以看到我們已經正常連接到了阿里雲容器服務的集群上面去。

今天將通過這些模板實際去體驗阿里雲服務上面去使用 K8s Service。有三個模板,首先是 client,就是用來模擬通過 service 去訪問 K8s 的 service,然後負載均衡到我們的 service 裏面去聲明的一組 pod 上。

K8s Service 的上面,跟剛才介紹一樣,我們創建了一個 K8s Service 模板,裏面 pod,K8s Service 會通過前端指定的 80 端口負載均衡到後端 pod 的 80 端口上面,然後 selector 選擇到 run:nginx 這樣標籤的一些 pod 去作為它的後端。

然後去創建帶有這樣標籤的一組 pod,通過什麼去創建 pod 呢?就是之前所介紹的 K8s deployment,通過 deployment 我們可以輕鬆創建出一組 pod,然後上面聲明 run:nginx 這樣一個label,並且它有兩個副本,會同時跑出來兩個 pod。

先創建一組 pod,就是創建這個 K8s deployment,通過 kubectl create -f service.yaml。這個 deployment 也創建好了,再看一下 pod 有沒有創建出來。如下圖看到這個 deployment 所創建的兩個 pod 都已經在 running 了。通過 kubectl get pod -o wide 可以看到 IP 地址。通過 -l,即 label 去做篩選,run=nginx。如下圖所示可以看到,這兩個 pod 分別是 10.0.0.135 和 10.0.0.12 這樣一個 IP 地址,並且都是帶 run=nginx 這個 label 的。

下面我們去創建 K8s service,就是剛才介紹的通過 service 去選擇這兩個 pod。這個 service 已經創建好了。

根據剛才介紹,通過 kubectl describe svc 可以看到這個 service 實際的一個狀態。如下圖所示,剛才創建的 nginx service,它的選擇器是 run=nginx,通過 run=nginx 這個選擇器選擇到後端的 pod 地址,就是剛才所看到那兩個 pod 的地址:10.0.0.12 和 10.0.0.135。這裏可以看到 K8s 為它生成了集群裏面一個虛擬 IP 地址,通過這個虛擬 IP 地址,它就可以負載均衡到後面的兩個 pod 上面去。

現在去創建一個客戶端的 pod 實際去感受一下如何去訪問這個 K8s Service,我們通過 client.yaml 去創建客戶端的 pod,kubectl get pod 可以看到客戶端 pod 已經創建好並且已經在運行中了。

通過 kubectl exec 到這個 pod 裏面,進入這個 pod 去感受一下剛才所說的三種訪問方式,首先可以直接去訪問這個 K8s 為它生成的這個 ClusterIP,就是虛擬 IP 地址,通過 curl 訪問這個 IP 地址,這個 pod 裏面沒有裝 curl。通過 wget 這個 IP 地址,輸入進去測試一下。可以看到通過這個去訪問到實際的 IP 地址是可以訪問到後端的 nginx 上面的,這個虛擬是一個統一的入口。

第二種方式,可以通過直接 service 名字的方式去訪問到這個 service。同樣通過 wget,訪問我們剛才所創建的 service 名 nginx,可以發現跟剛才看到的結果是一樣的。

在不同的 namespace 時,也可以通過加上 namespace 的一個名字去訪問到 service,比如這裏的 namespace 為 default。

最後我們介紹的訪問方式裏面還可以通過環境變量去訪問,在這個 pod 裏面直接通過執行 env 命令看一下它實際注入的環境變量的情況。看一下 nginx 的 service 的各種配置已經註冊進來了。

可以通過 wget 同樣去訪問這樣一個環境變量,然後可以訪問到我們的一個 service。

介紹完這三種訪問方式,再看一下如何通過 service 外部的網絡去訪問。我們 vim 直接修改一些剛才所創建的 service。

最後我們添加一個 type,就是 LoadBalancer,就是我們前面所介紹的外部訪問的方式。

然後通過 kubectl apply,這樣就把剛剛修改的內容直接生效在所創建的 service 裏面。

現在看一下 service 會有哪些變化呢?通過 kubectl get svc -o wide,我們發現剛剛創建的 nginx service 多了一個 EXTERNAL-IP,就是外部訪問的一個 IP 地址,剛才我們所訪問的都是 CLUSTER-IP,就是在集群裏面的一個虛擬 IP 地址。

然後現在實際去訪問一下這個外部 IP 地址 39.98.21.187,感受一下如何通過 service 去暴露我們的應用服務,直接在終端裏面點一下,這裏可以看到我們直接通過這個應用的外部訪問端點,可以訪問到這個 service,是不是很簡單?

我們最後再看一下用 service 去實現了 K8s 的服務發現,就是 service 的訪問地址跟 pod 的生命周期沒有關係。我們先看一下現在的 service 後面選擇的是這兩個 pod IP 地址。

我們現在把其中的一個 pod 刪掉,通過 kubectl delete 的方式把前面一個 pod 刪掉。

我們知道 deployment 會讓它自動生成一個新的 pod,現在看 IP 地址已經變成 137。

現在再去 describe 一下剛才的 service,如下圖,看到前面訪問端點就是集群的 IP 地址沒有發生變化,對外的 LoadBalancer 的 IP 地址也沒有發生變化。在所有不影響客戶端的訪問情況下,後端的一個 pod IP 已經自動放到了 service 後端裏面。

這樣就相當於在應用的組件調用的時候可以不用關心 pod 在生命周期的一個變化。

以上就是所有演示。

四、架構設計

最後是對 K8s 設計的一個簡單的分析以及實現的一些原理。

Kubernetes 服務發現架構

如上圖所示,K8s 服務發現以及 K8s Service 是這樣整體的一個架構。

K8s 分為 master 節點和 worker 節點:

  • master 裏面主要是 K8s 管控的內容;
  • worker 節點裏面是實際跑用戶應用的一個地方。

在 K8s master 節點裏面有 APIServer,就是統一管理 K8s 所有對象的地方,所有的組件都會註冊到 APIServer 上面去監聽這個對象的變化,比如說我們剛才的組件 pod 生命周期發生變化,這些事件。

這裏面最關鍵的有三個組件:

  • 一個是 Cloud Controller Manager,負責去配置 LoadBalancer 的一個負載均衡器給外部去訪問;
  • 另外一個就是 Coredns,就是通過 Coredns 去觀測 APIServer 裏面的 service 後端 pod 的一個變化,去配置 service 的 DNS 解析,實現可以通過 service 的名字直接訪問到 service 的虛擬 IP,或者是 Headless 類型的 Service 中的 IP 列表的解析;
  • 然後在每個 node 裏面會有 kube-proxy 這個組件,它通過監聽 service 以及 pod 變化,然後實際去配置集群裏面的 node pod 或者是虛擬 IP 地址的一個訪問。

實際訪問鏈路是什麼樣的呢?比如說從集群內部的一個 Client Pod3 去訪問 Service,就類似於剛才所演示的一個效果。Client Pod3 首先通過 Coredns 這裏去解析出 ServiceIP,Coredns 會返回給它 ServiceName 所對應的 service IP 是什麼,這個 Client Pod3 就會拿這個 Service IP 去做請求,它的請求到宿主機的網絡之後,就會被 kube-proxy 所配置的 iptables 或者 IPVS 去做一層攔截處理,之後去負載均衡到每一個實際的後端 pod 上面去,這樣就實現了一個負載均衡以及服務發現。

對於外部的流量,比如說剛才通過公網訪問的一個請求。它是通過外部的一個負載均衡器 Cloud Controller Manager 去監聽 service 的變化之後,去配置的一個負載均衡器,然後轉發到節點上的一個 NodePort 上面去,NodePort 也會經過 kube-proxy 的一個配置的一個 iptables,把 NodePort 的流量轉換成 ClusterIP,緊接着轉換成後端的一個 pod 的 IP 地址,去做負載均衡以及服務發現。這就是整個 K8s 服務發現以及 K8s Service 整體的結構。

後續進階

後續再進階部分我們還會更加深入地去講解 K8s Service 的實現原理,以及在 service 網絡出問題之後,如何去診斷以及去修復的技巧。

本文總結

本文的主要內容就到此為止了,這裏為大家簡單總結一下:

  1. 為什麼雲原生的場景需要服務發現和負載均衡,
  2. 在 Kubernetes 中如何使用 Kubernetes 的 Service 做服務發現和負載均衡
  3. Kubernetes 集群中 Service 涉及到的組件和大概實現原理

相信經過本文的學習與把握,大家能夠通過 Kubernetes Service 將複雜的企業級應用快速並標準地編排起來。

“阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

十年風雨,一個普通程序員的成長之路(九)一眼望到頭,一眼望不到頭

還有十幾天就是我的32歲生日,然後,33了,要過年了。

古人三十而立,我卻在這狹窄的圈子里兜兜轉轉。

多年前的喊的一句創業口號,現在還是口號。

焦慮、迷茫。

這两天一場網易的暴力裁員事件,犹如一盆涼水當頭澆下。

讓我又陷入了一年前的時刻。

渾身提不起勁。什麼都不想做。

不知前路在哪裡?

 

回過頭來看,對於當事人來說曲折圓轉的半生,之於他人,不過又是一個復讀機的普通人生而已。

上學、畢業、工作、買房、結婚、生子、還貸。

沒有家庭是形單影只,凄凄涼涼。

有了家庭卻只能蠅營狗苟,負重而行。

一眼望到頭的路罷了。

 

我們都只是這庸碌的銅爐里,亂糟糟破爛爛的一塊廢銅爛鐵而已。

所以財富神話才那麼多捧臭腳的雇從。

因為世間大多的煩惱,便是沒錢的煩惱。

所以我們信努力改變命運、知識改變命令、堅持改變命運。

其實是金錢改變命運。

 

滿是一些交錢的APP,讓你堅持下去,給你鼓勵。永遠溫馨對你。

你會改變命運的,你會財富自由的。

我們如飢似渴,似乎覺得比身邊的人多get了一門技能,升職加薪也是指日可待了。

如果你沒有升職加薪、財富自由、改變命運,那不過是你還不夠努力罷了。

不過是,交的錢還不夠多罷了。

一眼望不到頭。

 

什麼時候是個頭?

寫博客、開公眾號、寫小說。尋找出路。

我們永遠相信自己是天命之子。

堂吉訶德騎着馬,夾着騎士長槍,無知無畏地沖向了風車。

大風車吱喲喲地轉,這裏的風景呀真好看……哦,畫風跑偏了。

大風車吱喲喲地轉,不為堂吉訶德所動,不為騎士長槍所動。似能流轉萬世。

 

天地不仁,以萬物為芻狗。

你又憑什麼跳出世間這個熔爐呢?

只能在時間的鐵鎚下越來越彎曲自己的身子。

在夕陽里傴僂着身子,苟延殘喘。

一眼望到頭,又一眼望不到頭。

 

在這條短暫卻又無盡的路上,那麼多的大V與培訓機構告訴你:

劉強東曾跟你一樣賣過盜版盤跟電腦。

馬雲還沒你強,曾被肯德基拒絕臨時工。

比爾蓋茨中途就退了學,你跟惠普也就差個車庫而已了。

遺憾的是,給你上課的老師,可能正兒八經的資金來源還沒有你多。

你以為打開了得到,便真能得道。

你以為買了極客時間,便真成了極客。

你以為加了大V,便算是有了人脈。

你以為入了知識星球,便真學到了知識。

遺憾的是,大多時候,它們與書架上落灰的書籍沒什麼兩樣。

 

家庭的壓力也讓你學習的時間慢慢變少。

有了一點獨處的時間,你卻又想打兩把遊戲,松一松這命運壓迫的喉嚨,大口地喘息兩聲。

在遊戲中,孩子的哭鬧、老婆的絮絮叨叨,都已變成遙遠的過去。

可是玩了一會,你卻又充滿了負罪感。空虛與寂寞隨之而來。

因為普通人改變命運的機會太少了。

所以只能讀書改變命運。

 

在兩千年的中華文明史中,知識改變命運的箴言已印刻在了基因里。

犹如稻草。

給溺水的人,最後一點光芒與希望。

因為你這一生,我這一生。一眼便已能看到頭。

所以佛度來生。

所以道修逍遙。

若有仙人撫我頂,怎可結髮受長生?

我願化為北冥之鯤,潛於九淵,扶搖九天,逍遙星河之外。

只是一聲“爸爸,我要尿尿”。夢,便醒了。

 

你心裏嘆了一口氣,便把兒子從你跟你老婆身邊抱下了床。

穿衣、洗漱,照了照鏡子,才刮的鬍子又長了出來。

算了,反正是非單身的程序員,也沒什麼可講究的。

關上門,復讀機的一天又開始了。

可是我還是渾身提不起勁,總感覺失去了什麼。

於是,寫下此文。

一眼望到頭,一眼望不到頭。

 

——————————————————–
歡迎關注我的公眾號:姚毛毛的博客

這裡有我的編程生涯感悟與總結,有Java、Linux、Oracle、mysql的相關技術,有工作中進行的架構設計實踐和讀書理論,有JVM、Linux、數據庫的性能調優,有……

有技術,有情懷,有溫度

歡迎關注我:姚毛毛& 妖生

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

大型科技團隊的管理

介紹了高效科技組織的特點及管理經驗,指出科技團隊的定位和使命在於支持業務、賦能業務、最終引領業務,同時,還介紹了面向未來的科技組織的特點及對管理者提出的能力要求。

內容來源 | LeaTech全球CTO領導力峰會宜信公司CTO 高級副總裁向江旭分享《大型科技團隊的管理》

主講人 | 宜信公司CTO 高級副總裁向江旭

實錄整理 | 宜信技術學院成芳

引言:11月16日,由51CTO旗下CTO訓練營品牌精心打造的LeaTech全球CTO領導力峰會在北京粵財JW萬豪酒店拉開序幕。作為CTO、技術VP、技術總監等技術管理者人群的高端社交圈,本屆峰會現場聚集了CTO訓練營歷屆校友、CTO導師,以及行業中的資深技術管理者。600多位與會嘉賓在現場充分交流了有關技術性視野、技術領導力、技術團隊組織建設等精彩話題的觀點與思考,藉助峰會這個線下平台,技術管理者們积極探索了更多商業可能,開拓管理視野,令自身領導力再上新台階。

本次峰會邀請到宜信公司CTO 高級副總裁向江旭先生,帶來主題為《大型科技團隊的管理》的分享,向江旭先生在分享中提到科技團隊的定位和使命在於支持業務、賦能業務、最終引領業務,同時,他還介紹了面向未來的科技組織的特點及對管理者提出的能力要求。

以下為本次演講的分享實錄。

各位朋友下午好,今天我分享的主題是《大型科技團隊的管理》,非常高興能跟大家分享一些關於大型科技團隊管理的經驗和觀察。

去年12月,我在另一個峰會的分享中提到經濟寒冬、金融寒冬,今年這個時候也開始進入冬季,但是不管外面的大環境如何,技術人/技術圈都非常幸運,在科技賦能的市場中,技術人總是被需要的,技術圈也總是熱情高漲的。我認為,關於技術團隊的管理經驗非常值得與大家一起分享和交流。

一、大型科技團隊的特點及定位

大型科技團隊一般都有以下幾個特點:

  • 一定規模。顧名思義,談到大型科技團隊首先想到的特點肯定是團隊成員眾多。
  • 地域分佈廣。科技團隊的成員可能分佈在不同的地方。
  • 團隊背景多元。這種多元包括兩個層面的含義:一是角色背景多元,有的成員甚至不是做技術(這裏特指軟件研發)的,而是從其他行業轉過來的,比如行業數據分析師等角色。二是團隊成員的種族、國家背景多元。

一定規模、團隊背景多元化、分佈在不同地域等特點,使得大型科技團隊在管理上面臨着非常大的挑戰。

1.1 大型科技公司的科技團隊組織架構

上圖是一些大家耳熟能詳的漫畫,介紹了幾種典型的科技公司的科技團隊架構形式。

  • “Google式團隊架構”:最上面是兩個創始人和一個CEO,下面是部門負責人,其特點是下一級有多個彙報對象,這是谷歌的內部架構和管理方式,從一個層面代表了某個歷史階段科技組織的團隊構造。
  • “Amazon式團隊架構”:典型的層級式管理,逐級彙報。
  • “Oracle式團隊架構”:特點是有一個專門的法務團隊,因為Oracle收購了很多公司,某種程度上它是一個法務或銷售導向的公司。
  • “Facebook式團隊架構”:Facebook的團隊結構和它的業務一樣,是社交網絡形式的網狀結構。
  • “Apple式團隊架構”:(這是一張比較早期的圖)其特點是最核心的靈魂人物的觸角深入到公司的方方面面,事無巨細都是他在親自主導。
  • “Microsoft式團隊結構”:最後這張也是之前的圖,代表的是我服務過的公司-微軟。三大版塊部門沒有交集,很深的部門牆,互相之間存在比較激烈的內部鬥爭。

由此可見,大型科技公司的文化基因決定了其科技團隊的組織架構形式,而科技組織架構的設計和管理很大程度上決定了組織的效能。

1.2 科技團隊的使命和定位

在討論科技團隊的管理之前,有一個很重要的前提,要知道科技團隊的使命和定位是什麼。

很多非科技驅動的公司,比如銀行、保險公司、地產公司等,都在計劃或嘗試轉型成為一家科技公司。我認為在這樣的背景下,科技從業人員反而應該更清楚自己的定位。

1)支持業務

首先我們是支持業務的,要把業務放在最核心的位置,如果公司是靠產品和服務生存的,業務沒做好,科技也不一定能做好。當然公司類型不一樣,不能一概而論。比如純科技公司微軟,產品和服務本身就是技術,技術的好壞決定了公司業務的好壞。

2)賦能業務

科技團隊通過開發一些工具,幫助業務、銷售等部門實現業務流程信息化、智能化,使得業務流轉的過程更為暢通和友好。甚至更進一步,開發一個好的技術平台,使得生態圈、合作夥伴、第三方公司能夠在平台上發展業務、共建生態,這時科技團隊就起到了賦能業務的作用。

3)引領業務

科技團隊還可以通過技術創新、產品創新來拓展新的業務領域,催生新的業務模式,增加新的收入來源,成為引領業務的力量。

這三點是相輔相成的,支持業務、賦能業務,最後引領業務,不論是在技術驅動的公司,還是在業務驅動的公司,技術團隊的使命和定位都是如此。

1.3 技術和業務的關係

定位決定地位,業務發展是終極目標,當面臨整體技術戰略與商業戰略衝突、技術實施節點選擇、技術與業務路徑匹配等問題時,技術管理者可以從以下方面進行思考。

1)解決技術戰略與商業戰略的衝突。

技術和業務除了相輔相成的關係外,還存在一定的衝突。其實技術和業務的衝突在大家平時的工作中經常見到,比如業務同事着急上線一個功能來做活動;而技術覺得要達到同樣的目的,我們可能需要好的設計和架構,而不是簡單做一個臨時補丁式的功能,這就是很常見的技術和業務的衝突。

雖然技術負責人要對未來中長期的戰略布局保持持續思考,但這取決於公司的大小、規模和階段。如果是初創公司,首要任務是生存,那麼業務需求是最高優先級,首先要考慮解決系統不穩定、安全或其他問題;但對於有一定體量的企業,公司業務已經發展到一定階段,技術團隊也有一定的規模,當存在業務需求和技術戰略的衝突時,在滿足最緊迫的業務需求的同時,將一定精力投入到基礎技術研發中去,必須要做中長期的項目預研,做一些底層、甚至風險較高的研究。

2)技術變革的實施節點

我們永遠在高速公路上奔跑,一邊行駛一邊換輪子或換部件的事情一直在發生。技術重構、技術變革、技術債務償還的時機和節點和對業務產生的影響,是我們面臨的又一個挑戰,也是需要我們長期考慮的問題。

什麼節點選擇什麼樣的技術?技術負責人具體把握新技術引入節點,其實難度很大。如果新技術距離實現商用價值僅有一年時間,那麼必須要進行布局,申請預算,建立團隊推進;如果新技術商業化已經迫在眉睫,競爭對手已經在布局了,那麼採取的措施就不是從頭做起,更好的應對方式可能是進行併購或資本運作。

3)技術與業務路徑匹配

新技術來臨,對業務的影響和衝擊會分短、中、長期,有的技術在短期內對新的業態有幫助,有的技術需要一個比較長的周期才會對業務產生影響。這時技術領導人要評估技術本身,並將其與業務的戰略路徑進行匹配,如果對長期業務有幫助,新技術仍然要引入,只是選擇時間點、投入範圍等可能會不一樣。

同時,在引入新技術進行技術創新時,還要注意新技術對當前業務產生的影響。舉個例子,宜信是一家金融科技公司,有自己的催收部門,我們一方面通過規範催收人員的行為來進行催收,一方面自己研發催收機器人。催收機器人的出現意味着部分催收人員的工作將會被替代,同時,機器人的特點之一是沒有情緒,它會按照程序設定禮貌地和用戶溝通,這就會在一定程度對催收效果和業務產生影響。因此我們還要考慮在哪個環節使用機器人這個技術,帶來的效果會更好。這也是技術和業務相輔相成又存在矛盾衝突關係的體現。

1.4 科技戰略

對於大型科技團隊而言,科技戰略思維也非常重要。舉幾個例子。

舉例提到的是我工作過的幾家公司,它們在不同的時間點做了一些不同的戰略調整。

很多年以前,在中國90%的Windows都不是正版的,於是微軟內部提出一個計劃,希望Windows在中國免費。這個計劃現在看起來非常簡單,也很容易理解,正版Windows免費帶來的好處顯而易見:它是一個非常好的終端用戶觸點、可以獲取更多用戶,可以基於這些用戶數據做數據分析和精準營銷等。但是當時微軟Windows的老大極力反對這件事,他認為這會影響Windows的營收,因此這個計劃最終沒有實行。這就是戰略思維的問題,如果不是在中國本地親身體驗這個環境和市場,可能做出的戰略決策就不一定是準確的,帶來的結果可能是痛失更大的發展機會。

微軟後來的雲戰略轉型是成功的,而移動轉型卻是失敗的。移動轉型時期,微軟收購諾基亞旗下的大部分手機業務,並基於Windows自研出一個Windows操作系統放在手機硬件上。因為微軟覺得沒有推出自己的手機這是一個缺憾,還認為佔領移動終端必須基於Windows,因此作出這樣的決策,結果以失敗告終。Windows使微軟一度成為桌面系統垄斷的贏家,也使得微軟在移動轉型失敗,成為下一步發展的絆腳石,可謂是成也Windows,敗也Windows。

再看蘇寧,蘇寧受到阿里巴巴、京東等電商的衝擊,痛下決心必須做電商,它採取的戰略是結合自己線下門店的優勢做O2O智慧門戶,實行線上下單、線下體驗、送貨到家,這種全渠道、全觸點的用戶交互與服務形式,使得蘇寧成為“中國傳統行業数字化轉型互聯網”為數不多的成功案例之一。

現在金融行業正處於嚴監管的環境,金融公司該何去何從?在這裏分享一些我的看法。

縱觀改革開放以來的歷史,每個行業在開始初期都是開放的,任何人都可以參与進來,魚龍混雜,一旦行業出現亂象,就會有監管介入,那些能力不強的、不合規的會被淘汰,等到監管后再開放、行業再成熟時,最後存活下來的才能健康發展。金融行業也是如此。

科技同仁不僅要埋頭做好自己的工作,也要抬頭看看歷史和未來,思考我們身處的這個行業,我們從事的科技工作對公司、對行業的作用是什麼?行業未來的發展趨勢是什麼,這對我們未來的職業發展也有幫助。

二、大型科技團隊的管理實踐

2.1 成功科技組織的特點

無論是前面提到的國際科技巨頭,還是國內優秀的互聯網公司,成功的科技組織都具備一些共同的特點。

1)高效、敏捷

優秀的科技團隊一定是一個高效、敏捷的團隊,能夠快速響應用戶和市場的需求變化,快速上線產品、得到反饋、不斷迭代更新,滿足或超過用戶的預期。

2)商業思維

科技團隊一定要有商業頭腦、商業思維,因為無論是搭建系統,還是做APP,一定有用戶,我們需要真正洞察到用戶的痛點和問題,幫他們及時解決問題,給他們創造價值。

3)數據驅動

如果科技團隊的工作只圍繞產品經理提出的需求,未來的產品路線圖還不夠,一定要基於用戶反饋、市場反饋、日活、月活、留存、轉化等數據來驅動產品走向、技術走向。

4)變革創新

很多公司都在強調變革和創新,我認為這是科技團隊必須要打造的環境和氛圍,方式很多,比如組織各種各樣的黑客馬拉松、團隊之間的交流分享等。我們公司也在組織黑客馬拉松,每個月業務部門都有比較棘手或緊迫的項目,技術團隊與業務團隊聯合把這些業務痛點解決,成果馬上應用到業務環境中去。

2.2 績效管理

績效管理的機制在微軟實行了很多年,其強制5%、10%的淘汰機制,一直被人詬病,因為它使得很多團隊互相指責、互相拆台,有的員工為了績效“寧當雞頭不當鳳尾”,組織內形成不好的文化和結果。後來改成了5%、10%的獎勵,從懲罰後進者變成鼓勵先進者,取得的效果好很多。

2.3 溝通

溝通每天都在進行,比如各種大大小小的會議。我發現不管開多少會,高層領導的想法、思路並不一定能被所有同事理解,因為溝通方式、溝通渠道的原因,信息不能觸達到所有人,需要重複很多遍。

我們可以利用一些非正式場合或社交媒體來進行交流。比如我們公司有每月的CTO午餐會,抽籤的方式,各團隊都有機會可以坐下來跟我一起吃午餐,通過這種面對面的交流,我會向團隊成員提出一些我的看法,或者推薦一本書,他們會向我反饋當時的痛點、想法等。我覺得這是一種非常好的機制,大家能夠在一個寬鬆的環境下面對面地交流。記得以前在思科工作的時候,也有CEO早餐會,每個月過生日的員工可以跟CEO吃早餐,也是一種很好的溝通互動的方式。

2.4 團隊文化

團隊文化包含很多要素,我對這幾點的認同感比較深:主人翁意識、緊迫感、同理心。

2.5 技術決策

商業世界充滿了選擇和決策。作為一個技術決策人,有時很難做決定,難做決定就意味着拖延,這一點對於大型科技團隊的決策人來說是比較忌諱的,很多時候不在於你做的決策是對還是錯,而是在於你做不做決定。如果你是一個不敢做決定的人,帶領的團隊就會缺乏方向感,你做的決定就會受到團隊的質疑和挑戰。

有時候即使你做出了錯誤的決定,但大家一起努力執行的時候,可能可以逐步調整轉變從而達成正確的結果。技術管理人一定要懂得取捨,通過大腦計算,明確目標,了解關聯方,分析可選項和利弊,基於目標、數據和對未來的預期做出決策。

2.6 執行力

執行的時候要不忘初心,一步步地大處着眼、小處着手,小步快跑,快速迭代,及時反饋,及時調整,朝着設定的目標專心致志、心無旁騖。

2.7 終極目標

最後的終極目標是,希望科技團隊能夠做到比業務更懂業務,比用戶更懂用戶,讓技術本身變成公司核心的業務。我認為如果能做到科技團隊在公司起到了核心的作用,這才是永生的價值。

三、面向未來的科技組織

3.1 回顧微軟的3任CEO和3種組織

就微軟而言,同一家公司,文化和技術氛圍在不同階段是不一樣的。

第一個階段,CEO比爾蓋茨,技術為王,認為技術改變世界,代碼可以改變千萬人的生活。這一階段造就了好的產品,同時也存在一些負面影響,比如垄斷等。

第二個階段,CEO鮑爾默,業績為王,要求所有設備,包括手機、電視、車等,都跑到Windows上,還和海爾合作推出基於Windows的智能電視,當時所有跟Windows衝突的想法和計劃基本都被扼殺在搖籃里,這也導致公司錯失了很多發展的機會。

第三個階段,CEO納德拉,公司文化變得更加包容、強調同理心、開放透明,包括技術開源、提供雲服務、和蘋果/谷歌等競爭對手合作。這種開放包容的氛圍,讓微軟得以浴火重生。

3.2 面向未來的科技組織

面向未來的科技組織應該是什麼樣的?

1)跨界融合

不同背景的人,融合到一個跨界的氛圍和組織中,一起發力。

2)数字化轉型

傳統公司也在做数字化轉型,這時會引入很多科技人才,利用科技幫助公司完成轉型,並在行業實現快速發展。

3)全球共享、全球分佈

團隊組成全球化,來自不同國家、不同種族的人組成一個全球化的團隊。東南亞有一個集美團、滴滴、螞蟻金服為一體的公司,解決出行、支付、快遞的問題,這家公司有一個幾千人的科技團隊,團隊成員來自50多個種族,分佈在美國、新加坡、北京、印尼、印度等國家和地區。

4)技術引領

未來科技團隊不僅要做到賦能業務,還要能實現引領業務。

這樣的科技團隊需要跨界、全球化、複合型的領導人,只有既懂科技、又懂管理;既要懂業務,又懂行業的管理人才才能適合於領導面向未來的科技組織。

以上就是今天跟大家分享的全部內容,時間很短,主要介紹了一些在不同公司不同行業的大型科技團隊的管理經驗,希望大家在面向未來的科技組織中找到自己的定位,成為一個優秀的領導者。謝謝大家。

本文根據向江旭老師在LeaTech全球CTO領導力峰會上的分享內容整理所得,轉載請聯繫授權。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?