一步一步教你:用 Docker Compose 完成 Seata 的整合部署
在當今分布式系統大行其道的時代,確保數據在不同服務間的一致性和完整性,已然成為開發者們面臨的關鍵挑戰之一。分布式事務管理因此成為了構建可靠、高效分布式應用不可或缺的一部分。
Seata,作為一款優秀的分布式事務解決方案,為解決分布式環境下的數據一致性問題提供了強大的支持。它致力于提供高性能且易于使用的分布式事務服務,幫助開發者輕松應對復雜的業務場景。 而 Docker Compose,無疑是容器編排領域的得力助手。它能夠讓我們通過一個簡單的配置文件,便捷地定義和運行多個相互關聯的 Docker 容器,極大地提升了開發和部署的效率,簡化了環境管理的復雜度。 那么,當功能強大的 Seata 遇上便捷高效的 Docker Compose,會碰撞出怎樣的火花呢?
在本文中,我們將深入探索基于 Docker Compose 整合 Seata 的全過程,不僅會詳細介紹每一個步驟,還會剖析其中的關鍵要點和潛在問題。無論你是分布式系統開發的新手,渴望深入了解分布式事務管理,還是經驗豐富的開發者,希望借助容器技術優化現有架構,相信本文都能為你帶來有價值的參考和啟發。
詳解docker-compose整合seata步驟
1. 實踐案例描述
我們用一共比較簡單的購物下單的實例介紹一下本文示例的業務,當前業務用戶通過接口提交訂單請求,對應參數為如下,分別是用戶賬號id、產品id、購買的產品數量:
{
"accountCode": "0932897",
"productCode": "P001",
"count": 1
}
然后執行如下步驟:
- 基于傳入的商品id和數量計算售價。
- 基于用戶id和商品id、數量生成訂單信息。
- 基于售價信息到用戶賬戶上進行扣減。
- 基于傳入的數量和商品id進行庫存扣減。
需要注意的是上述的創建訂單、余額扣減、庫存扣減分別對應3個服務,這也就意味著一個原子業務需要在分布式環境下保證如下幾個業務合理性:
- 用戶余額不足扣款直接回滾,將生成的訂單信息銷毀。
- 商品庫存不足同樣回滾訂單和庫存扣減。
所以我們需要借助seata來實現分布式事務以保證分布式事務的ACID:
這里我們先給出對應的業務代碼,后續我們將通過seata保證這塊分布式事務的ACID:
public void createOrder(OrderDto orderDTO) {
Order order = new Order();
BeanUtils.copyProperties(orderDTO, order);
//調用產品服務獲取商品詳情
ResultData<ProductDTO> productInfo = productFeign.getByCode(orderDTO.getProductCode());
//計算總金額
BigDecimal total = productInfo.getData().getPrice().multiply(new BigDecimal(order.getCount()));
order.setAmount(total);
//創建訂單
save(order);
//扣減金額
accountFeign.reduceAccount(orderDTO.getAccountCode(), order.getAmount());
//扣減商品
productFeign.deduct(orderDTO.getProductCode(), orderDTO.getCount());
}
2. 容器編排和基礎環境配置
既然要用到docker-compose,所以我們就需要創建一個yml文件,以筆者為例創建一個名為seata-compose.yaml的文件,筆者都已給出注釋,內容如下:
version: "3"
services:
seata-server:
image: seataio/seata-server:1.4.2
ports:
# 內外部端口映射
- "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/,所以我們需要在這個位置創建registry.conf,以筆者為例,這個registry.conf內容如下,可以看到筆者指明了注冊中心的地址、命名空間id以及分組名。
這里唯一需要的注意的就是registry 上指明的cluster 集群節點名稱,該配置會將seata綁定到nacos對應的default節點上:
registry {
# 將seata注冊到nacos上
type = "nacos"
nacos {
# nacos地址
serverAddr = "ip:8848"
# 命名空間id
namespace = "63f0dbe6-ac91-4a2e-a88e-82b76f8187b6"
# 組名
group = "DEFAULT_GROUP"
# 集群節點名稱
cluster = "default"
}
}
config {
# 通過nacos獲取配置
type = "nacos"
nacos {
serverAddr = "ip:8848"
namespace = "63f0dbe6-ac91-4a2e-a88e-82b76f8187b6"
group = "DEFAULT_GROUP"
}
}
3. 將seata常規配置存到nacos中
完成上述步驟后,我們的seata已經可以注冊到nacos上了。只不過我們還需要在上述的命名空間(63f0dbe6-ac91-4a2e-a88e-82b76f8187b6)創建一個seataServer.properties的配置文件,將seata存儲設置為MySQL存儲,對應的配置如下注釋所示:
store.mode=db
#-----db-----
store.db.datasource=druid
store.db.dbType=mysql
# 需要根據mysql的版本調整driverClassName
# mysql8及以上版本對應的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= 數據庫密碼
# 數據庫初始連接數
store.db.minCnotallow=1
# 數據庫最大連接數
store.db.maxCnotallow=20
# 獲取連接時最大等待時間 默認5000,單位毫秒
store.db.maxWait=5000
# 全局事務表名 默認global_table
store.db.globalTable=global_table
# 分支事務表名 默認branch_table
store.db.branchTable=branch_table
# 全局鎖表名 默認lock_table
store.db.lockTable=lock_table
# 查詢全局事務一次的最大條數 默認100
store.db.queryLimit=100
# undo保留天數 默認7天,log_status=1(附錄3)和未正常清理的undo
server.undo.logSaveDays=7
# undo清理線程間隔時間 默認86400000,單位毫秒
server.undo.logDeletePeriod=86400000
# 二階段提交重試超時時長 單位ms,s,m,h,d,對應毫秒,秒,分,小時,天,默認毫秒。默認值-1表示無限重試
# 公式: timeout>=now-globalTransactionBeginTime,true表示超時則不再重試
# 注: 達到超時時間后將不會做任何重試,有數據不一致風險,除非業務自行可校準數據,否者慎用
server.maxCommitRetryTimeout=-1
# 二階段回滾重試超時時長
server.maxRollbackRetryTimeout=-1
# 二階段提交未完成狀態全局事務重試提交線程間隔時間 默認1000,單位毫秒
server.recovery.committingRetryPeriod=1000
# 二階段異步提交狀態重試提交線程間隔時間 默認1000,單位毫秒
server.recovery.asynCommittingRetryPeriod=1000
# 二階段回滾狀態重試回滾線程間隔時間 默認1000,單位毫秒
server.recovery.rollbackingRetryPeriod=1000
# 超時狀態檢測重試線程間隔時間 默認1000,單位毫秒,檢測出超時將全局事務置入回滾會話管理器
server.recovery.timeoutRetryPeriod=1000
# 指定SeaTa的命名空間
seata.config.nacos.namespace=63f0dbe6-ac91-4a2e-a88e-82b76f8187b6
4. 創建seata配置庫存儲分支事務和全局事務表
上文配置中我們指明一個名為seata_config的數據庫,所以我們就需要到創建一個名為seata_config的數據庫并刷入分支事務和全局事務表以及lock_table表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事務表
-- ----------------------------
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;
-- ----------------------------
-- 全局事務表
-- ----------------------------
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. 創建undo.log表
上述步驟我們完成了seata相關的功能維護的配置,以本文的AT模式為例,為保證每個分支事務在回滾時都能準確還原,seata參照MySQL的mvcc設計思想提出undo.log的概念,如果需要實現AT模式,我們需要針對每一個分支事務的數據庫刷入下面這張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對應命名空間的服務列表,可以看到seata-server已經成功注冊了,自此我們的seata就已經部署成功了。
7. 服務注冊到seata
完成這些步驟后,我們就可以將本地服務注冊到seata中,首先服務必須引入依賴seata-spring-boot-starter,只有引入這個依賴才會自動裝配seata相關組件確保服務可以注冊到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>
然后修改每個服務的yml文件配置,如下所示,這里需要注意一點,因為筆者在上文seataServer.properties指定事務分組名稱為seata-demo,所以我們這里的tx-service-group也是seata-demo。然后vgroup-mapping也指明seata-demo和我們nacos集群(筆者在上文registry.conf將cluster配置為default)的映射關系。
具體配置如下所示:
seata:
# TC服務注冊中心的配置,微服務根據這些信息去注冊中心獲取tc服務地址
registry:
# 注冊中心類型 nacos
type: nacos
nacos:
# nacos地址
server-addr: ip:8848
# namespace,默認為空
namespace: 63f0dbe6-ac91-4a2e-a88e-82b76f8187b6
# 配置組
group: DEFAULT_GROUP
# seata服務名稱
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: "密碼"
# 事務組名稱
tx-service-group: seata-demo
service:
vgroup-mapping: # 事務組與cluster的映射關系
seata-demo: default
grouplist.seata-server: ip:8091
data-source-proxy-mode: AT
注意yml文件對縮進的格式要求很高,讀者可以參考筆者的配置進行修改,筆者本次部署時遇到服務始終無法注冊到seata中,控制臺持續輸出can not get cluster name in registry config xxxx, please make sure registry config correct
經過查閱源碼NettyClientChannelManager的代碼段,大抵推測yml配置沒有生效,排查半天得出yml縮進有問題。
//筆者這里debug進去發現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發現,上面這個問題可能還需要補充這樣一個步驟:
在上文配置的命名空間中增加一條配置,data-id為service.vgroupMapping.事務分組名稱,以筆者為例就是service.vgroupMapping.seata-demo,內容為default
8. 啟動服務將其注冊到seata中
完成后啟動服務,以筆者的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
其他服務同理,都完成后,我們的下單服務代碼加一個GlobalTransactional注解,即可完成分布式事務了,感興趣的讀者可以在扣款或者庫存扣減調用上設置一個錯誤的調用,最終都會看到訂單回滾且當前用戶的余額和庫存都不會有扣減:
@Override
@GlobalTransactional
@Transactional(rollbackFor = RuntimeException.class)
public void createOrder(OrderDto orderDTO) {
Order order = new Order();
BeanUtils.copyProperties(orderDTO, order);
//調用產品服務獲取商品詳情
ResultData<ProductDTO> productInfo = productFeign.getByCode(orderDTO.getProductCode());
//計算總金額
BigDecimal total = productInfo.getData().getPrice().multiply(new BigDecimal(order.getCount()));
order.setAmount(total);
//創建訂單
save(order);
//扣減金額
accountFeign.reduceAccount(orderDTO.getAccountCode(), order.getAmount());
//扣減商品
productFeign.deduct(orderDTO.getProductCode(), orderDTO.getCount());
}