seata-快速使用

seata-快速使用

什么是分布式事務(wù)?

隨著互聯(lián)網(wǎng)的快速發(fā)展,軟件系統(tǒng)由原來的單體應(yīng)用轉(zhuǎn)變?yōu)榉植际綉?yīng)用:
傳統(tǒng)單體Web應(yīng)用


傳統(tǒng)單體應(yīng)用

拆分后的架構(gòu)


微服務(wù)架構(gòu)

分布式系統(tǒng)會把一個應(yīng)用系統(tǒng)拆分為可獨立部署的多個服務(wù)(通常一個服務(wù)對應(yīng)著一個DB),因此需要服務(wù)與服務(wù)之間遠程協(xié)作才能完成事務(wù)操作,這種分布式系統(tǒng)環(huán)境下由不同的服務(wù)之間通過網(wǎng)絡(luò)協(xié)作完成事務(wù)稱為分布式事務(wù)范嘱。例如,商品添加员魏,加庫存丑蛤,創(chuàng)建訂單減庫存等都是分布式事務(wù)。

分布式事務(wù)產(chǎn)生的場景

  1. 比較典型的場景就是微服務(wù)架構(gòu)撕阎,微服務(wù)之間通過遠程調(diào)用完成事務(wù)操作受裹。上邊的例子是訂單服務(wù)和庫存服務(wù),再比如商品服務(wù)和庫存服務(wù)虏束。也同樣會出現(xiàn)分布式事務(wù)問題棉饶。創(chuàng)建商品的同時,需要請求庫存服務(wù)增加商品镇匀。簡而言之就是:跨JVM進程產(chǎn)生分布式事務(wù)照藻。
微服務(wù)架構(gòu)
  1. 單體系統(tǒng)訪問多個數(shù)據(jù)庫實例,當單體系統(tǒng)需要訪問多個數(shù)據(jù)庫實例時就會產(chǎn)生分布式事務(wù)汗侵。比如用戶管理系統(tǒng)幸缕,用戶信息和訂單信息分別在兩個MySQL實例中存儲,用戶關(guān)系系統(tǒng)刪除用戶信息晃择,需要同時刪除用戶個人信息以及訂單信息冀值。由于數(shù)據(jù)庫在不同的數(shù)據(jù)庫實例,需要通過不同的數(shù)據(jù)庫連接去操作數(shù)據(jù)宫屠,此時產(chǎn)生分布式事務(wù)。簡而言之就是:跨數(shù)據(jù)庫實例產(chǎn)生分布式事務(wù)滑蚯。


    image.png
  1. 多服務(wù)訪問同一數(shù)據(jù)庫實例浪蹂,比如商品微服務(wù)和庫存微服務(wù)即使訪問同一個數(shù)據(jù)庫實例也會產(chǎn)生分布式事務(wù)抵栈,原因就是跨JVM進程,兩個微服務(wù)持有了不同的數(shù)據(jù)庫連接進行數(shù)據(jù)庫操作坤次,此時產(chǎn)生分布式事務(wù)古劲。
image.png

分布式事務(wù)解決方案

  1. 剛性事務(wù):
    標準分布式事務(wù)(2pc/3pc)

  2. 柔性事務(wù):
    可靠消息最終一致性
    TCC
    最大努力通知
    純補償性

本篇著重介紹標準分布式事務(wù)(2pc)解決方案,seata缰猴。
在開始介紹之前产艾,先來看一個例子。

案例

案例環(huán)境

我在本地創(chuàng)建了3個項目滑绒,分別是product商品服務(wù), stock庫存服務(wù)闷堡,admin統(tǒng)一的對外服務(wù)。每個服務(wù)單獨對應(yīng)一個數(shù)據(jù)庫疑故。在admin中進行調(diào)用product添加商品杠览,然后調(diào)用庫存服務(wù)進行添加庫存。
三個服務(wù)分別對應(yīng)著不同的數(shù)據(jù)庫纵势,由于是演示出分布式事務(wù)出現(xiàn)的問題踱阿,所以表結(jié)構(gòu)相對簡單。
pm_product表

CREATE TABLE `pm_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `prod_name` varchar(255) NOT NULL COMMENT '商品名稱',
  `model` varchar(255) NOT NULL COMMENT '商品型號',
  `price` decimal(20,10) NOT NULL '商品價格',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

pm_stock表

CREATE TABLE `pm_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `prod_id` bigint(20) NOT NULL COMMENT '商品ID',
  `quantity` int(11) NOT NULL '商品數(shù)量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;

admin服務(wù)中核心調(diào)用邏輯如下:

public ServerResponse saveProduct(ProductAddReq productAddReq) {
        ProductReq productReq = new ProductReq();
        productReq.setModel(productAddReq.getModel());
        productReq.setPrice(productAddReq.getPrice());
        productReq.setProdName(productAddReq.getProdName());
        // 添加商品 調(diào)用商品服務(wù)
        ServerResponse<Long> serverResponse = productFeign.saveProduct(productReq);
        if (!serverResponse.isSuccess() || serverResponse.getData() == null) {
            return serverResponse;
        }
        // 添加庫存 調(diào)用庫存服務(wù)
        StockReq stockReq = new StockReq();
        stockReq.setProdId(serverResponse.getData());
        stockReq.setQuantity(productAddReq.getQuantity());

        ServerResponse stockResponse = stockFeign.saveStock(stockReq);
        if (!stockResponse.isSuccess()) {
            return stockResponse;
        }
        return ServerResponse.ok("商品添加成功");
}

然后啟動項目進行運行钦铁,發(fā)現(xiàn)一切正常软舌,表中對應(yīng)的數(shù)據(jù)也都正常。
stock庫中和product庫中數(shù)據(jù)一致牛曹。

問題引出

然后我們開始制造一些意外佛点,首先把表中數(shù)據(jù)情況,便于對比數(shù)據(jù)躏仇。比如此時我把stock服務(wù)關(guān)閉恋脚,然后調(diào)用admin中的接口進行添加商品盟榴,看看會發(fā)生什么情況处嫌。

POST http://localhost:8080/product
Content-Type: application/json

{
  "prodName": "商品",
  "price": 23,
  "model": "",
  "quantity": 100
}

很顯然報錯了,返回前端錯誤伙单,但是此時數(shù)據(jù)庫里面已經(jīng)產(chǎn)生了臟數(shù)據(jù)书妻。


報錯

pm_product表


pm_product

pm_stock表


pm_stock

可以發(fā)現(xiàn)pm_product表中插入了一條數(shù)據(jù)船响,而pm_stock表中沒有數(shù)據(jù),此時明顯是不正確的躲履,pm_product表中數(shù)據(jù)屬于臟數(shù)據(jù)见间。
產(chǎn)生這種情況的現(xiàn)象,還有很多種工猜,例如米诉,庫存服務(wù)中有個異常,然后導(dǎo)致庫存服務(wù)本地事務(wù)回滾篷帅,但是商品服務(wù)已經(jīng)插進去了史侣。還有就是admin服務(wù)中在調(diào)用完商品服務(wù)之后報異常拴泌,會導(dǎo)致報錯,商品服務(wù)插入數(shù)據(jù)成功惊橱,但是庫存服務(wù)沒有被調(diào)用蚪腐,也會產(chǎn)生臟數(shù)據(jù)。

解決方案

那么該如何解決這種情況呢税朴?
解決這種情況的方案有很多種回季,這里只介紹基于兩階段提交的seata方案。
seata是由阿里中間件團隊發(fā)起的開源項目Fescar正林,后更名為seata泡一,它是一個開源的分布式事務(wù)框架、傳統(tǒng)2PC的問題在Seata中得到了解決卓囚,它通過對本地關(guān)系數(shù)據(jù)庫的分支事務(wù)的協(xié)調(diào)來驅(qū)動完成全局事務(wù)瘾杭,是工作在應(yīng)用層的中間件,主要的優(yōu)點是性能較好哪亿,且不長時間占用資源粥烁,它以高效并且對業(yè)務(wù)0侵入的方式解決微服務(wù)場景下面臨的分布式事務(wù)問題。它的宗旨是像解決本地事務(wù)一樣蝇棉,來解決分布式事務(wù)讨阻。好,現(xiàn)在來看一下如何來快速使用seata篡殷。關(guān)于原理性的東西這里先不做過多介紹钝吮,先著重看一下如何將seata引入到項目中來。

seata方案

1.引入pom依賴板辽。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

2.添加一個配置類

@Configuration
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
    @Primary
    @Bean("dataSourceProxy")
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
}

3.配置seata-server奇瘦。將上述配置加入到每個工程中。admin服務(wù)可以不用加劲弦,只加在需要操作DB的服務(wù)中耳标。好,下面還需要一個配置一下seata-server邑跪〈纹拢可以把它當成一個全局事務(wù)的協(xié)調(diào)器。具體的下載地址如下:

https://seata.io/zh-cn/blog/download.html

下下來之后画畅,其目錄結(jié)構(gòu)如下:


image.png

現(xiàn)在來簡單說明一下每個目錄下的文件砸琅,bin目錄下主要是啟動腳本,windows用戶啟動.bat文件,Linux和Mac用戶啟動.sh文件轴踱。conf目錄主要是一些配置文件症脂。

1. file.conf 主要是一些服務(wù)端和客戶端的一些配置。
2. registry.conf 主要是seata-server的注冊方式。它可以注冊到
redis,zk,eureka中摊腋,來對它進行管理沸版。

本案例主要使用eureka來進行注冊嘁傀。然后對這倆文件進行更改兴蒸。
在registry.conf中僅需更改eureka地址,然后把應(yīng)用名稱也設(shè)置一下细办。

eureka {
    serviceUrl = "http://127.0.0.1:8761/eureka"
    application = "seata-server"
    weight = "1"
}

之后更改file文件,首先更改store部分橙凳,這是seata-server在運行過程中,需要將運行時的一些臨時數(shù)據(jù)存儲的地方笑撞。這里我將它們存儲在DB中岛啸,需要配置一下地址等。

store {
  ## store mode: file茴肥、db 存儲模式坚踩,選擇DB
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
  }

  ## database store property 數(shù)據(jù)庫的一些配置
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "chusen"
  }
}

4.建庫建表。然后需要在數(shù)據(jù)庫中創(chuàng)建一個seata庫瓤狐,之后創(chuàng)建一些seata-server運行時需要的一些表瞬铸。

CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(128) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `lock_key` varchar(256) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(64) DEFAULT NULL,
  `transaction_service_group` varchar(64) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,
  `xid` varchar(128) DEFAULT NULL,
  `transaction_id` mediumtext,
  `branch_id` mediumtext,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(128) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

seata庫下一共三個表。
然后還需要在每個服務(wù)對應(yīng)的庫下創(chuàng)建一個undo_log表础锐。用戶記錄分支事務(wù)數(shù)據(jù)嗓节。

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 DEFAULT CHARSET=utf8;

數(shù)據(jù)庫建表到此為止。
5.下面再來簡單配置一下每個服務(wù)皆警。在每個服務(wù)的spring配置文件中加入一行配置

spring.cloud.alibaba.seata.tx-service-group = tx_group

然后進行更改seata-server中的file配置文件

service {
  #transaction service group mapping
  ## 這里tx_group對應(yīng)上面配置的tx——group拦宣。seata-server對應(yīng)上面eureka中注冊的應(yīng)用名稱
  vgroup_mapping.tx_group = "seata-server"
  #only support when registry.type=file, please don't set multiple addresses
  seata-server.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

最后將seata-server中conf下更改過的file.conf和registry.conf這兩個文件復(fù)制到每個服務(wù)的resources目錄下面。大功告成信姓。首先啟動seata-server鸵隧。
然后分別啟動每個服務(wù)。啟動之后意推,可以在seata-server控制臺打印的日志可以看到豆瘫。以admin服務(wù)為例。


image.png

然后重新訪問下接口左痢,發(fā)現(xiàn)一切正常靡羡,之后將stock服務(wù)關(guān)掉,看pm_product中是否還會產(chǎn)生臟數(shù)據(jù)俊性。結(jié)果大失所望略步,不生效,這是為什么呢定页?
因為還差很關(guān)鍵的一步趟薄,因為你要讓seata知道,要為哪個方法添加全局事務(wù)典徊。很簡單杭煎,在剛才的方法上面添加一個注解即可恩够。這體現(xiàn)了seata對業(yè)務(wù)代碼0侵入,真的像使用本地事務(wù)一樣羡铲。

@GlobalTransactional
public ServerResponse saveProduct(ProductAddReq productAddReq) {
        ProductReq productReq = new ProductReq();
        productReq.setModel(productAddReq.getModel());
        productReq.setPrice(productAddReq.getPrice());
        productReq.setProdName(productAddReq.getProdName());
        // 添加商品
        ServerResponse<Long> serverResponse = productFeign.saveProduct(productReq);
        if (!serverResponse.isSuccess() || serverResponse.getData() == null) {
            return serverResponse;
        }
        // 添加庫存
        StockReq stockReq = new StockReq();
        stockReq.setProdId(serverResponse.getData());
        stockReq.setQuantity(productAddReq.getQuantity());

        ServerResponse stockResponse = stockFeign.saveStock(stockReq);
        if (!stockResponse.isSuccess()) {
            return stockResponse;
        }
        return ServerResponse.ok("商品添加成功");
}

然后進行測試蜂桶,大功告成!當運行時也切,出現(xiàn)異常時扑媚,分支事務(wù)會被回滾±资眩可以在seata-server的控制臺看到相關(guān)日志疆股。


image.png

當沒有異常時,可以看到全局事務(wù)會被提交倒槐。


image.png

小結(jié)

1.在使用微服務(wù)架構(gòu)時旬痹,既帶來了很多優(yōu)勢,同時也帶來了很多新的挑戰(zhàn)讨越,這其中就包括分布式事務(wù)問題两残。
2.解決分布式事務(wù)問題,有很多種解決方案谎痢,本篇著重介紹了基于2pc提交的seata方案的使用磕昼。
3.需要注意的是,每個服務(wù)下面的配置文件需要和seata-server中的配置文件一致节猿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末票从,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子滨嘱,更是在濱河造成了極大的恐慌峰鄙,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件太雨,死亡現(xiàn)場離奇詭異吟榴,居然都是意外死亡,警方通過查閱死者的電腦和手機囊扳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門吩翻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锥咸,你說我怎么就攤上這事狭瞎。” “怎么了搏予?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵熊锭,是天一觀的道長。 經(jīng)常有香客問我,道長碗殷,這世上最難降的妖魔是什么精绎? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮锌妻,結(jié)果婚禮上代乃,老公的妹妹穿的比我還像新娘。我一直安慰自己从祝,他們只是感情好襟己,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著牍陌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪员咽。 梳的紋絲不亂的頭發(fā)上毒涧,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音贝室,去河邊找鬼契讲。 笑死,一個胖子當著我的面吹牛滑频,可吹牛的內(nèi)容都是我干的捡偏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼峡迷,長吁一口氣:“原來是場噩夢啊……” “哼银伟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绘搞,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤彤避,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后夯辖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琉预,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年蒿褂,在試婚紗的時候發(fā)現(xiàn)自己被綠了圆米。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡啄栓,死狀恐怖娄帖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谴供,我是刑警寧澤块茁,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響数焊,放射性物質(zhì)發(fā)生泄漏永淌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一佩耳、第九天 我趴在偏房一處隱蔽的房頂上張望遂蛀。 院中可真熱鬧,春花似錦干厚、人聲如沸李滴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽所坯。三九已至,卻和暖如春挂捅,著一層夾襖步出監(jiān)牢的瞬間芹助,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工闲先, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留状土,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓伺糠,卻偏偏與公主長得像蒙谓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子训桶,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容