一步一步教你:用 Docker Compose 完成 Seata 的整合部署
在當(dāng)今分布式系統(tǒng)大行其道的時(shí)代,確保數(shù)據(jù)在不同服務(wù)間的一致性和完整性,已然成為開發(fā)者們面臨的關(guān)鍵挑戰(zhàn)之一。分布式事務(wù)管理因此成為了構(gòu)建可靠、高效分布式應(yīng)用不可或缺的一部分。
Seata,作為一款優(yōu)秀的分布式事務(wù)解決方案,為解決分布式環(huán)境下的數(shù)據(jù)一致性問題提供了強(qiáng)大的支持。它致力于提供高性能且易于使用的分布式事務(wù)服務(wù),幫助開發(fā)者輕松應(yīng)對復(fù)雜的業(yè)務(wù)場景。 而 Docker Compose,無疑是容器編排領(lǐng)域的得力助手。它能夠讓我們通過一個(gè)簡單的配置文件,便捷地定義和運(yùn)行多個(gè)相互關(guān)聯(lián)的 Docker 容器,極大地提升了開發(fā)和部署的效率,簡化了環(huán)境管理的復(fù)雜度。 那么,當(dāng)功能強(qiáng)大的 Seata 遇上便捷高效的 Docker Compose,會碰撞出怎樣的火花呢?
在本文中,我們將深入探索基于 Docker Compose 整合 Seata 的全過程,不僅會詳細(xì)介紹每一個(gè)步驟,還會剖析其中的關(guān)鍵要點(diǎn)和潛在問題。無論你是分布式系統(tǒng)開發(fā)的新手,渴望深入了解分布式事務(wù)管理,還是經(jīng)驗(yàn)豐富的開發(fā)者,希望借助容器技術(shù)優(yōu)化現(xiàn)有架構(gòu),相信本文都能為你帶來有價(jià)值的參考和啟發(fā)。
詳解docker-compose整合seata步驟
1. 實(shí)踐案例描述
我們用一共比較簡單的購物下單的實(shí)例介紹一下本文示例的業(yè)務(wù),當(dāng)前業(yè)務(wù)用戶通過接口提交訂單請求,對應(yīng)參數(shù)為如下,分別是用戶賬號id、產(chǎn)品id、購買的產(chǎn)品數(shù)量:
{
"accountCode": "0932897",
"productCode": "P001",
"count": 1
}
然后執(zhí)行如下步驟:
- 基于傳入的商品id和數(shù)量計(jì)算售價(jià)。
- 基于用戶id和商品id、數(shù)量生成訂單信息。
- 基于售價(jià)信息到用戶賬戶上進(jìn)行扣減。
- 基于傳入的數(shù)量和商品id進(jìn)行庫存扣減。
需要注意的是上述的創(chuàng)建訂單、余額扣減、庫存扣減分別對應(yīng)3個(gè)服務(wù),這也就意味著一個(gè)原子業(yè)務(wù)需要在分布式環(huán)境下保證如下幾個(gè)業(yè)務(wù)合理性:
- 用戶余額不足扣款直接回滾,將生成的訂單信息銷毀。
- 商品庫存不足同樣回滾訂單和庫存扣減。
所以我們需要借助seata來實(shí)現(xiàn)分布式事務(wù)以保證分布式事務(wù)的ACID:
這里我們先給出對應(yīng)的業(yè)務(wù)代碼,后續(xù)我們將通過seata保證這塊分布式事務(wù)的ACID:
public void createOrder(OrderDto orderDTO) {
Order order = new Order();
BeanUtils.copyProperties(orderDTO, order);
//調(diào)用產(chǎn)品服務(wù)獲取商品詳情
ResultData<ProductDTO> productInfo = productFeign.getByCode(orderDTO.getProductCode());
//計(jì)算總金額
BigDecimal total = productInfo.getData().getPrice().multiply(new BigDecimal(order.getCount()));
order.setAmount(total);
//創(chuàng)建訂單
save(order);
//扣減金額
accountFeign.reduceAccount(orderDTO.getAccountCode(), order.getAmount());
//扣減商品
productFeign.deduct(orderDTO.getProductCode(), orderDTO.getCount());
}
2. 容器編排和基礎(chǔ)環(huán)境配置
既然要用到docker-compose,所以我們就需要創(chuàng)建一個(gè)yml文件,以筆者為例創(chuàng)建一個(gè)名為seata-compose.yaml的文件,筆者都已給出注釋,內(nèi)容如下:
version: "3"
services:
seata-server:
image: seataio/seata-server:1.4.2
ports:
# 內(nèi)外部端口映射
- "8091:8091"
environment:
# 端口號和seata的ip地址
- SEATA_PORT=8091
- SEATA_IP=x.x.x.x
volumes:
# 宿主和容器之間registry.conf文件映射地址
- "/usr/local/seata/seata-config/registry.conf:/seata-server/resources/registry.conf"
# 宿主和容器之間file.conf文件映射地址
- "/usr/local/seata/seata-config/file.conf:/seata-server/resources/file.conf"
expose:
# 暴露端口號
- 8091
# 容器名稱
container_name: seata-server
可以看到筆者上文配置中registry.conf宿主存放位置在/usr/local/seata/seata-config/,所以我們需要在這個(gè)位置創(chuàng)建registry.conf,以筆者為例,這個(gè)registry.conf內(nèi)容如下,可以看到筆者指明了注冊中心的地址、命名空間id以及分組名。
這里唯一需要的注意的就是registry 上指明的cluster 集群節(jié)點(diǎn)名稱,該配置會將seata綁定到nacos對應(yīng)的default節(jié)點(diǎn)上:
registry {
# 將seata注冊到nacos上
type = "nacos"
nacos {
# nacos地址
serverAddr = "ip:8848"
# 命名空間id
namespace = "63f0dbe6-ac91-4a2e-a88e-82b76f8187b6"
# 組名
group = "DEFAULT_GROUP"
# 集群節(jié)點(diǎn)名稱
cluster = "default"
}
}
config {
# 通過nacos獲取配置
type = "nacos"
nacos {
serverAddr = "ip:8848"
namespace = "63f0dbe6-ac91-4a2e-a88e-82b76f8187b6"
group = "DEFAULT_GROUP"
}
}
3. 將seata常規(guī)配置存到nacos中
完成上述步驟后,我們的seata已經(jīng)可以注冊到nacos上了。只不過我們還需要在上述的命名空間(63f0dbe6-ac91-4a2e-a88e-82b76f8187b6)創(chuàng)建一個(gè)seataServer.properties的配置文件,將seata存儲設(shè)置為MySQL存儲,對應(yīng)的配置如下注釋所示:
store.mode=db
#-----db-----
store.db.datasource=druid
store.db.dbType=mysql
# 需要根據(jù)mysql的版本調(diào)整driverClassName
# mysql8及以上版本對應(yīng)的driver:com.mysql.cj.jdbc.Driver
# mysql8以下版本的driver:com.mysql.jdbc.Driver
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://ip:3306/seata?useUnicode=true&characterEncoding=utf8&cnotallow=1000&socketTimeout=3000&autoRecnotallow=true&useSSL=false
store.db.user= 用戶
store.db.password= 數(shù)據(jù)庫密碼
# 數(shù)據(jù)庫初始連接數(shù)
store.db.minCnotallow=1
# 數(shù)據(jù)庫最大連接數(shù)
store.db.maxCnotallow=20
# 獲取連接時(shí)最大等待時(shí)間 默認(rèn)5000,單位毫秒
store.db.maxWait=5000
# 全局事務(wù)表名 默認(rèn)global_table
store.db.globalTable=global_table
# 分支事務(wù)表名 默認(rèn)branch_table
store.db.branchTable=branch_table
# 全局鎖表名 默認(rèn)lock_table
store.db.lockTable=lock_table
# 查詢?nèi)质聞?wù)一次的最大條數(shù) 默認(rèn)100
store.db.queryLimit=100
# undo保留天數(shù) 默認(rèn)7天,log_status=1(附錄3)和未正常清理的undo
server.undo.logSaveDays=7
# undo清理線程間隔時(shí)間 默認(rèn)86400000,單位毫秒
server.undo.logDeletePeriod=86400000
# 二階段提交重試超時(shí)時(shí)長 單位ms,s,m,h,d,對應(yīng)毫秒,秒,分,小時(shí),天,默認(rèn)毫秒。默認(rèn)值-1表示無限重試
# 公式: timeout>=now-globalTransactionBeginTime,true表示超時(shí)則不再重試
# 注: 達(dá)到超時(shí)時(shí)間后將不會做任何重試,有數(shù)據(jù)不一致風(fēng)險(xiǎn),除非業(yè)務(wù)自行可校準(zhǔn)數(shù)據(jù),否者慎用
server.maxCommitRetryTimeout=-1
# 二階段回滾重試超時(shí)時(shí)長
server.maxRollbackRetryTimeout=-1
# 二階段提交未完成狀態(tài)全局事務(wù)重試提交線程間隔時(shí)間 默認(rèn)1000,單位毫秒
server.recovery.committingRetryPeriod=1000
# 二階段異步提交狀態(tài)重試提交線程間隔時(shí)間 默認(rèn)1000,單位毫秒
server.recovery.asynCommittingRetryPeriod=1000
# 二階段回滾狀態(tài)重試回滾線程間隔時(shí)間 默認(rèn)1000,單位毫秒
server.recovery.rollbackingRetryPeriod=1000
# 超時(shí)狀態(tài)檢測重試線程間隔時(shí)間 默認(rèn)1000,單位毫秒,檢測出超時(shí)將全局事務(wù)置入回滾會話管理器
server.recovery.timeoutRetryPeriod=1000
# 指定SeaTa的命名空間
seata.config.nacos.namespace=63f0dbe6-ac91-4a2e-a88e-82b76f8187b6
4. 創(chuàng)建seata配置庫存儲分支事務(wù)和全局事務(wù)表
上文配置中我們指明一個(gè)名為seata_config的數(shù)據(jù)庫,所以我們就需要到創(chuàng)建一個(gè)名為seata_config的數(shù)據(jù)庫并刷入分支事務(wù)和全局事務(wù)表以及l(fā)ock_table表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事務(wù)表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全局事務(wù)表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96) DEFAULT NULL,
`transaction_id` bigint DEFAULT NULL,
`branch_id` bigint NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
SET FOREIGN_KEY_CHECKS = 1;
5. 創(chuàng)建undo.log表
上述步驟我們完成了seata相關(guān)的功能維護(hù)的配置,以本文的AT模式為例,為保證每個(gè)分支事務(wù)在回滾時(shí)都能準(zhǔn)確還原,seata參照MySQL的mvcc設(shè)計(jì)思想提出undo.log的概念,如果需要實(shí)現(xiàn)AT模式,我們需要針對每一個(gè)分支事務(wù)的數(shù)據(jù)庫刷入下面這張undo_log表:
-- 日志文件表--
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
6. 啟動seata將其注冊到nacos中
完成這些步驟之后,我們就可以啟動seata容器查看是否注冊到容器中,我們在seata-compose.yaml文件所在的路徑鍵入這條命令:
docker-compose -f seata-compose.yaml up -d
完成這條命令后,我們通過docker ps獲取到seata的id值,以筆者為例,容器的id為8c48c75d07ad,所以我們鍵入:
docker logs 8c48c75d07ad
如下圖所示,如果正常讀取到registry.conf文件以及輸出端口號,就說明啟動成功了。
查看nacos對應(yīng)命名空間的服務(wù)列表,可以看到seata-server已經(jīng)成功注冊了,自此我們的seata就已經(jīng)部署成功了。
7. 服務(wù)注冊到seata
完成這些步驟后,我們就可以將本地服務(wù)注冊到seata中,首先服務(wù)必須引入依賴seata-spring-boot-starter,只有引入這個(gè)依賴才會自動裝配seata相關(guān)組件確保服務(wù)可以注冊到seata中。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本較低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--版本在父工程中配置,seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
然后修改每個(gè)服務(wù)的yml文件配置,如下所示,這里需要注意一點(diǎn),因?yàn)楣P者在上文seataServer.properties指定事務(wù)分組名稱為seata-demo,所以我們這里的tx-service-group也是seata-demo。然后vgroup-mapping也指明seata-demo和我們nacos集群(筆者在上文registry.conf將cluster配置為default)的映射關(guān)系。
具體配置如下所示:
seata:
# TC服務(wù)注冊中心的配置,微服務(wù)根據(jù)這些信息去注冊中心獲取tc服務(wù)地址
registry:
# 注冊中心類型 nacos
type: nacos
nacos:
# nacos地址
server-addr: ip:8848
# namespace,默認(rèn)為空
namespace: 63f0dbe6-ac91-4a2e-a88e-82b76f8187b6
# 配置組
group: DEFAULT_GROUP
# seata服務(wù)名稱
application: seata-server
username: nacos
password: 密碼
config:
type: nacos
nacos:
server-addr: ip:8848
group : "DEFAULT_GROUP"
namespace: "63f0dbe6-ac91-4a2e-a88e-82b76f8187b6"
dataId: "seataServer.properties"
username: "nacos"
password: "密碼"
# 事務(wù)組名稱
tx-service-group: seata-demo
service:
vgroup-mapping: # 事務(wù)組與cluster的映射關(guān)系
seata-demo: default
grouplist.seata-server: ip:8091
data-source-proxy-mode: AT
注意yml文件對縮進(jìn)的格式要求很高,讀者可以參考筆者的配置進(jìn)行修改,筆者本次部署時(shí)遇到服務(wù)始終無法注冊到seata中,控制臺持續(xù)輸出can not get cluster name in registry config xxxx, please make sure registry config correct
經(jīng)過查閱源碼NettyClientChannelManager的代碼段,大抵推測yml配置沒有生效,排查半天得出yml縮進(jìn)有問題。
//筆者這里debug進(jìn)去發(fā)現(xiàn)group取的SEATA-GROUP和我們的指定的DEFAULT-GROUP不一樣
String clusterName = registryService.getServiceGroup(transactionServiceGroup);
if (StringUtils.isBlank(clusterName)) {
LOGGER.error("can not get cluster name in registry config '{}{}', please make sure registry config correct",
ConfigurationKeys.SERVICE_GROUP_MAPPING_PREFIX,
transactionServiceGroup);
return;
}
筆者查閱github一些issue發(fā)現(xiàn),上面這個(gè)問題可能還需要補(bǔ)充這樣一個(gè)步驟:
在上文配置的命名空間中增加一條配置,data-id為service.vgroupMapping.事務(wù)分組名稱,以筆者為例就是service.vgroupMapping.seata-demo,內(nèi)容為default
8. 啟動服務(wù)將其注冊到seata中
完成后啟動服務(wù),以筆者的order-service為例,啟動后如果seata日志中輸出這樣一段話,則說明啟動成功了。
16:31:15.596 INFO --- [rverHandlerThread_1_1_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://ip:3306/cloud_alibaba', applicatinotallow='order-service', transactinotallow='seata-demo'},channel:[id: 0x25ec75ef, L:/172.23.0.5:8091 - R:/220.200.39.1:30735],client version:1.4.2
16:31:15.732 INFO --- [rverHandlerThread_1_2_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://ip:3306/cloud_alibaba', applicatinotallow='order-service', transactinotallow='seata-demo'},channel:[id: 0x25ec75ef, L:/172.23.0.5:8091 - R:/220.200.39.1:30735],client version:1.4.2
其他服務(wù)同理,都完成后,我們的下單服務(wù)代碼加一個(gè)GlobalTransactional注解,即可完成分布式事務(wù)了,感興趣的讀者可以在扣款或者庫存扣減調(diào)用上設(shè)置一個(gè)錯(cuò)誤的調(diào)用,最終都會看到訂單回滾且當(dāng)前用戶的余額和庫存都不會有扣減:
@Override
@GlobalTransactional
@Transactional(rollbackFor = RuntimeException.class)
public void createOrder(OrderDto orderDTO) {
Order order = new Order();
BeanUtils.copyProperties(orderDTO, order);
//調(diào)用產(chǎn)品服務(wù)獲取商品詳情
ResultData<ProductDTO> productInfo = productFeign.getByCode(orderDTO.getProductCode());
//計(jì)算總金額
BigDecimal total = productInfo.getData().getPrice().multiply(new BigDecimal(order.getCount()));
order.setAmount(total);
//創(chuàng)建訂單
save(order);
//扣減金額
accountFeign.reduceAccount(orderDTO.getAccountCode(), order.getAmount());
//扣減商品
productFeign.deduct(orderDTO.getProductCode(), orderDTO.getCount());
}