有时候一个看起来只有几个容器的部署任务,真正花时间的不是敲命令,而是理清楚容器之间、组件之间的依赖关系。这不,前段时间我在一个微服务项目里要搭分布式事务环境,需求很明确:docker安装nacos+seata,但是真正操作的时候才发现,版本、网络、配置中心、事务分组,每一层都有坑。这篇文章就把我从零到一把 Nacos 和 Seata 跑在 Docker 里的全过程记录下来,包括命令、配置文件以及排错思路,希望能帮你少走点弯路。
1. 为什么这套组合值得用Docker部署:两个组件分别解决什么问题
先说个很多人没想明白的点:Nacos 和 Seata 是两回事。Nacos 负责服务发现和配置管理,可以理解成整个微服务架构的"通讯录"和"设置中心";Seata 负责分布式事务,是保证多个服务之间数据一致性的"协调员"。它们经常同时出现,是因为 Seata 需要一个地方来注册自己、拉取配置,而 Nacos 正好提供了这个能力。很多人误以为装了 Seata 就必须装 Nacos,其实 Seata 也支持别的注册中心,但 Nacos 确实是最常见的搭配,尤其是 Spring Cloud Alibaba 的生态里。
在 Docker 环境下,这两个组件的部署有几个直接的痛点:第一,Nacos 的配置项很多,环境变量和数据库设置容易漏;第二,Seata 的配置分成注册和配置中心两块,改错一个文件整个服务就起不来;第三,容器网络里注册的 IP 可能导致客户端连不上;第四,数据持久化问题,不挂载目录的话容器一删,配置全部丢失。这篇文章会围绕这几个痛点展开。
这套环境适合谁来参考?如果你是刚接触微服务、想在学习环境快速搭一套注册中心加分布式事务环境的人;或者你在公司内网要部署一套测试环境,但不想手动改一堆配置文件;再或者你在排查"为什么 Seata 客户端报了找不到事务分组"这类问题——这篇文章都能给你一个完整的参考路径。
2. 动手前先定案:版本组合、网络规划与目录挂载
我见过太多人上来就docker pull,结果 Nacos 和 Seata 版本不兼容,或者 Seata 客户端和服务端版本差太多,导致分支事务类型不匹配。所以第一步不是拉镜像,而是定版本。
2.1 版本组合的兼容矩阵
我实测下来比较稳的组合是:Nacos 2.2.3(或 2.3.2)搭配 Seata Server 1.6.1。如果是生产环境想用更新的版本,1.8.0 也是可选,但客户端最好用 1.6.x 或 1.7.x 对应版本。Spring Cloud Alibaba 的版本也很关键,旧版本客户端往新版本服务端注册时,有时候会报invalid registry之类的错误。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Nacos Server | 2.2.3 / 2.3.2 | 2.x 才支持 gRPC 端口(9848/9849) |
| Seata Server | 1.6.1 / 1.8.0 | 1.6.1 最经典,1.8.0 对 AT 模式更友好 |
| Spring Boot | 2.5.x / 2.6.x | 与 Seata starter 兼容性较好 |
| Spring Cloud Alibaba | 2021.0.5.0 左右 | 版本号与服务端版本需匹配 |
| MySQL | 5.7 / 8.0 | 必须初始化 Nacos 和 Seata 的数据库 |
这里给个忠告:Nacos 的 1.x 版本和 2.x 版本在客户端兼容性上差别很大,尽量别混用。我遇到过 Nacos Server 用 2.2.3,客户端依赖还是 1.4.x 的情况,服务注册能成功,但配置订阅偶尔失效,查起来非常费劲。
2.2 网络规划:docker network 比端口映射更好用
为什么推荐先建一个 Docker 网络?因为 Nacos 和 Seata 之间要互相访问,用容器名作为 host 是最方便的方式。端口映射是给宿主机访问用的,而容器间通信走的是 Docker 内部网络。如果你只做了端口映射,却没让它们加入同一个网络,那 Seata 里配置 Nacos 地址时就只能写宿主机 IP,虽然也能跑,但不够干净,也容易在跨主机时出问题。
docker network create micro-net这条命令创建一个桥接网络。后面启动的 Nacos 容器和 Seata 容器都加入这个网络,互相之间直接用服务名访问即可。比如 Nacos 容器名为nacos-server,那么 Seata 里配置localhost或者轮流 IP 都不需要了,直接写nacos-server就行。
2.3 目录挂载规划
容器一旦重建,配置就丢,这是 Docker 最常见的坑。所以我建议在宿主机上先建好目录,把 Nacos 的配置目录、日志目录,还有 Seata 的配置目录、日志目录都挂载出来。
mkdir -p /opt/docker-volume/nacos/{logs,conf} mkdir -p /opt/docker-volume/seata/{logs,conf}这样以后改配置不用进容器,直接在宿主机改完重启容器即可。如果是测试环境,挂载 logs 目录就够了,conf 目录是否挂载可以看情况;但生产环境建议全部挂载。
3. 启动Nacos容器:数据库初始化、环境变量与鉴权
Nacos 的 Docker 启动其实不算难,但很多教程故意省略了数据库初始化这一步。Nacos 默认会用内嵌数据库,重启数据就没了,而且集群模式下内嵌库根本不支持。所以我们要做的第一件正事是准备 MySQL。
3.1 初始化 Nacos 的数据库和账号
Nacos 官方提供了数据库初始化脚本,在 GitHub 仓库的distribution/conf/nacos-mysql.sql里。下载这个脚本,在 MySQL 中创建名为nacos_config的库,执行脚本:
CREATE DATABASE nacos_config DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'nacos'@'%' IDENTIFIED BY 'nacos'; GRANT ALL PRIVILEGES ON nacos_config.* TO 'nacos'@'%'; FLUSH PRIVILEGES;然后执行 sql 文件导入表结构。这一步完成了,Nacos 就不会再依赖内嵌的 Derby 数据库,控制台里的配置、用户、权限都会持久化到 MySQL。
3.2 Nacos 容器的环境变量设置
Nacos 官方镜像里的环境变量比较多,我只挑关键的。MODE=standalone表示单机模式;SPRING_DATASOURCE_PLATFORM=mysql指定数据源为 MySQL;MYSQL_SERVICE_HOST指 MySQL 的地址,这里要注意,如果 MySQL 也跑在 Docker 里,要用同一个网络并写容器名;MYSQL_SERVICE_DB_NAME是刚才创建的库名;NACOS_AUTH_ENABLE用来开启鉴权。
docker run -d \ --name nacos-server \ --network micro-net \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ -e MODE=standalone \ -e SPRING_DATASOURCE_PLATFORM=mysql \ -e MYSQL_SERVICE_HOST=mysql-container \ -e MYSQL_SERVICE_PORT=3306 \ -e MYSQL_SERVICE_DB_NAME=nacos_config \ -e MYSQL_SERVICE_USER=nacos \ -e MYSQL_SERVICE_PASSWORD=nacos \ -e NACOS_AUTH_ENABLE=true \ -e NACOS_AUTH_TOKEN=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMDE= \ -v /opt/docker-volume/nacos/logs:/home/nacos/logs \ nacos/nacos-server:v2.2.3很多人不知道 9848 和 9849 是干什么的。Nacos 2.x 版本的服务端和客户端之间不仅有 HTTP 通信,还有 gRPC 通信。8848 是 HTTP 主端口,9848 是客户端 gRPC 端口,9849 是服务端之间同步的 gRPC 端口。如果只映射 8848,客户端会一直报连接超时,因为 gRPC 端口不通。这个坑非常隐蔽,我第一次部署时漏了 9848,服务注册总失败。
3.3 启动后验证:访问控制台和确认数据源
启动完成后,浏览器访问http://宿主机IP:8848/nacos,默认账号密码都是nacos。如果能登录,说明容器本身没问题。接下来去 Nacos 控制台的"配置管理"里随便建一条配置,再重启 Nacos 容器,看看配置是否还在——这一步能验证 MySQL 是否生效。如果重启后配置没了,说明数据源配置没生效,检查环境变量里的MYSQL_SERVICE_HOST是否写错。
鉴权这块多说一句:Nacos 2.2.3 之后默认鉴权是可以关闭的,但生产环境必须要开。NACOS_AUTH_TOKEN这个值不能太短,否则服务端不启动,它会校验 Base64 编码后的长度。如果你不设这个值,控制台登录还好说,但客户端连的时候会报 403。测试环境图省事可以不开鉴权,但后面 Seata 连接 Nacos 时也要同步关掉或带上 token,否则 Seata 拿不到配置。
4. 启动Seata容器:注册配置、初始化脚本和挂载细节
Seata 比 Nacos 复杂一个量级,因为它的配置体系分两层:注册中心配置和配置中心配置。很多人把registry.conf里的registry和config混为一谈,导致 Seata Server 能启动但客户端连不上,或者服务端拿到了错误的配置。
4.1 Seata 的配置模型:registry.conf 与 config.txt
Seata Server 启动时需要两个关键信息:它自己要注册到哪里去,以及它的运行时配置从哪里读取。这两个信息都定义在registry.conf里。其中registry节点决定注册中心类型,我们选nacos;config节点决定配置中心类型,我们也选nacos,意思是 Seata Server 的配置项不是从本地文件读,而是从 Nacos 配置中心拉取。
既然配置放在 Nacos,那 Seata 官方提供了一套配置文件模板叫config.txt,里面是各种key=value形式的事务配置项。我们需要把这份config.txt的内容批量导入到 Nacos 中。导入之后,Seata Server 启动时会从 Nacos 读取这些配置。这个思路很像"把配置文件交给注册中心管理",好处是多个 Seata 实例不用各自保存一份配置。
我在实测中发现,Seata 1.6.1 的config.txt里最关键的几个配置项是:
store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://mysql-container:3306/seata?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai store.db.user=nacos store.db.password=nacos这段配置的意思是 Seata Server 要把全局事务会话、分支事务这些记录持久化到 MySQL 的seata库中。如果你不配置 store.mode=db,默认是 file 存储,虽然也能跑,但生产环境不推荐,而且多实例会互相覆盖数据。
4.2 初始化 Seata 的数据库脚本
Seata 的数据库和 Nacos 不是同一个库。需要新建一个seata数据库,然后执行官方脚本script/server/db/mysql.sql,这个脚本会创建global_table、branch_table、lock_table、distributed_lock四张表。
CREATE DATABASE seata DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;除此之外,你的每个业务数据库里还要执行script/client/at/db/mysql.sql,这个脚本创建一张undo_log表,Seata 的 AT 模式靠它来做回滚日志。这个表经常被漏掉,后果是全局事务提交的时候一切正常,一旦回滚就会报"undo_log表不存在"。如果你手下有多个业务服务,每个业务的库都要建这张表。
4.3 把 config.txt 灌入 Nacos 配置中心
这一步是整个部署中最容易被忽略的。官方仓库里提供了脚本目录,进入scripts/config-center/nacos/,运行nacos-config.sh脚本即可。脚本会读取上一级目录的config.txt,然后把每条配置都写入 Nacos 配置中心。
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 你的命名空间ID -u nacos -w nacos参数说明:-h是 Nacos 地址,-p是端口,-g是配置分组,-t是命名空间 ID。如果你没建命名空间,默认 public 命名空间的 ID 是空字符串,-t可以不传。导入完成后,到 Nacos 控制台看"配置管理-配置列表",搜索SEATA_GROUP,会看到上百条key=value配置。如果一条都没有,那就是脚本没跑成功,检查 Nacos 的鉴权是否已开启,脚本不带 token 就会全失败。
4.4 Seata 容器启动命令与目录挂载
启动 Seata Server 时,registry.conf是必须挂载的。我建议先在自己的目录里写好registry.conf,再挂载进容器:
registry { type = "nacos" nacos { application = "seata-server" serverAddr = "nacos-server:8848" group = "SEATA_GROUP" namespace = "" username = "nacos" password = "nacos" } } config { type = "nacos" nacos { serverAddr = "nacos-server:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" } }注意这里serverAddr写的是nacos-server:8848,因为我们在同一个 Docker 网络里,Nacos 容器的名称是nacos-server。写完之后,启动容器:
docker run -d \ --name seata-server \ --network micro-net \ -p 8091:8091 \ -e SEATA_IP=192.168.1.100 \ -e SEATA_PORT=8091 \ -e SEATA_GROUP=SEATA_GROUP \ -v /opt/docker-volume/seata/conf/registry.conf:/seata-server/resources/registry.conf \ -v /opt/docker-volume/seata/logs:/seata-server/logs \ seataio/seata-server:1.6.1SEATA_IP这个环境变量特别重要。如果容器运行在 Docker 默认的 bridge 网络,Seata 会把自己的容器 IP 注册到 Nacos。客户端如果从宿主机网络访问,能访问到宿主机 IP 但访问不到容器 IP,就会一直报"无法连接 TC"。设置SEATA_IP为宿主机 IP,注册到 Nacos 的地址就是宿主机 IP,配合-p 8091:8091端口映射,客户端就能正常连接了。这是个非常容易踩的坑,很多人卡在这里,日志显示 Nacos 里有 seata-server 服务,但客户端就是连不上。
4.5 查看 Seata Server 启动日志
启动后通过docker logs -f seata-server看日志。正常情况下会出现"Server started"之类的字样。如果没有,大概率是registry.conf里 Nacos 地址写错,或者config中心的配置没导入成功。出现"dataId not exist"之类的报错时,优先检查是不是已经用脚本导入过配置。
5. 客户端与服务端"握手":事务分组是怎么连上的
环境跑起来之后,真正容易让人迷惑的不是容器,而是 Seata 的事务分组机制。客户端配置一个tx-service-group,服务端在注册中心里有一个service.vgroupMapping的映射关系,两者必须对齐,不然客户端会报"could not find service group"。
5.1 事务分组与 vgroupMapping 的关系
先放一个类比:事务分组就像快递地址里的"小区名",而 Seata Server 集群就是"快递站点"。客户端只知道自己住在哪个小区,但不知道哪个快递站负责这个小区,所以它要去 Nacos 里查一张映射表。这张表就是vgroupMapping。在 Seata 1.6.x 里,默认的映射关系是:
service.vgroupMapping.default = default意思是tx-service-group为default的客户端,对应服务端集群名为default。如果客户端配置的tx-service-group不是default,比如是my_tx_group,那你必须在 Nacos 的配置中心添加一条:
service.vgroupMapping.my_tx_group = default这条配置的位置要看版本。旧版常在config.txt里,新版可以通过 Nacos 配置中心动态添加。我在部署时就吃过这个亏:客户端写了个看起来很"合理"的分组名payment_group,但没有在 Nacos 里加映射关系,服务一直报找不到组,日志里全是no available service。
5.2 服务端注册到 Nacos 的名字是什么
上面说的"服务端集群名"default,对应到 Nacos 里是 Seata Server 注册时使用的application名称。看registry.conf里的application = "seata-server",不,这里单独说清楚:注册到 Nacos 的服务名是seata-server,而应用名的映射关系是service.vgroupMapping。其实在 Nacos 控制台的"服务列表"里搜索seata-server,能看到一个名为seata-server的服务,下面有一个临时实例,IP 是你在SEATA_IP里设置的值。客户端和这个 IP 建立连接后,才算真正跟事务协调器建立会话。
很多教程会直接告诉你"客户端加几行配置就能用",但没人说这几个名字之间是怎么绕的。你只要记住一条链:客户端的tx-service-group根据vgroupMapping找到集群名,再根据集群名找到 Nacos 里的服务实例。链路中任何一个名字对不上,事务协调器就找不到。
5.3 客户端的最小配置参考
如果用的是 Spring Cloud Alibaba,最简配置如下:
spring: cloud: nacos: discovery: server-addr: localhost:8848 config: server-addr: localhost:8848 file-extension: yaml seata: tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default registry: type: nacos nacos: application: seata-server server-addr: localhost:8848 group: SEATA_GROUP namespace: username: nacos password: nacos config: type: nacos nacos: server-addr: localhost:8848 group: SEATA_GROUP namespace: username: nacos password: nacos这里要注意,客户端的registry.nacos.application不一定等于服务端的容器名,它应该是服务端注册到 Nacos 的application。上面registry.conf里写的是seata-server,所以客户端这里也对应写一致。另外,客户端的 group 要和服务端一致,不然在 Nacos 的同一个命名空间里会隔离掉。
6. 端到端跑通验证:从日志看全局事务的完整生命周期
环境都起来了,配置也写了,怎么确认真的能跑通?我的建议是准备一个最简单的 Demo:两个服务,一个模拟订单创建,一个模拟库存扣减,中间用 OpenFeign 调用,然后在订单服务的方法上加上@GlobalTransactional注解,故意让库存服务抛异常,看订单会不会回滚。
6.1 准备数据库表和 Demo 工程
两个业务库各建一张表,订单库有orders表,库存库有stock表。两个库都要执行undo_log建表脚本。Demo 工程的结构不多说,重点是引入依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>很多新手会在@GlobalTransactional和@Transactional之间犹豫。实际用法是:@GlobalTransactional加在全局事务发起方的入口方法上,内部的远程调用方法的本地事务用@Transactional保持正常。全局事务的入口方法里,Seata 会拦截并开启全局事务,分支事务通过 XID 串联起来。
6.2 看日志:begin 与 commit/rollback
当订单服务调用库存服务时,观察 Seata Server 容器的日志,能看到类似这样的关键信息:
Begin new global transaction [192.168.1.100:8091:203123456789] Register branch transaction successfully Global transaction [192.168.1.100:8091:203123456789] commit/rollback如果一切正常,库存扣减成功后,日志里是 commit。如果你在库存服务里加一行throw new RuntimeException(),订单服务和库存服务的数据都应该恢复原状。这就是 AT 模式的作用:Seata 会在分支事务执行前记录镜像,回滚时用镜像数据覆盖回去。
6.3 回滚失败时的排查点
回滚失败最常见的情况有两种:一种是undo_log表没建,另一种是全局锁等待超时。第一种报错直接看异常信息就能定位;第二种通常在并发场景出现,测试环境没那么明显。还有一个容易被忽略的点:业务表的字段类型要和实体类里的一致,否则回滚时类型转换异常会导致回滚失败。如果你看到UndoLogManager相关的错误,先查表是否存在,再查表结构是否和官方脚本一致。
6.4 隐藏的时区问题
我那次部署就是栽在时区上。MySQL 的 URL 里如果没有serverTimezone=Asia/Shanghai,Seata 记录分支事务时间时可能会错位,导致全局事务的创建时间和分支事务时间不一致,进而引发异常。建议在config.txt的store.db.url里显式加上serverTimezone=Asia/Shanghai,同时在业务数据源连接串里也保持一致。这个配置不显眼,但出了问题非常难查。
7. 接入Spring Boot客户端时的高频坑与排查顺序
最后把我在实际接入阶段遇到的几个高频率问题整理一下,这些问题几乎每个初次部署的人都会碰到,按照从下到上的排查顺序写。
7.1 先看容器 IP,再看注册地址
客户端报连接超时,先不要急着怀疑代码。第一步看 Nacos 控制台,打开服务列表,找到seata-server,看看实例列表里的 IP 是什么。如果 IP 是172.17.0.x这种容器 IP,而你的客户端跑在宿主机或者别的机器上,那就是SEATA_IP没设置。重新设置SEATA_IP并重启 Seata Server 即可。这里的逻辑是:Seata 只会把配置的地址告诉别人,它自己并不知道你在外面做了端口映射。
7.2 检查命名空间和 Group 是否一致
Nacos 里的 namespace 和 group 是隔离维度。服务端registry.conf里如果namespace = "",那就是 public;客户端里也必须是 public。Group 默认建议都用SEATA_GROUP,但如果你在config.txt导入时用了别的 Group,客户端和服务端必须完全一致。这个不一致的报错很典型:客户端能启动,但拉取配置时为空,然后 Seata Server 的默认配置兜底生效,事务数据进不了 db,回滚也查不到日志。
7.3 数据源代理:不代理就白搭
在 Spring Boot 集成中,Seata 要求数据源必须被代理,否则全局事务拦截到了,但分支事务不会参与。通常的写法是:
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); } @Bean public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }如果没有这个代理配置,@GlobalTransactional注解虽然生效,但分支事务注册不到 Seata Server,线程变量里的 XID 也传不过去,最终效果就是"事务像没加一样"。这个问题代码层面上不报错,只在日志里能看到分支事务没有注册,所以容易被忽略。
7.4 按顺序排查的参考清单
我把排查顺序整理成一张表,方便直接对照:
| 现象 | 先检查什么 | 再检查什么 |
|---|---|---|
| 客户端报连不上 Seata Server | Nacos 服务列表里的 IP 是否为宿主机 IP | 8091 端口映射是否正确 |
| 客户端报找不到事务分组 | Nacos 配置中心是否有 vgroupMapping 配置 | 客户端 tx-service-group 是否拼写正确 |
| 回滚失败 | 业务库是否有 undo_log 表 | 表结构是否与官方脚本一致 |
| 事务不生效,无分支日志 | 数据源是否被 DataSourceProxy 代理 | @GlobalTransactional是否加在了入口方法上 |
这张表是我实际排查多个项目后的总结,每一行都出过问题。如果按这个顺序走完还没解决,再看一遍 docker logs,Seata 日志会把异常原因写得很清楚。
7.5 最后一个实用建议
如果你只是个人学习或做 Demo,镜像是官方镜像,版本用 1.6.1,别用太新的版本,因为新版本对 Spring Boot 3.x 的适配要求不一样,默认依赖冲突会耗费不少时间。等基础链路跑通了,再考虑升级版本。另外建议把整个环境的启动命令写成脚本,包括建网、拉镜像、启动容器、导入配置,这样以后重置环境时,一条命令就能恢复,不用再对着文档敲一遍。
部署这套环境,真正花时间的不是容器本身,而是理解 Seata 的配置模型和数据源代理机制。把registry.conf、config.txt、事务分组和数据源代理这四个点的关系理顺,你的 Docker 部署才算真正完成。