首页 / 资讯中心 / 文章详情

Docker部署Seata并注册Nacos:分布式事务中间件实操指南

Docker部署Seata并注册Nacos:分布式事务中间件实操指南 ★ FEATURED ARTICLE
1. 先搞清楚Seata 和 Nacos 在这件事里各扮演什么角色1.1 分布式事务难题Seata 到底解决了什么微服务拆到一定程度一定会撞上这样一个问题一笔订单操作同时扣了库存、锁了优惠券、创建了订单记录如果第三步失败了前两步已经扣掉的数据怎么办最直接的办法是业务代码里写一堆 try-catch-finally一个模块一个模块去补偿但模块一多补偿逻辑比业务逻辑还复杂而且根本没法保证“刚好补偿成功一次”。Seata 解决的就是这件事。它是目前国内微服务领域用得最多的分布式事务中间件核心就是全局事务协调把多个分支事务纳入一个全局事务要么全部提交要么全部回滚。它提供了 AT、TCC、SAGA、XA 四种模式其中 AT 模式基于数据库的 undo_log 表和 SQL 解析自动完成回滚业务代码几乎不用改所以普及率最高。在 Seata 的架构里有三个角色TCTransaction Coordinator事务协调器就是 Seata Server 本身负责全局事务的提交与回滚决策TMTransaction Manager事务管理器负责开启、提交或回滚全局事务它在发起全局事务的客户端应用里RMResource Manager资源管理器管理分支事务负责分支事务的注册、状态上报也在各个微服务应用里你可以把 TC 理解成调度中心TM 是发号施令的人RM 是干活的员工。本文要做的 Docker 安装 Seata装的就是 TC 这个角色然后把它的地址注册进 Nacos让各个微服务里的 TM 和 RM 能通过 Nacos 找到 TC。1.2 为什么非要让 Seata 注册进 Nacos很多人刚接触 Seata 的时候都会有一个疑问TC 明明有自己的服务端口客户端配置里直接写死 IP 和端口不就行了为什么非要绕一圈注册到 Nacos单个实例确实可以直接写死地址比如192.168.1.10:8091。但只要 TC 变成两个节点做高可用或者进行了扩容、迁移、IP 变更直接写死地址的客户端就全部需要修改配置并重启。这种操作在线上环境是不可接受的。把 Seata Server 注册进 Nacos 之后客户端只配置一个逻辑的事务分组名称例如my_test_tx_group客户端启动时会通过 Nacos 找到该分组对应的 TC 服务实例列表再从中选一个发起连接。TC 地址变化、节点挂掉、新增节点客户端都无感知连接会自动切换到可用节点。这也是 Seata 官方推荐的做法。顺带说一下Nacos 在 Seata 体系里还有第二层作用——作为 Seata Server 的配置中心。也就是说Seata 启动时会先从 Nacos 拉取自己的配置不再依赖本地文件。注册中心解决“客户端怎么找到 TC”的问题配置中心解决“TC 自己怎么配置”的问题两个功能共用同一个 Nacos非常省事。2. 部署前必知的版本搭配与配置项2.1 版本选型镜像、Nacos、MySQL 的兼容组合我踩过最深的坑就是版本乱配。Seata、Nacos、MySQL 三者之间不是随便组合就能跑的尤其是 Seata 1.5.0 之后配置方式发生了一次比较大的调整网上很多旧教程还在用老写法照着做很容易翻车。我这里实测稳定的组合是组件版本备注Docker20.x 及以上宿主机环境seataio/seata-server1.6.1镜像完整包含启动脚本Nacos2.2.3作为注册中心和配置中心MySQL5.7 或 8.0Seata Server 的存储库先说为什么是 Seata 1.6.1。1.4.x 及更早版本配置全部依赖容器内的file.conf和registry.conf两个文件部署思路是改完文件挂载进去再启动。而 1.5.x 开始Seata 支持把配置直接放到 Nacos、Consul、Apollo 这类配置中心里file.conf不再作为主要配置来源。1.6.1 是 1.5 系列之后比较稳定的版本生态成熟、资料多、适配的客户端版本也多适合绝大多数项目。再说 Nacos。Nacos 2.x 是目前的主流版本相比 1.xgRPC 通信能力更强Seata 1.6.1 对 Nacos 2.x 的支持已经非常完善。注意一点Nacos 2.x 启动时除了 8848 端口还会额外占用 9848 和 9849 端口用于 gRPC 通信防火墙和安全组里这三个端口要一起放开不然 Seata 能启动但客户端连不上。MySQL 的选择上没有太多讲究5.7 和 8.0 都能用。如果选 8.0注意 JDBC 驱动的driverClassName要写com.mysql.cj.jdbc.Driver而且 URL 里要带上时区参数比如serverTimezoneAsia/Shanghai不然驱动初始化的时候会报时区相关的异常。2.2 配置项拆解registry.conf、Nacos 配置中心、环境变量Seata Server 容器启动的时候真正影响行为的配置集中在两个地方一是容器内的/seata-server/resources/registry.conf文件。这个文件负责告诉 Seata“注册中心用谁、配置中心用谁”。它的结构非常直白registry { type nacos nacos { application seata-server serverAddr 192.168.1.100:8848 group SEATA_GROUP namespace cluster default username password } } config { type nacos nacos { serverAddr 192.168.1.100:8848 namespace group SEATA_GROUP username password dataId seata-server.properties } }注意这里的application seata-server这是 Seata Server 注册到 Nacos 后的服务名。客户端连接的时候会通过这个服务名去 Nacos 服务列表里查找所以客户端配置里的application必须和这里保持一致默认都是seata-server。另一个容易忽略的点是namespace。如果你没有在 Nacos 创建命名空间这里就留空字符串对应的是 Nacos 的public命名空间。如果填了abc123之类的命名空间 ID那 Nacos 上创建的配置和 Seata 注册的服务都必须放在这个命名空间下否则两边对不上表现就是配置拉取不到、服务发现不到。二是配置中心里的seata-server.properties数据文件。这个文件的内容决定 TC 怎么连接存储、怎么处理事务才是在 Nacos 里真正干活的部分。举个最常用的 MySQL 存储配置store.modedb store.lock.modedb store.session.modedb store.db.datasourcedruid store.db.dbTypemysql store.db.driverClassNamecom.mysql.jdbc.Driver store.db.urljdbc:mysql://192.168.1.100:3306/seata?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai store.db.userseata store.db.passwordseata123 store.db.minConn5 store.db.maxConn20 store.db.globalTableglobal_table store.db.branchTablebranch_table store.db.lockTablelock_table store.db.queryLimit100 service.vgroupMapping.my_test_tx_groupdefault看到service.vgroupMapping.my_test_tx_groupdefault这一行这就是前面说的事务分组映射把客户端上报的事务分组my_test_tx_group映射到 Seata Server 集群名default。Nacos 注册列表里Seata Server 的 cluster 属性就是default两边通过这个关系完成最终的关联。store.modedb表示用数据库存储事务信息。相比默认的 file 模式db 模式可以支持多个 Seata Server 实例共享同一份事务数据是做高可用的基础。如果只有单机测试file 模式也能跑但容器重启后事务状态容易丢失生产环境一律建议 db。2.3 事务分组这个绕不开的概念事务分组是整个 Seata 配置里最绕、也最容易出错的环节。简单说它的作用是在客户端和 TC 之间加一层“逻辑映射”避免客户端直接依赖 TC 的物理地址。完整的链路是这样的客户端配置tx-service-group: my_test_tx_group客户端启动后用这个名字到配置中心查询service.vgroupMapping.my_test_tx_group得到映射值default也就是 Seata Server 的集群名。然后客户端再用这个集群名去注册中心查找对应 cluster 的实例列表拿到实际 IP 和端口建立连接。也就是说客户端只知道一个逻辑分组号通过两层查表最后才能摸到 TC 的地址。这样做的最大好处是TC 的物理拓扑怎么变客户端的配置都不用动只需调整配置中心的映射关系即可。这里需要特别提醒Seata 1.5.0 之后的版本中vgroupMapping配置存放在 Nacos 配置中心而 1.4.x 及更早版本放在file.conf的service.vgroupMapping节点下面。如果你用的是 1.6.1 镜像却想修改file.conf里的 vgroup 配置会发现根本不生效因为服务端在启动时已经从 Nacos 拉取了配置中心的seata-server.properties本地文件里的对应配置会被覆盖。我见过不少人在这个点上卡好几天。3. 实操Docker 部署 Seata 并完成 Nacos 注册3.1 初始化数据库与准备配置文件这一步分两件事给 Seata Server 建库建表以及给容器准备registry.conf。先说建库建表。MySQL 中执行以下 SQL先建数据库再建三张表。重点说一下global_table、branch_table、lock_table和undo_log的区别前两张表是 TC 记录全局事务和分支事务状态用的lock_table存储行锁信息这三张属于 Seata Server 自己的存储而undo_log是客户端 AT 模式下每个业务库都必须创建的回滚日志表属于业务侧。创建 undo_log 的 SQL 在官方文档里有建在业务数据库而不是 Seata 的库里别搞混了。CREATE DATABASE IF NOT EXISTS seata DEFAULT CHARACTER SET utf8mb4; USE seata; -- 全局事务表 CREATE TABLE IF NOT EXISTS global_table ( xid VARCHAR(128) NOT NULL, transaction_id BIGINT, status TINYINT NOT NULL, application_id VARCHAR(32), transaction_service_group VARCHAR(32), transaction_name VARCHAR(128), timeout INT, begin_time BIGINT, application_data VARCHAR(2000), gmt_create DATETIME, gmt_modified DATETIME, PRIMARY KEY (xid), KEY idx_gmt_modified_status (gmt_modified, status), KEY idx_transaction_id (transaction_id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4; -- 分支事务表 CREATE TABLE IF NOT EXISTS branch_table ( branch_id BIGINT NOT NULL, xid VARCHAR(128), transaction_id BIGINT, resource_group_id VARCHAR(32), resource_id VARCHAR(64), branch_type VARCHAR(8), status TINYINT, client_id VARCHAR(64), application_data VARCHAR(2000), gmt_create DATETIME(6), gmt_modified DATETIME(6), PRIMARY KEY (branch_id), KEY idx_xid (xid) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4; -- 全局锁表 CREATE TABLE IF NOT EXISTS lock_table ( row_key VARCHAR(128) NOT NULL, xid VARCHAR(96), transaction_id BIGINT, branch_id BIGINT NOT NULL, resource_id VARCHAR(64), table_name VARCHAR(32), pk VARCHAR(36), status TINYINT NOT NULL DEFAULT 0, gmt_create DATETIME, gmt_modified DATETIME, PRIMARY KEY (row_key), KEY idx_branch_id (branch_id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;再说registry.conf。在宿主机上创建一个目录比如/opt/seata/config在里面新建registry.conf内容参照上一节的配置。这里有几个细节值得注意serverAddr如果是本机 Nacos写127.0.0.1:8848或者宿主机实际 IP 都行但要保证容器内能访问到这个地址。如果 Nacos 也跑在 Docker 里且使用--network host模式可以直接用127.0.0.1。如果你在 Nacos 上启用了鉴权username/password这里的账号密码必须填对否则启动时报“无效的用户名或密码”错误。如果不需要鉴权username 和 password 留空字符串就可以不要注释掉否则有些版本的客户端会报空指针异常。准备好registry.conf之后在 Nacos 控制台上创建配置seata-server.propertiesgroup 填SEATA_GROUP配置内容就是上一章节提到的那段 properties。可以通过控制台手动粘贴也可以通过 Open API 上传curl -X POST http://127.0.0.1:8848/nacos/v1/cs/configs \ -d dataIdseata-server.properties \ -d groupSEATA_GROUP \ -d content$(cat seata-server.properties)如果你的 Nacos 版本是 2.2 及以上Open API 路径可能带/nacos/v1/cs/configs或/nacos/v1/cs/configs建议先在控制台手动创建成功一次顺手验证配置是否能被正确解析再考虑脚本化。3.2 启动 Seata 容器配置就绪后拉镜像并启动容器。这一步容易踩坑的是端口映射和环境变量我直接给出一个生产环境可用的启动命令docker pull seataio/seata-server:1.6.1 docker run -d \ --name seata-server \ -p 8091:8091 \ -p 7091:7091 \ -e SEATA_IP192.168.1.100 \ -e SEATA_PORT8091 \ -e STORE_MODEdb \ -e JAVA_OPT-Xmx1024m -Xms1024m \ -v /opt/seata/config:/seata-server/resources \ seataio/seata-server:1.6.1逐个解释一下关键参数-p 8091:80918091 是 Seata Server 的默认服务端口注册到 Nacos 时对外发布的端口是 8091。客户端通过这个端口和 TC 通信必须映射出来。-p 7091:7091这是 1.5.0 之后的版本新增的 HTTP 端口提供健康检查、监控等接口可以不映射但建议映射出来方便排查问题。-e SEATA_IP192.168.1.100这个环境变量决定了 Seata Server 注册到 Nacos 时对外暴露的 IP。这一点非常关键。如果不设置容器启动时会自动探测宿主机的内网 IP但在某些网络环境例如多网卡、macvlan、虚拟化网络下探测结果可能是172.17.0.x这样的容器内部网段 IP客户端拿到这个 IP 根本连不上。所以生产环境一定要显式指定为宿主机对外可达的 IP。-e SEATA_PORT8091对外暴露的服务端口和宿主机映射端口保持一致必须写 8091。-e STORE_MODEdb指定存储模式为 db。在 1.5.0 版本之后这个变量主要影响启动时的默认配置选择真正决定存储行为的还是配置中心里seata-server.properties中的store.modedb。两个地方要一致否则可能出现配置不一致的问题。-v /opt/seata/config:/seata-server/resources挂载配置目录把宿主机的registry.conf挂载到容器内的 resources 目录。注意这个挂载是文件目录级挂载宿主机目录里只需要有registry.conf不要放其他无关文件否则会覆盖容器内的默认资源。启动后先看一眼日志docker logs -f seata-server如果一切正常你会看到类似这样的输出The server IP is 192.168.1.100 The server port is 8091 Nacos registry started, service name: seata-server, cluster: default Nacos config started, dataId: seata-server.properties, group: SEATA_GROUP看到 Nacos registry started 说明服务注册成功Nacos config started 说明配置拉取成功。如果其中一行报错就要进入下一节排查了。3.3 在 Nacos 控制台验证注册状态容器起来后打开 Nacos 控制台进入“服务管理 - 服务列表”。如果 namespace 是 public直接在默认列表里就能看到一个名为seata-server的服务集群数为 1实例 IP 就是你在SEATA_IP里配置的地址。这里有一个需要留意的细节如果你在registry.conf里配置了 cluster 为defaultNacos 服务列表里的“集群”列会显示DEFAULT这是 Nacos 对 cluster 名做了大小写归一化的结果不需要担心。客户端在查找时使用的 cluster 名也会进行相同的归一化处理所以两边是对得上的。再去“配置管理 - 配置列表”能看到SEATA_GROUP分组下有一条seata-server.properties配置。点击配置可以查看具体内容确认配置正确拉取。还有一个快速验证方式在宿主机上执行curl http://127.0.0.1:7091/health返回UP或者类似的状态字符串说明 Seata Server 本身的健康检查已经通过。如果 7091 端口没有映射到宿主机可以进入容器内部执行或者在 Docker 网络中访问。3.4 补充用 Docker Compose 编排的写法如果你需要把 Nacos、Seata、MySQL 一起用 Compose 管理提供一个参考片段version: 3.8 services: seata-server: image: seataio/seata-server:1.6.1 container_name: seata-server ports: - 8091:8091 - 7091:7091 environment: - SEATA_IP192.168.1.100 - SEATA_PORT8091 - STORE_MODEdb - JAVA_OPT-Xmx1024m -Xms1024m volumes: - /opt/seata/config:/seata-server/resources depends_on: - nacos restart: always networks: - seata-netCompose 编排的优点是整体部署、重启方便但要注意depends_on只控制容器启动顺序不保证 Nacos 已经“可用”。建议在 Seata 启动脚本里加一个健康检查等待逻辑或者接受“Nacos 未就绪导致 Seata 启动失败靠 restart: always 自动拉起”的兜底方案。我个人的习惯是加一段简单的 shell 等待循环确认 8848 端口通后再启动 Seata 容器这样日志里不会出现一堆连接异常。4. 客户端接入与全局事务验证4.1 客户端 application.yml 的核心配置Seata Server 部署完成只是第一步真正检验部署是否正确要看客户端能不能通过它完成一次分布式事务。这里以 Spring Cloud Alibaba 体系为例客户端的application.yml中需要增加以下配置spring: application: name: order-service seata: enabled: true application-id: ${spring.application.name} tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 192.168.1.100:8848 application: seata-server group: SEATA_GROUP namespace: cluster: default username: password: config: type: nacos nacos: server-addr: 192.168.1.100:8848 group: SEATA_GROUP namespace: >GlobalTransactional(timeoutMills 30000, name create-order-tx) public void createOrder(OrderDTO order) { orderMapper.insert(order); productClient.deductStock(order.getProductId(), order.getCount()); }product-service的扣减方法不加GlobalTransactional它作为分支事务会被全局事务管理。当它抛出异常时观察两个结果product-service里已经执行的扣减 SQL 被自动回滚库存数据恢复原状order-service里同样在全局事务中执行的订单插入也被回滚数据库中查不到这条订单记录这两个回滚动作就是 AT 模式的价值所在。如果验证通过说明 Seata Server 的部署、Nacos 的注册和配置拉取、客户端的接入全部打通了。这里还有一个重要前提参与事务的每个业务数据库都要有undo_log表否则 AT 模式启动时会报错java.sql.SQLException: Failed to get connection或者更直接的提示Could not found undo_log table。建表 SQL 在 Seata 官方仓库的script/client/at/db/mysql.sql下把表建在对应的业务库表名必须叫undo_log不能改。5. 常见问题与排查指南5.1 启动失败类问题现象原因解决方案容器启动后立即退出数据库连接失败或配置读取失败先docker logs seata-server看报错。重点检查store.db.url的地址、账号密码、MySQL 是否允许远程连接日志报NoSuchMethodErrorNacos client 版本和 Seata 内置版本冲突更新镜像到 1.6.1或者检查宿主机是否设置了会影响容器内 classpath 的环境变量日志报Failed to init: java.net.SocketTimeoutExceptionSeata 连接不上 Nacos检查 8848/9848/9849 端口连通性确认registry.conf中 serverAddr 是否宿主机可达日志报Invalid username or passwordNacos 开启了鉴权但配置了错误账号在registry.conf中配置正确的用户名密码或者在 Nacos 中关闭鉴权启动失败的问题大部分集中在配置不一致。我一般建议的排查顺序是先看日志里有没有打印出 Nacos 注册和配置的地址确认registry.conf被正确加载再确认 Nacos 上配置中心的数据是否完整最后才是排查网络和数据库。不要一上来就怀疑镜像有问题Seata 官方镜像的稳定性是很高的。5.2 注册失败类问题最常见的一种情况是Seata 容器正常启动日志没有报错但 Nacos 服务列表里就是找不到seata-server。这类问题九成出在 namespace 上。检查registry.conf里配置的 namespace 和 Nacos 控制台当前切换的命名空间是否一致。如果namespace 那你就应该在public即空命名空间下查看服务列表而不是切换到某个自定义命名空间。反过来也一样。另一种情况是注册了但客户端连不上。此时打开 Nacos 服务列表点开 seata-server 的详情看实例 IP 是什么。如果显示的是172.17.0.x这类容器内网地址说明SEATA_IP环境变量没有正确设置或者根本没有设置。修改后重启容器即可解决。顺带说一句SEATA_IP和SEATA_PORT这两个环境变量官方文档里有明确说明但不少人会漏掉。尤其在 Docker 默认的 bridge 网络模式下容器内自动探测到的 IP 几乎总是不可达的宿主机对端地址这个坑几乎每个人都会踩一次。5.3 客户端连接与事务分组类问题如果客户端日志一直报Service group name is empty或者Could not find service group原因基本都在事务分组映射上。排查步骤可以按下面这个顺序客户端tx-service-group是否正确配置是否为空Nacos 配置中心里seata-server.properties中service.vgroupMapping.你的分组名集群名是否存在前缀是否和客户端分组名一致客户端registry.nacos.cluster是否与服务端registry.conf里的cluster一致其中最容易搞混的是第 2 步和第 3 步的组合关系。举一个具体的错误场景客户端分组名是my_tx_groupServer 端配置的是service.vgroupMapping.my_tx_groupdefault看起来没问题但客户端里把cluster配成了my_cluster而 Server 的 cluster 是default最终结果是客户端查到了配置映射但在 Nacos 里找不到 cluster 名为my_cluster的实例。排查的时候要先把这条链路完整画出来每一步对上再去改配置文件。还有一类问题跟事务超时有关。默认全局事务超时时间是 60 秒如果某个分支事务执行时间较长比如调用了慢接口或者锁等待TC 会主动回滚整个事务。现象上表现为业务操作偶发性失败日志里有transaction timeout相关异常。解决方案是在GlobalTransactional(timeoutMills 60000)中适当加大超时时间或者优化分支事务的响应时间。这个问题不是部署错误但很容易被误判为 Seata 部署问题。6. 一些实战中的补充建议整个部署链路打通之后有几个事情建议在生产环境落地之前提前做好第一个是给 Seata Server 配置 JVM 参数。默认的堆内存配置在大多数场景下偏保守高并发场景容易频繁触发 GC。建议在JAVA_OPT环境变量里显式指定-Xms和-Xmx并且把两者设置成相同值避免运行期堆振荡。例如-Xms2048m -Xmx2048m -Xmn1024m具体数值根据业务流量来定。第二个是客户端依赖版本必须收敛。在使用 Spring Cloud Alibaba 体系时Seata 客户端版本可能被 BOM 统一管理如果 BOM 里声明的版本和服务端不一致即使你单独引入了客户端 Starter也可能因为依赖仲裁机制被覆盖成错误版本。建议在 Maven 或 Gradle 中通过显式的seata-spring-boot-starter版本声明来覆盖 BOM 中的版本。第三个是妥善处理 Seata Server 的日志。容器默认把日志输出到 stdout长时间运行会积累大量日志建议在 Docker 的logging配置里加上日志轮转logging: driver: json-file options: max-size: 100m max-file: 3否则一台机器上跑多个容器时间久了磁盘会被日志占满影响整个宿主机的运行。最后再分享一个小技巧验证 Seata 是否正常工作不要一上来就写复杂的多服务调链。先用两个服务、一张表、一个GlobalTransactional做最小验证确认回滚行为符合预期再逐步增加复杂度。这样一旦出问题排查范围会小很多不会陷入“到底是业务代码问题还是 Seata 问题”的拉锯战。我每次在新环境部署 Seata都会先跑这个最小验证流程确认绿灯之后才敢把业务服务接入。这套流程虽然不起眼但能省下的排查时间谁用谁知道。
阅读完成 · 觉得有帮助?
咨询建站