Java進階專題(二十二) 微服務架構體系-SpringCloudAlibaba


前言

"微服務”一詞源於 Martin Fowler的名為 Microservices的,博文,可以在他的官方博客上找到http:/ /martinfowler . com/articles/microservices.html簡單地說,微服務是系統架構上的一種設計風格,它的主旨是將一個原本獨立的系統拆分成多個小型服務,這些小型服務都在各自獨立的進程中運行,服務之間通過基於HTTP的 RESTfuL AP進行通信協作。常見微服務框架:Spring的spring cloud、阿里dubbo、華為ServiceComb、騰訊Tars、Facebook thrift、新浪微博Motan。本章節我們先從了解組成完整系統的各個組件開始,下章節將利用這些組件,搭建出一個完善的分布式系統。

Spring Cloud

這就不用多說了,官網有詳細的介紹。

Spring Cloud Alibaba

Spring Cloud Alibaba 致力於提供微服務開發的一站式解決方案。此項目包含開發分布式應用微服務的必需組件,方便開發者通過 Spring Cloud 編程模型輕松使用這些組件來開發分布式應用服務

主要組件
Sentinel:把流量作為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。
Nacos:一個更易於構建雲原生應用的動態服務發現、配置管理和服務管理平台。
RocketMQ:一款開源的分布式消息系統,基於高可用分布式集群技術,提供低延時的、高可靠的消息發布與訂閱服務。
Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
Seata:阿里巴巴開源產品,一個易於使用的高性能微服務分布式事務解決方案。

服務注冊與發現

Eureka:官方宣布2.x不再開源(閉源),之前的版本已經停止更新;也就說Eureka將來更多的技術提升已經沒有了。所以,如果希望注冊中心有更多強大功能的話,還需要另辟蹊徑 。
Zookeeper:在企業級Zookeeper注冊中心與 Dubbo組合比較多一些,kafka使用的也是,隨着Eureka的停更,我們可以通過spring-cloud-starter-zookeeper-discovery這個啟動器,將Zookeeper做為springcloud的注冊中心。
Consul:go語言開發的,也是一個優秀的服務注冊框架,使用量也比較多。
Nacos:來自於SpringCloudɵɹibaba,在企業中經過了百萬級注冊考驗的,不但可以完美替換Eureka,還能做其他組件的替換,所以,Naocs也強烈建議使用。

介紹下Nacos用作注冊中心

Nacos簡介

Nacos(Dynamic Naming and Configur ation Service) 是阿里巴巴2018年7月開源的項目,致力於發現、配置和管理微服務。

Nacos安裝

單節點

--下載鏡像
docker pull nacos/nacos-server:1.3.1
--啟動容器
docker run  --name nacos --env MODE=standalone --privileged=true  -p 8848:8848 --restart=always   -d dc833dc45d8f

訪問:

http://127.0.0.1:8848/nacos 賬號密碼都是nacos

集群

安裝前提

64 bit OS Linux/Unix/Mac,推薦使用Linux系統。
集群需要依賴mysql,單機可不必
3個或3個以上Nacos節點才能構成集群
搭建Nacos高可用集群步驟:

1、需要去Nacos官網clone Nacos集群項目nacos-docker
2、nacos-docker是使用的Docker Compose對容器進行編排,所以首先需要安裝Docker Compose詳細信息可參照Nacos官網:https:/ /nacos.io/zh-cn/docs/quick-start-docker.html

1)安裝Docker Compose
什么是Docker Compose
Compose項目是Docker官方的開源項目,負責實現對Docker容器集群的快速編排。

#在Linux下下載(下載到/usr/local/bin)
curl -L https://github.com/docker/compose/releases/download/1.25.0/run.sh > /usr/local/bin/docker-compose
# 設置文件可執行權限
chmod +x /usr/local/bin/docker-compose
# 查看版本信息
docker-compose --version

2)克隆Nacos-docker項目

#切換到自定義目錄
cd /usr/local/nacos
#開始clone
git clone https://github.com/nacos-group/nacos-docker.git

3)運行nacos-docker腳本

#執行編排命令
docker-compose -f /usr/local/nacos/nacos-docker/example/cluster-hostname.yaml up

上面的編排命令主要下載mysql鏡像和nacos鏡像,自動完成鏡像下載/容器啟動
Pulling from nacos/nacos-mysql,版本是5. 7(執行初始化腳本)
Pulling nacos3 (nacos/nacos-server:latest)最新版本

4)停止、啟動

#啟動
docker-compose -f  /usr/local/nacos/nacos-docker/example/cluster-hostname.yaml start
#停止
docker-compose -f  /usr/local/nacos/nacos-docker/example/cluster-hostname.yaml stop

5)訪問Nacos

http://192.168.1.1:8848/nacos
http://192.168.1.1:8849/nacos
http://192.168.1.1:8850/nacos

Nacos快速入門

配置服務提供者

服務提供者可以通過 Nacos 的服務注冊發現功能將其服務注冊到 Nacos server 上。

添加nacos依賴

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>${latest.version}</version>
</dependency>

添加配置

server.port=8070
spring.application.name=nacos-demo

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

啟動類

@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderApplication {

	public static void main(String[] args) {
		SpringApplication.run(NacosProviderApplication.class, args);
	}

	@RestController
	class EchoController {
		@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
		public String echo(@PathVariable String string) {
			return "Hello Nacos Discovery " + string;
		}
	}
}

啟動后,控制台:

說明注冊成功,后台查看該服務:

下面我們用spring cloud 整合naocs實現服務調用

配置服務消費者

添加配置

server.port=8080
spring.application.name=service-consumer

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

添加啟動類:服務消費者使用 @LoadBalanced RestTemplate 實現服務調用

@SpringBootApplication
@EnableDiscoveryClient
public class NacosConsumerApplication {

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(NacosConsumerApplication.class, args);
    }

    @RestController
    public class TestController {

        private final RestTemplate restTemplate;

        @Autowired
        public TestController(RestTemplate restTemplate) {this.restTemplate = restTemplate;}

        @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
        public String echo(@PathVariable String str) {
            return restTemplate.getForObject("http://service-provider/echo/" + str, String.class);
        }
    }
}

測試

啟動 ProviderApplicationConsumerApplication ,調用 http://localhost:8080/echo/2018,返回內容為 Hello Nacos Discovery 2018

分布式配置中心解決方案與應用

目前市面上用的比較多的配置中心有(時間順序)
Disconf:2014年7月百度開源的配置管理中心,同樣具備配置的管理能力,不過目前已經不維護了,最近的一次提交是4-5年前了。
Spring Cloud Config:2014年9月開源,Spring Cloud 生態組件,可以和Spring Cloud體系無縫整合。
Apollo:2016年5月,攜程開源的配置管理中心,具備規范的權限、流程治理等特性。
Nacos:2018年6月,阿里開源的配置中心,也可以做DNS和RPC的服務發現

介紹下Nacos用作分布式配置中心

啟動了 Nacos server 后,您就可以參考以下示例代碼,為您的 Spring Cloud 應用啟動 Nacos 配置管理服務了。完整示例代碼請參考:nacos-spring-cloud-config-example

  1. 添加依賴:
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    <version>${latest.version}</version>
</dependency>

注意:版本 2.1.x.RELEASE 對應的是 Spring Boot 2.1.x 版本。版本 2.0.x.RELEASE 對應的是 Spring Boot 2.0.x 版本,版本 1.5.x.RELEASE 對應的是 Spring Boot 1.5.x 版本。

更多版本對應關系參考:版本說明 Wiki

  1. bootstrap.properties 中配置 Nacos server 的地址和應用名
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

spring.application.name=example

說明:之所以需要配置 spring.application.name ,是因為它是構成 Nacos 配置管理 dataId字段的一部分。

在 Nacos Spring Cloud 中,dataId 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默認為 spring.application.name 的值,也可以通過配置項 spring.cloud.nacos.config.prefix來配置。
  • spring.profiles.active 即為當前環境對應的 profile,詳情可以參考 Spring Boot文檔注意:當 spring.profiles.active 為空時,對應的連接符 - 也將不存在,dataId 的拼接格式變成 ${prefix}.${file-extension}
  • file-exetension 為配置內容的數據格式,可以通過配置項 spring.cloud.nacos.config.file-extension 來配置。目前只支持 propertiesyaml 類型。
  1. 通過 Spring Cloud 原生注解 @RefreshScope 實現配置自動更新:
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {

    @Value("${useLocalCache:false}")
    private boolean useLocalCache;

    @RequestMapping("/get")
    public boolean get() {
        return useLocalCache;
    }
}
  1. 首先通過調用 Nacos Open API 向 Nacos Server 發布配置:dataId 為example.properties,內容為useLocalCache=true
curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=example.properties&group=DEFAULT_GROUP&content=useLocalCache=true"
  1. 運行 NacosConfigApplication,調用 curl http://localhost:8080/config/get,返回內容是 true
  2. 再次調用 Nacos Open API 向 Nacos server 發布配置:dataId 為example.properties,內容為useLocalCache=false
curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=example.properties&group=DEFAULT_GROUP&content=useLocalCache=false"
  1. 再次訪問 http://localhost:8080/config/get,此時返回內容為false,說明程序中的useLocalCache值已經被動態更新了。

分布式服務調用

RPC概述

RPC 的主要功能目標是讓構建分布式計算(應用)更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。為實現該目標,RPC 框架需提供一種透明調用機制,讓使用者不必顯式的區分本地調用和遠程調用。

RPC的優點:分布式設計、部署靈活、解耦服務、擴展性強。

RPC框架

Dubbo:國內最早開源的 RPC 框架,由阿里巴巴公司開發並於 2011 年末對外開源,僅支持 Java 語言。
Motan:微博內部使用的 RPC 框架,於 2016 年對外開源,僅支持 Java 語言。
Tars:騰訊內部使用的 RPC 框架,於 2017 年對外開源,僅支持 C++ 語言。
Spring Cloud:國外 Pivotal 公司 2014 年對外開源的 RPC 框架,提供了豐富的生態組件。
gRPC:Google 於 2015 年對外開源的跨語言 RPC 框架,支持多種語言。
Thrift:最初是由 Facebook 開發的內部系統跨語言的 RPC 框架,2007 年貢獻給了 Apache 基金,成為
Apache:開源項目之一,支持多種語言。

RPC框架優點

RPC框架一般使用長鏈接,不必每次通信都要3次握手,減少網絡開銷。
RPC框架一般都有注冊中心,有豐富的監控管理發布、下線接口、動態擴展等,對調用方來說是無感知、統一化的操作協議私密,安全性較高
RPC 協議更簡單內容更小,效率更高,服務化架構、服務化治理,RPC框架是一個強力的支撐。

RPC框架應用:使用Spring Cloud Alibaba 整合Dubbo實現

由於 Dubbo Spring Cloud 構建在原生的 Spring Cloud 之上, 其服務治理方面的能力可認為是 Spring Cloud Plus,不僅完全覆蓋 Spring Cloud 原生特性,而且提供更為穩定和成熟的實現,特性比對如下表所示:

Dubbo 作為 Spring Cloud 服務調用

默認情況,Spring Cloud Open Feign 以及@LoadBalanced`RestTemplate 作為 Spring Cloud 的兩種服務調用方式。 Dubbo Spring Cloud 為其提供了第三種選擇,即 Dubbo 服務將作為 Spring Cloud 服務調用的同等公民出現,應用可通過 Apache Dubbo 注解@Service 和@Reference 暴露和引用 Dubbo 服務,實現服務間多種協議的通訊。同時,也可以利用 Dubbo 泛化接口輕松實現服務網關。

快速上手

按照傳統的 Dubbo 開發模式,在構建服務提供者之前,第一個步驟是為服務提供者和服務消費者定義 Dubbo 服務接口。
為了確保契約的一致性,推薦的做法是將 Dubbo 服務接口打包在第二方或者第三方的 artifact(jar)中,該 artifact 甚至無需添加任何依賴。
對於服務提供方而言,不僅通過依賴 artifact 的形式引入 Dubbo 服務接口,而且需要將其實現。對應的服務消費端,同樣地需要依賴該 artifact,並以接口調用的方式執行遠程方法。接下來的步驟則是創建 artifact。

創建服務API

創建一個api模塊,專門寫各種接口的:

/**
 * @author 原
 * @date 2020/12/8
 * @since 1.0
 **/
public interface TestService {

    String getMsg();
}

創建服務提供者

導入依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>
        <!-- api接口的依賴包-->
        <dependency>
            <groupId>com.dubbo.demo</groupId>
            <artifactId>dubbo-demo-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

編寫配置

dubbo:
	scan:
	# dubbo 服務掃描基准包
		base-packages: org.springframework.cloud.alibaba.dubbo.bootstrap
	protocol:
	# dubbo 協議
		name: dubbo
		# dubbo 協議端口( -1 表示自增端口,從 20880 開始)
		port: -1
	spring:
		cloud:
			nacos:
			# Nacos 服務發現與注冊配置
			discovery:
				server-addr: 127.0.0.1:8848實現
/**
 * @author 原
 * @date 2021/1/28
 * @since 1.0
 **/
@Service//dubbo的service注解
public class TestServiceImpl implements TestService {
    @Override
    public String getMsg() {
        return "123123";
    }
}

啟動類

/**
 * @author 原
 * @date 2021/1/28
 * @since 1.0
 **/
@SpringBootApplication
@EnableDiscoveryClient
public class DubboProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(DubboProviderApplication.class,args);
    }
}

創建服務消費者

除了api的實現類 其他復用提供者的代碼

編寫測試類

/**
 * @author 原
 * @date 2021/1/28
 * @since 1.0
 **/
@RestController
public class TestController {

    @Reference
    TestService testService;

    @GetMapping("/dubbo/test")
    public String getMsg(){
        return testService.getMsg();
    }
}

訪問:

返回111

服務流量管理

為什么要流控降級

流量是非常隨機性的、不可預測的。前一秒可能還風平浪靜,后一秒可能就出現流量洪峰了(例如雙十一零點的場景)。然而我們系統的容量總是有限的,如果突然而來的流量超過了系統的承受能力,就可能會導致請求處理不過來,堆積的請求處理緩慢,CPU/Load飆高,最后導致系統崩潰。因此,我們需要針對這種突發的流量來進行限制,在盡可能處理請求的同時來保障服務不被打垮,這就是流量控制

一個服務常常會調用別的模塊,可能是另外的一個遠程服務、數據庫,或者第三方API 等。例如,支付的時候,可能需要遠程調用銀聯提供的 API;查詢某個商品的價格,可能需要進行數據庫查詢。然而,這個被依賴服務的穩定性是不能保證的。如果依賴的服務出現了不穩定的情況,請求的響應時間變長,那么調用服務的方法的響應時間也會變長,線程會產生堆積,最終可能耗盡業務自身的線程池,服務本身也變得不可用。

現代微服務架構都是分布式的,由非常多的服務組成。不同服務之間相互調用,組成復雜的調用鏈路。以上的問題在鏈路調用中會產生放大的效果。復雜鏈路上的某一環不穩定,就可能會層層級聯, 最終導致整個鏈路都不可用。 因此我們需要對不穩定的弱依賴服務進行熔斷降級,暫時切斷不穩定調用,避免局部不穩定因素導致整體的雪崩。

關於容錯組件的停更/升級/替換

服務降級:
Hystrix:官網不極力推薦,但是中國企業中還在大規模使用,對於限流和熔斷降級雖然在1 .5版本官方還支持(版本穩定),
但現在官方已經開始推薦大家使用Resilience4j
Resilience4J:官網推薦使用,但是國內很少用這個。
Sentienl:來自於Spring Cloud Alibaba,在中國企業替換Hystrix的組件,國內強烈建議使用

這里就主要介紹下Sentinel。

Sentinel介紹

Sentinel是阿里開源的項目,提供了流量控制、熔斷降級、系統負載保護等多個維度來保障服務之間的穩定性。
Sentinel的流控操作起來非常簡單,在控制台進行配置即可看見效,所見即所得

Sentinel 具有以下特征:

  • 豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
  • 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機器秒級數據,甚至 500 台以下規模的集群的匯總運行情況。
  • 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
  • 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定制邏輯。例如定制規則管理、適配動態數據源等。

官網

https://github.com/alibaba/Sentinel
中文
https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
https://sentinelguard.io/zh-cn/docs/introduction.html

Sentinel 的使用可以分為兩個部分:
核心庫(Java 客戶端):不依賴任何框架/庫,能夠運行於 Java 7 及以上的版本的運行時環境,同時對Dubbo / Spring Cloud 等框架也有較好的支持。
控制台(Dashboard):控制台主要負責管理推送規則、監控、集群限流分配管理、機器發現等

使用場景

 在服務提供方(Service Provider)的場景下,我們需要保護服務提供方自身不被流量洪峰打垮。 這時候通常根據服務提供方的服務能力進行流量控制, 或針對特定的服務調用方進行限制。我們可以結合前期壓測評估核心口的承受能力,配置 QPS 模式的限流,當每秒的請求量超過設定的閾值時,會自動拒絕多余的請求。

 為了避免調用其他服務時被不穩定的服務拖垮自身,我們需要在服務調用端(Service Consumer)對不穩定服務依賴進行隔離和熔斷。手段包括信號量隔離、異常比例降級、RT 降級等多種手段。

 當系統長期處於低水位的情況下, 流量突然增加時, 直接把系統拉升到高水位可能瞬間把系統壓垮。這時候我們可以借助 Sentinel 的 WarmUp 流控模式控制通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,而不是在一瞬間全部放行。這樣可以給冷系統一個預熱的時間,避免冷系統被壓垮。

 利用 Sentinel 的勻速排隊模式進行“削峰填谷”, 把請求突刺均攤到一段時間內, 讓系統負載保持在請求處理水位之內,同時盡可能地處理更多請求。

 利用 Sentinel 的網關流控特性,在網關入口處進行流量防護,或限制 API 的調用頻率。

Sentinel安裝

1、下載jar包https://github.com/alibaba/Sentinel/releases

2、啟動

java -Dserver.port=8787 -Dcsp.sentinel.dashboard.server=127.0.0.1:8787 -Dproject.name=sentinel-dashboard -jar /home/sentinel/sentinel-dashboard-1.8.0.jar

3、訪問

http://127.0.0.1:8787/#/login

初始賬號密碼sentinel/sentinel

可以看到sentinel是自己本身的監控

sentinel快速入門

1、導入依賴

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.0</version>
</dependency>

2、測試類

public class TestService {

    public static void main(String[] args) {
        initFlowRules();
        while (true) {
            Entry entry = null;
            try {
                entry = SphU.entry("HelloWorld");
                /*您的業務邏輯 - 開始*/
                System.out.println("hello world");
                /*您的業務邏輯 - 結束*/
            } catch (BlockException e1) {
                /*流控邏輯處理 - 開始*/
                System.out.println("block!");
                /*流控邏輯處理 - 結束*/
            } finally {
                if (entry != null) {
                    entry.exit();
                }
            }
        }
    }

    //設置流量控制規則 設置當QPS達到20時 會限制流量(拋出異常,可以執行處理)
    private static void initFlowRules(){
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        rule.setResource("HelloWorld");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // Set limit QPS to 20.
        rule.setCount(20);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

執行結果:

可以看到,這個程序每秒穩定輸出 "hello world" 20 次,和規則中預先設定的閾值是一樣的。 block表示被阻止的請求。

官方使用文檔:https://github.com/alibaba/Sentinel/wiki/如何使用

Sentinel整合SpringCloud實現服務限流/熔斷

導入依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

配置

server.port=8082
spring.application.name=sentinel-demo
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8787
//在需要流控的方法加上@SentinelResource
@RestController
public class TestController {

    @GetMapping("/sentinel")
    @SentinelResource
    public String getMsg(){
        return "11";
    }
}

啟動應用,訪問http://127.0.0.1:8082/sentinel后去sentinel后台

下面我們來配一條最簡單的流控規則。針對 sentinel_spring_web_context /sentinel 這個服務調用配置限流規則(需要有過訪問量才能看到)。我們配一條 QPS 為 1的流控規則,這代表針對該服務方法的調用每秒鍾不能超過 1 次,超出會直接拒絕。

現在快速訪問:http://localhost:8082/sentinel

查看實時監控頁面:

其他功能的使用,大家可以參考官方文檔自行摸索。

如何選擇流控降級組件

以下是 Sent inel 與其它fault-tolerance 組件的對比:

分布式事務

為什么需要分布式事務?

分布式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不同的分布式系統的不同節點之上。簡單來說就是組成事務的各個單元處於不同數據庫服務器上。 如何保證服務間的一致性?當在一條較長的微服務調用鏈中, 位於中間位置的微服務節點出現異常,如何保證整個服務的數據一致性?分布式一致性的引入, 一定不可避免帶來性能問題, 如何更高效的解決分布式一致性問題,一直是我們致力於解決此問題的關鍵出發點。

分布式事務解決方案分類

剛性事務

剛性事務指的就是遵循本地事務四大特性(ACID)的強一致性事務。它的特點就是強一致性,要求組成事務的各個
單元馬上提交或者馬上回滾,沒有時間彈性,要求以同步的方式執行。通常在單體架構項目中應用較多,一般都是
企業級應用(或者局域網應用)。例如:生成合同時記錄日志,付款成功后生成憑據等等。但是,在時下流行的互
聯網項目中,這種剛性事務來解決分布式事務就會有很多的弊端。其中最明顯的或者說最致命的就是性能問題。

因為某個參與者不能自己提交事務,必須等待所有參與者執行OK了之后,一起提交事務,那么事務鎖住的時間就
變得非常長,從而導致性能非常低下。基於此我們也就得出了一個結論,階段越多,性能越差。

柔性事務

柔性事務是針對剛性事務而說的,我們剛才分析了剛性事務,它有兩個特點,第一個強一致性,第二個近實時性
(NRT)。而柔性事務的特點是不需要立刻馬上執行(同步性),且不需要強一致性。它只要滿足基本可用和最終
一致就可以了。要想真正的明白,需要從BASE理論和CAP理論說起。

CAP理論和BASE理論

1)CAP理論
CAP理論,又叫做CAP原則,網上對他的概念描述非常的清晰,且一致。換句話說,我們在網上搜到的CAP理論的
描述,基本都是一樣的。它的描述是這樣的:
CAP指的是在一個分布式系統中,一致性(Consistency)、可用性(Availability)、分區容錯性(Partition
tolerance)。其中,C,A,P的說明如下:
一致性(C):在分布式系統中的所有數據備份,在同一時刻是否同樣的值。(等同於所有節點訪問同一份最新的數據副本)
可用性(A):在集群中一部分節點故障后,集群整體是否還能響應客戶端]的讀寫請求。(對數據更新具備高可用
性)
分區容錯性(P):以實際效果而言,分區相當於對通信的時限要求。系統如果不能在時限內達成數據一致性,就
意味着發生了分區的情況,必須就當前操作在C和A之間做出選擇。

CAP原則指的是,這三個要素最多只能同時實現兩點,不可能三者兼顧。因此在進行分布式架構設計時,必須做出
取舍。而對於分布式數據系統,分區容錯性是基本要求,否則就失去了價值。因此設計分布式數據系統,就是在一
致性和可用性之間取一個平衡。對於大多數web應用,其實並不需要強一致性,因此犧牲一致性而換取高可用性,
是目前多數分布式數據庫產品的方向。

2)BASE理論
BASE理論是指,Basically Available(基本可用)、Soft-state( 軟狀態/柔性事務)、Eventual Consistency(最
終一致性)。
1、基本可用 BA:(Basically Available ):
指分布式系統在出現故障的時候,允許損失部分可用性,保證核心可用。但不等價於不可用。比如:搜索引擎0.5
秒返回查詢結果,但由於故障,2秒響應查詢結果;網頁訪問過大時,部分用戶提供降級服務等。簡單來說就是基
本可用。
2、軟狀態 S:( Soft State):
軟狀態是指允許系統存在中間狀態,並且該中間狀態不會影響系統整體可用性。即允許系統在不同節點間副本同步
的時候存在延時。簡單來說就是狀態可以在一段時間內不同步。
3、最終一致性 E:(Eventually Consistent ):

系統中的所有數據副本經過一定時間后,最終能夠達到一致的狀態,不需要實時保證系統數據的強一致性。最終一
致性是弱一致性的一種特殊情況。BASE理論面向的是大型高可用可擴展的分布式系統,通過犧牲強一致性來獲得
可用性。ACID是傳統數據庫常用的概念設計,追求強一致性模型。簡單來說就是在一定的時間窗口內, 最終數據
達成一致即可。
BASE理論是基於CAP原則演化而來,是對CAP中一致性和可用性權衡的結果。
核心思想:即使無法做到強一致性,但每個業務根據自身的特點,采用適當的方式來使系統達到最終一致性。

分布式事務解決方案

二階段提交2PC(3PC)

關於更多2pc與3pc的介紹,可以參考博文:2PC和3PC原理,這不是我們今天討論的重點。

XA

XA 標准提出后的 20 多年間未能得到持續的演進,在學術界有協議優化和日志協同處理等相關的研究,在工業界使用 XA 落地方案的相對較少,主要集中在應用服務器的場景。XA 方案要求相關的廠商提供其具體協議的實現,目前大部分關系數據庫支持了 XA 協議,但是支持程度不盡相同,例如,MySQL 在 5.7 才對 xa_prepare 語義做了完整支持。XA 方案被人詬病的是其性能, 其實更為嚴重的是對於連接資源的占用, 導致在高並發未有足夠的連接資源來響應請求成為系統的瓶頸。在微服務架構下 XA 事務方案隨着微服務鏈路的擴展成為一種反伸縮模式,進一步加劇了資源的占用。另外 XA 事務方案要求事務鏈路中的 resource 全部實現 XA 協議方可使用,若其中某一資源不滿足,那么就無法保證整個鏈路的數據一致性。

TCC補償方案

TCC分別指的是Try,Confirm,Cancel。它是補償型分布式事務解決方案。何為補償呢?其實我們把TCC這3個部分分別做什么捋清楚,就很容理解了。首先,我們先來看下它們的主要作用:

Try 階段主要是對業務系統做檢測及資源預留。
Confirm 階段主要是對業務系統做確認提交,Try階段執行成功並開始執行 Confirm階段時,默認 Confirm階段是
不會出錯的。即:只要Try成功,Confirm一定成功。
Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放。

由此,我們可以得出結論,就是在Try階段進行嘗試提交事務,當Try執行OK了,Confirm執行,且默認認為它一定成功。但是當Try提交失敗了,則由Cancel處理回滾和資源釋放。

從概念上 TCC 框架可以認為是一種萬能框架,但是其難點是業務對於這三個接口的實現,開發成本相對較高,有較多業務難以做資源預留相關的邏輯處理, 以及是否需要在預留資源的同時從業務層面來保證隔離性。因此,這種模式比較適應於金融場景中易於做資源預留的扣減模型。

Saga

Saga其實是30年前的一篇數據庫論文里提到的一個概念。在論文中一個Saga事務就是一個長期運行的事務,這個事務是由多個本地事務所組成, 每個本地事務有相應的執行模塊和補償模塊,當saga事務中的任意一個本地事務出錯了, 可以通過調用相關事務對應的補償方法恢復,達到事務的最終一致性。

有了 TCC 解決方案為什么還需要 Saga 事務解決方案?上文提到了 TCC 方案中對業務的改造成本較大, 對於內部系統可以自上而下大刀闊斧的推進系統的改造, 但對於第三方的接口的調用往往很難推動第三方進行 TCC 的改造,讓對方為了你這一個用戶去改造 TCC 方案而其他用戶並不需要,需求上明顯也是不合理的。要求第三方業務接口提供正反接口比如扣款和退款,在異常場景下必要的數據沖正是合理的。另外,Saga 方案更加適應於工作流式的長事務方案並且可異步化。

最終一致性方案

本地消息表

這種實現方式應該是業界使用最多的,其核心思想是將分布式事務拆分成本地事務進行處理,這種思路是來源於
ebay。它和MQ事務消息的實現思路都是一樣的,都是利用MQ通知不同的服務實現事務的操作。不同的是,針對
消息隊列的信任情況,分成了兩種不同的實現。本地消息表它是對消息隊列的穩定性處於不信任的態度,認為消息
可能會出現丟失,或者消息隊列的運行網絡會出現阻塞,於是在數據庫中建立一張獨立的表,用於存放事務執行的
狀態,配合消息隊列實現事務的控制。

MQ事務消息

有一些第三方的MQ是支持事務消息的,比如RocketMQ,ActiveMQ,他們支持事務消息的方式也是類似於采用的
二階段提交。但是有一些常用的MQ也不支持事務消息,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中間件為例,其思路大致為:
第一階段Prepared消息,會拿到消息的地址。
第二階段執行本地事務。
第三階段通過第一階段拿到的地址去訪問消息,並修改狀態。
也就是說在業務方法內要想消息隊列提交兩次請求,一次發送消息和一次確認消息。如果確認消息發送失敗了
RocketMQ會定期掃描消息集群中的事務消息,這時候發現了Prepared消息,它會向消息發送者確認,所以生產方需要實現一個check接口,RocketMQ會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。

用一個下單業務流程圖來表示下:

兩者對比

分布式事務框架Seata

FESCAR是阿里巴巴 開源的分布式事務中間件,以高效並且對業務0侵入的方式,解決微服務場景下面臨的分布式
事務問題。Seata是fescar的升級版本,從2019年4月起,更名為seata。

seata的典型案例分析

在seata的官方網站中提供了一套詳細的業務流程介紹,我們就以此為例,展開講解。

首先是單體架構的事務場景介紹:電商購物中的3個業務模塊(庫存、訂單和賬戶)。他們操作本地數據庫,由於
同庫同源,所以本地事務可以保證其一致性,如下圖所示:

但是在微服務架構中,由於每個業務模塊變成了獨立的服務,且每個服務連接自己的數據庫,此時由原來同庫同源
變成了不同數據庫不同數據源的情況,雖然每個服務自己的數據庫操作仍然可以使用本地事務控制,但是要讓服務
間的事務保持一致性就需要用到分布式事務了。

在此時,引入了seata,它提供了一個“完美”的解決方案。如下圖所示:

在圖中的三個和seata相關的組件,前面在介紹fescar的時候已經介紹了,它也是seata的基本組件,他們分別是:
1)事務協調器(TC):維護全局和分支事務的狀態,驅動全局提交或回滾。
2)事務管理器(TM):定義全局事務的范圍:開始全局事務,提交或回滾全局事務。
3)資源管理器(RM):管理分支事務的資源,與TC通信以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。

seata管理分布式事務的生命周期

  1. TM要求TC開始新的全局事務。 TC生成表示全局事務的XID。
  2. XID通過微服務的調用鏈傳播。
  3. RM將本地事務注冊為XID到TC的相應全局事務的分支。
  4. TM要求TC提交或回滾XID的相應全局事務。
  5. TC在XID的相應全局事務下驅動所有分支事務,以完成分支提交或回滾。

庫存服務

public interface StorageService {

    /**
     * deduct storage count
     */
    void deduct(String commodityCode, int count);
}

訂單服務

public interface OrderService {

    /**
     * create order
     */
    Order create(String userId, String commodityCode, int orderCount);
}

賬戶服務

public interface AccountService {

    /**
     * debit balance of user's account
     */
    void debit(String userId, int money);
}

主業務邏輯

public class BusinessServiceImpl implements BusinessService {

    private StorageService storageService;

    private OrderService orderService;

    /**
     * purchase
     */
    public void purchase(String userId, String commodityCode, int orderCount) {

        storageService.deduct(commodityCode, orderCount);

        orderService.create(userId, commodityCode, orderCount);
    }
}
public class OrderServiceImpl implements OrderService {

    private OrderDAO orderDAO;

    private AccountService accountService;

    public Order create(String userId, String commodityCode, int orderCount) {

        int orderMoney = calculate(commodityCode, orderCount);

        accountService.debit(userId, orderMoney);

        Order order = new Order();
        order.userId = userId;
        order.commodityCode = commodityCode;
        order.count = orderCount;
        order.money = orderMoney;

        // INSERT INTO orders ...
        return orderDAO.insert(order);
    }
}

我們只需要一個@GlobalTransactional關於業務方法的注釋:

    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        ......
    }

由Dubbo + SEATA提供支持的示例

步驟1:建立資料庫

  • 要求:具有InnoDB引擎的MySQL。

注意:實際上,在示例用例中,這3個服務應該有3個數據庫。但是,為了簡單起見,我們只能創建一個數據庫並配置3個數據源。

使用您剛創建的數據庫URL /用戶名/密碼修改Spring XML。

dubbo-account-service.xml dubbo-order-service.xml dubbo-storage-service.xml

        <property name="url" value="jdbc:mysql://x.x.x.x:3306/xxx" />
        <property name="username" value="xxx" />
        <property name="password" value="xxx" />

步驟2:創建UNDO_LOG表

UNDO_LOG SEATA AT模式需要此表。

-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

步驟3:建立表格,例如業務

DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

步驟4:啟動服務器

  • 從https://github.com/seata/seata/releases下載服務器軟件包,將其解壓縮。
Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
  Options:
    --host, -h
      The host to bind.
      Default: 0.0.0.0
    --port, -p
      The port to listen.
      Default: 8091
    --storeMode, -m
      log store mode : file、db
      Default: file
    --help

e.g.

sh seata-server.sh -p 8091 -h 127.0.0.1 -m file

步驟5:運行示例

前往樣品倉庫seata-samples

  • 啟動DubboAccountServiceStarter
  • 啟動DubboStorageServiceStarter
  • 啟動DubboOrderServiceStarter
  • 運行DubboBusinessTester進行演示測試

TBD:用於運行演示應用程序的腳本


免責聲明!

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



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