seata-快速使用
什么是分布式事務(wù)?
隨著互聯(lián)網(wǎng)的快速發(fā)展,軟件系統(tǒng)由原來的單體應(yīng)用轉(zhuǎn)變?yōu)榉植际綉?yīng)用:
傳統(tǒng)單體Web應(yīng)用
拆分后的架構(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)生的場景
- 比較典型的場景就是微服務(wù)架構(gòu)撕阎,微服務(wù)之間通過遠程調(diào)用完成事務(wù)操作受裹。上邊的例子是訂單服務(wù)和庫存服務(wù),再比如商品服務(wù)和庫存服務(wù)虏束。也同樣會出現(xiàn)分布式事務(wù)問題棉饶。創(chuàng)建商品的同時,需要請求庫存服務(wù)增加商品镇匀。簡而言之就是:跨JVM進程產(chǎn)生分布式事務(wù)照藻。
-
單體系統(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
- 多服務(wù)訪問同一數(shù)據(jù)庫實例浪蹂,比如商品微服務(wù)和庫存微服務(wù)即使訪問同一個數(shù)據(jù)庫實例也會產(chǎn)生分布式事務(wù)抵栈,原因就是跨JVM進程,兩個微服務(wù)持有了不同的數(shù)據(jù)庫連接進行數(shù)據(jù)庫操作坤次,此時產(chǎn)生分布式事務(wù)古劲。
分布式事務(wù)解決方案
剛性事務(wù):
標準分布式事務(wù)(2pc/3pc)柔性事務(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_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)如下:
現(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ù)為例。
然后重新訪問下接口左痢,發(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)日志疆股。
當沒有異常時,可以看到全局事務(wù)會被提交倒槐。
小結(jié)
1.在使用微服務(wù)架構(gòu)時旬痹,既帶來了很多優(yōu)勢,同時也帶來了很多新的挑戰(zhàn)讨越,這其中就包括分布式事務(wù)問題两残。
2.解決分布式事務(wù)問題,有很多種解決方案谎痢,本篇著重介紹了基于2pc提交的seata方案的使用磕昼。
3.需要注意的是,每個服務(wù)下面的配置文件需要和seata-server中的配置文件一致节猿。