Spring Cloud Alibaba—Seata分佈式事務方案

寫在前面

Seata 是一款 Alibaba 開源的分佈式事務解決方案,致力於提供高性能和簡單易用的分佈式事務服務。Seata 將爲用戶提供了 AT、TCC、SAGA 和 XA 事務模式,爲用戶打造一站式的分佈式解決方案。本文將帶你們從頭至尾搭建基於 Seata 的分佈式事務解決方案,涉及的內容也會比較詳細。java

同時用XMind畫了一張導圖記錄Spring Cloud Alibaba的學習筆記(源文件對部分節點有詳細備註和參考資料,因爲太大就沒展現所有,歡迎關注個人公衆號:阿風的架構筆記 後臺發送【導圖】拿下載連接, 已經完善更新): git

分佈式事務基礎

事務

事務指的就是一個操做單元,在這個操做單元中的全部操做最終要保持一致的行爲,要麼全部操做都成功,要麼全部的操做都被撤銷。簡單地說,事務提供一種「要麼什麼都不作,要麼作全套」機制。github

本地事務

本地事物其實能夠認爲是數據庫提供的事務機制。說到數據庫事務就不得不說,數據庫事務中的四大特性:spring

  • A:原子性(Atomicity),一個事務中的全部操做,要麼所有完成,要麼所有不完成sql

  • C:一致性(Consistency),在一個事務執行以前和執行以後數據庫都必須處於一致性狀態數據庫

  • I:隔離性(Isolation),在併發環境中,當不一樣的事務同時操做相同的數據時,事務之間互不影響bootstrap

  • D:持久性(Durability),指的是隻要事務成功結束,它對數據庫所作的更新就必須永久的保存下來服務器

數據庫事務在實現時會將一次事務涉及的全部操做所有歸入到一個不可分割的執行單元,該執行單元中的全部操做要麼都成功,要麼都失敗,只要其中任一操做執行失敗,都將致使整個事務的回滾markdown

分佈式事務

分佈式事務指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不一樣的分佈式系統的不一樣節點之上。架構

簡單的說,就是一次大的操做由不一樣的小操做組成,這些小的操做分佈在不一樣的服務器上,且屬於不一樣的應用,分佈式事務須要保證這些小操做要麼所有成功,要麼所有失敗。

本質上來講,分佈式事務就是爲了保證不一樣數據庫的數據一致性。

分佈式事務的場景

單體系統訪問多個數據庫

一個服務須要調用多個數據庫實例完成數據的增刪改操做

多個微服務訪問同一個數據庫

多個服務須要調用一個數據庫實例完成數據的增刪改操做

多個微服務訪問多個數據庫

多個服務須要調用一個數據庫實例完成數據的增刪改操做

分佈式事務解決方案

全局事務

全局事務基於DTP模型實現。DTP是由X/Open組織提出的一種分佈式事務模型——X/Open Distributed Transaction Processing Reference Model。它規定了要實現分佈式事務,須要三種角色:

  • AP: Application 應用系統 (微服務)

  • TM: Transaction Manager 事務管理器 (全局事務管理)

  • RM: Resource Manager 資源管理器 (數據庫)

整個事務分紅兩個階段:

  • 階段一: 表決階段,全部參與者都將本事務執行預提交,並將可否成功的信息反饋發給協調者。

  • 階段二: 執行階段,協調者根據全部參與者的反饋,通知全部參與者,步調一致地執行提交或者回滾。

優勢

  • 提升了數據一致性的機率,實現成本較低

缺點

  • 單點問題: 事務協調者宕機

  • 同步阻塞: 延遲了提交時間,加長了資源阻塞時間

  • 數據不一致: 提交第二階段,依然存在commit結果未知的狀況,有可能致使數據不一致

可靠消息服務

基於可靠消息服務的方案是經過消息中間件保證上、下游應用數據操做的一致性。假設有A和B兩個系統,分別能夠處理任務A和任務B。此時存在一個業務流程,須要將任務A和任務B在同一個事務中處理。就可使用消息中間件來實現這種分佈式事務。

第一步:消息由系統A投遞到中間件

  1. 在系統A處理任務A前,首先向消息中間件發送一條消息

  2. 消息中間件收到後將該條消息持久化,但並不投遞。持久化成功後,向A回覆一個確認應答

  3. 系統A收到確認應答後,則能夠開始處理任務A

  4. 任務A處理完成後,向消息中間件發送Commit或者Rollback請求。該請求發送完成後,對系統A而言,該事務的處理過程就結束了

  5. 若是消息中間件收到Commit,則向B系統投遞消息;若是收到Rollback,則直接丟棄消息。可是若是消息中間件收不到Commit和Rollback指令,那麼就要依靠"超時詢問機制"。

超時詢問機制

系統A除了實現正常的業務流程外,還需提供一個事務詢問的接口,供消息中間件調用。當消息中間件收到發佈消息便開始計時,若是到了超時沒收到確認指令,就會主動調用系統A提供的事務詢問接口詢問該系統目前的狀態。該接口會返回三種結果,中間件根據三種結果作出不一樣反應:

  • 提交:將該消息投遞給系統B

  • 回滾:直接將條消息丟棄

  • 處理中:繼續等待

第二步:消息由中間件投遞到系統B

消息中間件向下遊系統投遞完消息後便進入阻塞等待狀態,下游系統便當即進行任務的處理,任務處理完成後便向消息中間件返回應答。

  • 若是消息中間件收到確認應答後便認爲該事務處理完畢

  • 若是消息中間件在等待確認應答超時以後就會從新投遞,直到下游消費者返回消費成功響應爲止。

通常消息中間件能夠設置消息重試的次數和時間間隔,若是最終仍是不能成功投遞,則須要手工干預。這裏之因此使用人工干預,而不是使用讓A系統回滾,主要是考慮到整個系統設計的複雜度問題。

基於可靠消息服務的分佈式事務,前半部分使用異步,注重性能;後半部分使用同步,注重開發成本。

最大努力通知

最大努力通知也被稱爲按期校對,實際上是對第二種解決方案的進一步優化。它引入了本地消息表來記錄錯誤消息,而後加入失敗消息的按期校對功能,來進一步保證消息會被下游系統消費。

第一步:消息由系統A投遞到中間件

  1. 處理業務的同一事務中,向本地消息表中寫入一條記錄

  2. 準備專門的消息發送者不斷地發送本地消息表中的消息到消息中間件,若是發送失敗則重試

第二步:消息由中間件投遞到系統B

  1. 消息中間件收到消息後負責將該消息同步投遞給相應的下游系統,並觸發下游系統的任務執行

  2. 當下遊系統處理成功後,向消息中間件反饋確認應答,消息中間件即可以將該條消息刪除,從而該事務完成

  3. 對於投遞失敗的消息,利用重試機制進行重試,對於重試失敗的,寫入錯誤消息表

  4. 消息中間件須要提供失敗消息的查詢接口,下游系統會按期查詢失敗消息,並將其消費

這種方式的優缺點:

  • 優勢: 一種很是經典的實現,實現了最終一致性。

  • 缺點: 消息表會耦合到業務系統中,若是沒有封裝好的解決方案,會有不少雜活須要處理。

TCC事務

TCC即爲Try Confifirm Cancel,它屬於補償型分佈式事務。TCC實現分佈式事務一共有三個步驟:

  • Try: 嘗試待執行的業務:這個過程並未執行業務,只是完成全部業務的一致性檢查,並預留好執行所需的所有資源

  • Confifirm: 確認執行業務:確認執行業務操做,不作任何業務檢查, 只使用Try階段預留的業務資源。一般狀況下,採用TCC則認爲 Confifirm階段是不會出錯的。即:只要Try成功,Confifirm必定成功。若Confifirm階段真的出錯了,需引入重試機制或人工處理。

  • Cancel: 取消待執行的業務:取消Try階段預留的業務資源。一般狀況下,採用TCC則認爲Cancel階段也是必定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理

TCC兩階段提交與XA兩階段提交的區別是:

  • XA是資源層面的分佈式事務,強一致性,在兩階段提交的整個過程當中,一直會持有資源的鎖。

  • TCC是業務層面的分佈式事務,最終一致性,不會一直持有資源的鎖。

TCC事務的優缺點:

  • 優勢:把數據庫層的二階段提交上提到了應用層來實現,規避了數據庫層的2PC性能低下問題。

  • 缺點:TCC的Try、Confifirm和Cancel操做功能需業務提供,開發成本高。

Seata介紹

2019 年 1 月,阿里巴巴中間件團隊發起了開源項目 Fescar(Fast & EaSy Commit AndRollback),其願景是讓分佈式事務的使用像本地事務的使用同樣,簡單和高效,並逐步解決開發者們遇到的分佈式事務方面的全部難題。後來改名爲 Seata,意爲:Simple Extensible Autonomous Transaction Architecture,是一套分佈式事務解決方案。

Seata的設計目標是對業務無侵入,所以從業務無侵入的2PC方案着手,在傳統2PC的基礎上演進。它把一個分佈式事務理解成一個包含了若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一致,要麼一塊兒成功提交,要麼一塊兒失敗回滾。此外,一般分支事務自己就是一個關係數據庫的本地事務。

Seata主要由三個重要組件組成:

  • TC:Transaction Coordinator 事務協調器,管理全局的分支事務的狀態,用於全局性事務的提交和回滾。

  • TM:Transaction Manager 事務管理器,用於開啓、提交或者回滾全局事務。

  • RM:Resource Manager 資源管理器,用於分支事務上的資源管理,向TC註冊分支事務,上報分支事務的狀態,接受TC的命令來提交或者回滾分支事務。

Seata的執行流程以下:

  1. A服務的TM向TC申請開啓一個全局事務,TC就會建立一個全局事務並返回一個惟一的XID

  2. A服務的RM向TC註冊分支事務,並及其歸入XID對應全局事務的管轄

  3. A服務執行分支事務,向數據庫作操做

  4. A服務開始遠程調用B服務,此時XID會在微服務的調用鏈上傳播

  5. B服務的RM向TC註冊分支事務,並將其歸入XID對應的全局事務的管轄

  6. B服務執行分支事務,向數據庫作操做

  7. 全局事務調用鏈處理完畢,TM根據有無異常向TC發起全局事務的提交或者回滾

  8. TC協調其管轄之下的全部分支事務, 決定是否回滾

Seata實現2PC與傳統2PC的差異

  1. 架構層次方面,傳統2PC方案的 RM 其實是在數據庫層,RM本質上就是數據庫自身,經過XA協議實現,而 Seata的RM是以jar包的形式做爲中間件層部署在應用程序這一側的。

  2. 兩階段提交方面,傳統2PC不管第二階段的決議是commit仍是rollback,事務性資源的鎖都要保持到Phase2完成才釋放。而Seata的作法是在Phase1 就將本地事務提交,這樣就能夠省去Phase2持鎖的時間,總體提升效率

Seata實現分佈式事務控制

本示例經過Seata中間件實現分佈式事務,模擬電商中的下單和扣庫存的過程

咱們經過訂單微服務執行下單操做,而後由訂單微服務調用商品微服務扣除庫存

案例基本代碼

修改order微服務

controller

@RestController 
@Slf4j 
public class OrderController5 {
	@Autowired 
	private OrderServiceImpl5 orderService;
	//下單 
	@RequestMapping("/order/prod/{pid}") 
	public Order order(@PathVariable("pid") Integer pid) {
		log.info("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);
		return orderService.createOrder(pid);
	}
}
複製代碼

OrderService

@Service 
@Slf4j 
public class OrderServiceImpl5{
	@Autowired 
	private OrderDao orderDao;
	@Autowired 
	private ProductService productService;
	@Autowired 
	private RocketMQTemplate rocketMQTemplate;
	@GlobalTransactional 
	public Order createOrder(Integer pid) {
		//1 調用商品微服務,查詢商品信息 
		Product product = productService.findByPid(pid);
		log.info("查詢到{}號商品的信息,內容是:{}", pid, JSON.toJSONString(product));
		//2 下單(建立訂單) 
		Order order = new Order();
		order.setUid(1);
		order.setUsername("測試用戶");
		order.setPid(pid);
		order.setPname(product.getPname());
		order.setPprice(product.getPprice());
		order.setNumber(1);
		orderDao.save(order);
		log.info("建立訂單成功,訂單信息爲{}", JSON.toJSONString(order));
		//3 扣庫存 
		productService.reduceInventory(pid, order.getNumber());
		//4 向mq中投遞一個下單成功的消息 
		rocketMQTemplate.convertAndSend("order-topic", order);
		return order;
	}
}
複製代碼

ProductService

@FeignClient(value = "service-product") 
public interface ProductService {
	//減庫存 
	@RequestMapping("/product/reduceInventory") 
		void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") int num);
}
複製代碼

修改Product微服務

controller

//減小庫存 
@RequestMapping("/product/reduceInventory") 
public void reduceInventory(Integer pid, int num) {
	productService.reduceInventory(pid, num);
}
複製代碼

service

@Override 
public void reduceInventory(Integer pid, int num) {
	Product product = productDao.findById(pid).get();
	product.setStock(product.getStock() - num);
	//減庫存 
	productDao.save(product);
}
複製代碼

異常模擬

在ProductServiceImpl的代碼中模擬一個異常, 而後調用下單接口

@Override 
public void reduceInventory(Integer pid, Integer number) {
	Product product = productDao.findById(pid).get();
	if (product.getStock() < number) {
		throw new RuntimeException("庫存不足");
	}
	int i = 1 / 0;
	product.setStock(product.getStock() - number);
	productDao.save(product);
}
複製代碼

啓動Seata

下載seata

下載地址:github.com/seata/seata…

修改配置文件

將下載獲得的壓縮包進行解壓,進入conf目錄,調整下面的配置文件:

registry.conf

registry {
	type = "nacos" 
	nacos { 
	serverAddr = "localhost" 
	namespace = "public" 
	cluster = "default" 
	} 
	}
	config { 
	type = "nacos" 
	nacos { 
	serverAddr = "localhost" 
	namespace = "public" 
	cluster = "default" 
	} 
	}
複製代碼

nacos-confifig.txt

service.vgroup_mapping.service-product=default 
service.vgroup_mapping.service-order=default
複製代碼

這裏的語法爲: service.vgroup_mapping.${your-service-gruop}=default ,中間的${your-service-gruop} 爲本身定義的服務組名稱, 這裏須要咱們在程序的配置文件中配置

初始化seata在nacos的配置

\# 初始化seata 的nacos配置 

\# 注意: 這裏要保證nacos是已經正常運行的 

cd conf 

nacos-config.sh 127.0.0.1
複製代碼

執行成功後能夠打開Nacos的控制檯,在配置列表中,能夠看到初始化了不少Group爲SEATA_GROUP的配置。

啓動seata服務

cd bin 

seata-server.bat -p 9000 -m file
複製代碼

啓動後在 Nacos 的服務列表下面能夠看到一個名爲 serverAddr 的服務。

使用Seata實現事務控制

初始化數據表

在咱們的數據庫中加入一張undo_log表,這是Seata記錄事務日誌要用到的表

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;
複製代碼

添加配置

在須要進行分佈式控制的微服務中進行下面幾項配置:

添加依賴

<dependency> 

<groupId>com.alibaba.cloud</groupId> 

<artifactId>spring-cloud-starter-alibaba-seata</artifactId> 

</dependency> 

<dependency> 

<groupId>com.alibaba.cloud</groupId> 

<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> 

</dependency>
複製代碼

DataSourceProxyConfifig

Seata 是經過代理數據源實現事務分支的,因此須要配置 io.seata.rm.datasource.DataSourceProxy 的Bean,且是 @Primary默認的數據源,不然事務不會回滾,沒法實現分佈式事務

@Configuration 
public class DataSourceProxyConfig {
	@Bean 
	@ConfigurationProperties(prefix = "spring.datasource") 
	public DruidDataSource druidDataSource() {
		return new DruidDataSource();
	}
	@Primary 
	@Bean 
	public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
		return new DataSourceProxy(druidDataSource);
	}
}
複製代碼

registry.conf

在resources下添加Seata的配置文件 registry.conf

registry {
	type = "nacos" 
	nacos { 
	serverAddr = "localhost" 
	namespace = "public" 
	cluster = "default" 
	} 
	}
	config { 
	type = "nacos" 
	nacos { 
	serverAddr = "localhost" 
	namespace = "public" 
	cluster = "default"
	} 
	}
複製代碼

bootstrap.yaml

spring: 
application: 
name: service-product 
cloud: 
nacos: 
config: 
server-addr: localhost:8848 # nacos的服務端地址 
namespace: public 
group: SEATA_GROUP 
alibaba: 
seata: 
tx-service-group: ${
	spring.application.name
}
複製代碼

在order微服務開啓全局事務

@GlobalTransactional//全局事務控制 

public Order createOrder(Integer pid) {}
複製代碼

測試

再次下單測試

seata運行流程分析

要點說明:

  1. 每一個RM使用DataSourceProxy鏈接數據庫,其目的是使用ConnectionProxy,使用數據源和數據鏈接代理的目的就是在第一階段將undo_log和業務數據放在一個本地事務提交,這樣就保存了只要有業務操做就必定有undo_log。

  2. 在第一階段undo_log中存放了數據修改前和修改後的值,爲事務回滾做好準備,因此第一階段完成就已經將分支事務提交,也就釋放了鎖資源。

  3. TM開啓全局事務開始,將XID全局事務id放在事務上下文中,經過feign調用也將XID傳入下游分支事務,每一個分支事務將本身的Branch ID分支事務ID與XID關聯。

  4. 第二階段全局事務提交,TC會通知各各分支參與者提交分支事務,在第一階段就已經提交了分支事務,這裏各各參與者只須要刪除undo_log便可,而且能夠異步執行,第二階段很快能夠完成。

  5. 第二階段全局事務回滾,TC會通知各各分支參與者回滾分支事務,經過 XID 和 Branch ID 找到相應的回滾日誌,經過回滾日誌生成反向的 SQL 並執行,以完成分支事務回滾到以前的狀態,若是回滾失敗則會重試回滾操做

看完三件事❤️

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。
  2. 關注公衆號 『 阿風的架構筆記 』,不按期分享原創知識。
  3. 同時能夠期待後續文章ing🚀
  4. 關注後回覆【666】掃碼便可獲取架構進階學習資料包
相關文章
相關標籤/搜索