springboot + seata + nacos 分布式事务使用

本文档详细介绍了如何在Spring Boot应用中集成Seata进行分布式事务管理,包括Seata的依赖配置、数据库准备、Nacos注册中心和配置中心的设置。同时,展示了在服务间调用中如何传递和绑定全局事务ID(XID)以确保事务的一致性。在遇到事务回滚和提交的问题时,提供了排查思路和解决方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
     <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.0</version>
        </dependency>

数据库: 8.0.25 MySQL Community Server - GPL

创建mysql实例与表(docker)

docker run --name db1 -e MYSQL_ROOT_PASSWORD=111111  -v ~/soft/mysql/1/data:/var/lib/mysql -p 3308:3306  -itd mysql
docker run --name db2 -e MYSQL_ROOT_PASSWORD=111111  -v ~/soft/mysql/2/data:/var/lib/mysql -p 3309:3306  -itd mysql

db1 创建表

 create database test;
 create table account( id int, account int);
 insert account values (1,100);

在这里插入图片描述
db2 创建表

 create database test;
 create table account( id int, account int);
 insert account values (2,100);

在这里插入图片描述

springboot seata+nacos 配置

该配置仅适用1.4.0,1.4.2会发生数据不会滚的情况,尚未发现原因。。。

discovery:
  server-addr: 127.0.0.1:8848
  username: nacos
  password: nacos
  group: SEATA_GROUP

seata:
  tx-service-group: my_test_tx_group # 值与seata源码中 config.txt中的service.vgroupMapping的后缀相同,否则服务启动会提示找不到可用的service
  registry:
    type: nacos
    nacos:
      server-addr: ${discovery.server-addr}
      username: ${discovery.username}
      password: ${discovery.password}
      group: ${discovery.group}
  config:
    type: nacos
    nacos:
      server-addr: ${discovery.server-addr}
      username: ${discovery.username}
      password: ${discovery.password}
      group: ${discovery.group}
  service:
    vgroup-mapping:
      my_test_tx_group: default

提交事务

单个服务

controller

 @GlobalTransactional
    @GetMapping
    public Object test(){
        globalServicel.testGlobal();
        return 0;
    }

service

public class GlobalService {
    @Autowired
    AccountMapper accountMapper;
    public void testGlobal(){
        accountMapper.update();
//        HttpUtils.get("http://localhost:9101/test", null);
        throw new RuntimeException("12");
    }
}

当提交事务时日志中可以看到开启了一个全局事务与一个子事务,若是只有global事务没有Branch提示可能是哪里出了错(1.4.2出了这个问题)
还有一点要查看sql是否更新了数据,比如update xxx from xxx where id=1这条数据不存在,因此没有更新任何数据时候,seata日志将不会显示commit,此时要注意查看,有可能不是没有开启事务,只是没有发生数据的更改而已
在这里插入图片描述
事务回滚时候
在这里插入图片描述

调用第二个服务

service

public class GlobalService {
    @Autowired
    AccountMapper accountMapper;
    public void testGlobal(){
        accountMapper.update();
        HttpUtils.get("http://localhost:9101/test", null);
    }
}

需要注意的是这里进行http通信时候要把xid传过去,feign被seata改写了默认会传过去,自己构造的http要手动传一下

 public static String get(String url, Map<String, String> headers) {
        HttpGet req = new HttpGet(url);
        String xId = RootContext.getXID();
        log.info("xid:{}", xId);
        if (headers == null){
            headers = new HashMap<>();
        }
        headers.put("TX_XID", xId);
        addHeader(req, headers);
        return execute(req);
    }

被调用的服务要接收这个xid绑定到RootContext里面,starter中有个SeataHandlerInterceptor已经被声明了,会自己拦截header绑定xid,有特殊需求我们可以自己写一下,手动绑定

public class SeataHandlerInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(SeataHandlerInterceptor.class);

    public SeataHandlerInterceptor() {
        log.info("拦截器初始化成功");
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String xid = RootContext.getXID();
        String rpcXid = request.getHeader("TX_XID");
        if (log.isDebugEnabled()) {
            log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
        }

        if (StringUtils.isBlank(xid) && rpcXid != null) {
            RootContext.bind(rpcXid);
            if (log.isDebugEnabled()) {
                log.debug("bind {} to RootContext", rpcXid);
            }
        }

        return true;
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
        if (StringUtils.isNotBlank(RootContext.getXID())) {
            String rpcXid = request.getHeader("TX_XID");
            if (StringUtils.isEmpty(rpcXid)) {
                return;
            }

            String unbindXid = RootContext.unbind();
            if (log.isDebugEnabled()) {
                log.debug("unbind {} from RootContext", unbindXid);
            }

            if (!rpcXid.equalsIgnoreCase(unbindXid)) {
                log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid);
                if (unbindXid != null) {
                    RootContext.bind(unbindXid);
                    log.warn("bind {} back to RootContext", unbindXid);
                }
            }
        }

    }
}

然后第2个服务就可以开始子事务了,需要注意这里不需要加事务直接,有了xid seata会帮我们自动开启事务

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private Db1Service db1Service;

    @GetMapping
    public Object test(){
        db1Service.test1();
        return 0;
    }
}
@Service
public class Db1Service {

    @Autowired
    private AccountMapper accountMapper;
    
    public void test1(){
        accountMapper.update();
        throw  new RuntimeException("ee");
    }
}

回滚事务

在这里插入图片描述

提交事务成功

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值