八、Seata的XA模式

八、Seata的XA模式

8.1 XA原理流程图

在这里插入图片描述

8.2 流程图详细说明

-

8.3 XA模式多数据源场景

1 环境搭建

  • 建库建表
    代码的db.sql中

  • 创建工程
    在这里插入图片描述

  • 添加依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>
  • 编写主配置类
@SpringBootApplication
public class XAMultiApp {
    public static void main(String[] args) {
        SpringApplication.run(XAMultiApp.class);
    }
}
  • 配置文件
server:
  port: 8080
spring:
  datasource:
    nong:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.239.11:3306/nonghang?serverTimezone=UTC
      username: root
      password: houchen
    jian:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.239.11:3306/jianhang?serverTimezone=UTC
      username: root
      password: houchen
logging:
  level:
    com:
      hc: debug
  • mapper + 业务代码
    见上述 gitee 仓库

  • 编写多数据源配置

@SpringBootConfiguration
@MapperScans({@MapperScan(basePackages = "com.hc.nong", sqlSessionFactoryRef = "nongFactory"), @MapperScan(basePackages = "com.hc.jian", sqlSessionFactoryRef = "jianFactory")})
public class DbConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.nong")
    public DruidDataSource nongDataSource() {
        return new DruidDataSource();
    }


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.jian")
    public DruidDataSource jianDataSource() {
        return new DruidDataSource();
    }


    /**
     * 配置NongSqlSession
     */
    @Bean
    public SqlSessionFactory nongFactory(@Qualifier("nongDataSource") DruidDataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public SqlSessionFactory jianFactory(@Qualifier("jianDataSource") DruidDataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }

}

2、使⽤XA模式解决事务

  • 添加依赖
<dependency>
     <groupId>io.seata</groupId>
     <artifactId>seata-spring-boot-starter</artifactId>
     <version>1.5.2</version>
 </dependency>
  • yaml文件
    注意:
    我们添加的是seata-spring-boot-starter
    依赖⽀持registry.conf和 file.conf 同时也⽀持把配置编写在application.yml中

但是:
当我们添加的是seata-all依赖时 我们需要添加registry.conf和 file.conf 不能使⽤yml格式配置
因为seata-all没有⾃动配置 所以依赖的不同的包 配置是不同的

seata:
  enabled: true
  registry:
    type: file
  config:
    type: file
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
    disable-global-transaction: false #默认为false 可以不配置
  application-id: abc # 初始化TM和RM使用
  tx-service-group: default_tx_group
  enable-auto-data-source-proxy: true # 设置datasource自动代理
  data-source-proxy-mode: XA          # 指定代理模式 XA
  • ⾃动代理的⽅式
    就是上面yaml文件的最后两行配置

  • 业务代码上面添加注解

   @GlobalTransactional(rollbackFor = Exception.class)
    public void transfer(int fromId, int toId, double monoey) {
        nongMapper.reduceMoney(fromId, monoey);
        int i = 10 / 0;
        jianMapper.increaseMoney(toId, monoey);
    }
  • 测试
    调用接口 http://localhost:8080/user/transfer?fromId=1&toId=1&monoey=100,查看日志
    在这里插入图片描述

⼿动代理的⽅式

  • 修改配置关闭⾃动代理
    在这里插入图片描述

  • ⼿动构建代理数据源

@SpringBootConfiguration
@MapperScans({@MapperScan(basePackages = "com.hc.nong", sqlSessionFactoryRef = "nongFactory"), @MapperScan(basePackages = "com.hc.jian", sqlSessionFactoryRef = "jianFactory")})
public class DbConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.nong")
    public DruidDataSource nongDataSource() {
        return new DruidDataSource();
    }


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.jian")
    public DruidDataSource jianDataSource() {
        return new DruidDataSource();
    }


    /**
     * ⼿动配置农⾏代理数据源
     * @param dataSource
     * @return
     */
    @Bean
    public DataSourceProxyXA nongXA(@Qualifier("nongDataSource") DruidDataSource dataSource){
        return new DataSourceProxyXA(dataSource);
    }

    /**
     * ⼿动代理建⾏数据源
     * @param dataSource
     * @return
     */
    @Bean
    public DataSourceProxyXA jianXA(@Qualifier("jianDataSource") DruidDataSource dataSource){
        return new DataSourceProxyXA(dataSource);
    }
    /**
     * 配置NongSqlSession
     */
    /*@Bean
    public SqlSessionFactory nongFactory(@Qualifier("nongDataSource") DruidDataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public SqlSessionFactory jianFactory(@Qualifier("jianDataSource") DruidDataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }*/

    /**
     * 配置NongSqlSession
     */
    @Bean
    public SqlSessionFactory nongFactory(@Qualifier("nongXA") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }
    @Bean
    public SqlSessionFactory jianFactory(@Qualifier("jianXA") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }
}



8.4 XA模式分库分表场景

添加依赖

 <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-transaction-xa-core</artifactId>
            <version>4.1.1</version>
        </dependency>

添加注解

@GetMapping("/transfer")
    @ShardingTransactionType(TransactionType.XA)
    @Transactional
    public String transfer(Long fromId, Long toId, double money) {
        User fromUser = userMapper.findById(fromId);
        User toUser = userMapper.findById(toId);
        fromUser.setMoney(fromUser.getMoney() - money);
        toUser.setMoney(toUser.getMoney() + money);
        userMapper.updateUser(fromUser);
        int i = 1 / 0;
        userMapper.updateUser(toUser);
        return "success";
    }

8.5 XA模式微服务场景

1、环境搭建

创建订单 —> 扣库存 —> 扣减余额

具体见 gitee : https://gitee.com/houchen1996/seata-shangma
xa-cloud-master

2、XA默认解决事务问题

1) 通⽤⼯程添加依赖

 <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>
  1. 所有⼯程添加配置
seata:
  service:
    vgroup-mapping:
      shangma: default
    grouplist:
      default: 127.0.0.1:8091
    disable-global-transaction: false #默认为false 可以不配置
  tx-service-group: shangma
  enable-auto-data-source-proxy: true
  data-source-proxy-mode: XA

3) 业务系统修改
@SpringBootApplication(exclude = SeataTCCFenceAutoConfiguration.class)

// 新版本的seata tcc模式需要数据源,而业务系统并没有配置数据源
@SpringBootApplication(exclude = SeataTCCFenceAutoConfiguration.class)
public class XABusinessApp {
    public static void main(String[] args) {
        SpringApplication.run(XABusinessApp.class, args);
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
        interceptors.add(new XIDIntercepter());
        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }

}

4)业务系统添加 @GlobalTransactional

@Service
@Slf4j
public class BusinessService {


    @Autowired
    RestTemplate restTemplate;

    @GlobalTransactional
    public void placeOrder(int accountId, int goodId, int num) {
        log.info("order是否在事务中:{}", RootContext.inGlobalTransaction());
        log.info("全局事务ID:{}", RootContext.getXID());
        log.info("事务模式:{}", RootContext.getBranchType());

        Good good = restTemplate.getForObject(String.format(Urls.GOOD_INFO, goodId), Good.class);
        double amount = good.getGoodPrice() * num;
        Order order = new Order().setGoodId(goodId).setGoodNum(num).setAccountId(accountId).setOrderAmount(amount).setStatus(OrderStatus.CREATE.getValue());
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders);
        // 创建订单
        restTemplate.postForObject(Urls.CEATE_ORDER, httpEntity, String.class);
        // 减库存
        restTemplate.getForObject(String.format(Urls.REDUCE_STOCK, goodId, num), Object.class);
        // 减余额
        restTemplate.getForObject(String.format(Urls.REDUCE_MONEY, accountId, amount), Object.class);

    }
}

5)测试问题分析
微服务下,事务失效的两个原因:

  • 使⽤⾃带的HikariDataSource数据源 切换XA时⽆效 通过打印查看还是默认的AT (兼容问题)

  • 在业务系统开启全局事务 全局事务id返回给了业务系统,但是订单 账户 商品服务想要注册分⽀事
    务 需要全局事务id 此时全局事务id需要从业务系统传递给订单 账户 商品服务 此时各个服务才能
    注册分⽀事务

3、全局事务id传递问题解决

1) 业务系统定义http请求拦截器

public class XIDIntercepter implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
        if (StringUtils.isNotBlank(RootContext.getXID())) {
            httpRequest.getHeaders().add(RootContext.KEY_XID, RootContext.getXID());
        }

        return clientHttpRequestExecution.execute(httpRequest, bytes);
    }
}

2) 业务系统http请求设置拦截器

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
        interceptors.add(new XIDIntercepter());
        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }
  1. common模块添加添加拦截器
@Component
public class ReceiveXIDInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String xid = request.getHeader(RootContext.KEY_XID);
        if (StringUtils.isNotBlank(xid)) {
            RootContext.bind(xid);
        }
        return true;
    }
}

请求: http://localhost:8103/placeOrder?accountId=1&goodId=1&num=100
在这里插入图片描述

4、数据源兼容问题解决 - 使用durid数据源

1) common模块添加依赖
// 为了引入 PlatformTransactionManager 类

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <optional>true</optional>
        </dependency>
  1. common工程添加配置类
@Configuration
@ConditionalOnClass(PlatformTransactionManager.class)
public class DbConfig {


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }
}

5、使⽤Cloud包优化

刚刚我们添加的是Seata的boot下的依赖 此时我们在微服务项⽬中 想要让事务⽣效,需要⼿动传递事务id

如果调⽤的服务⽐较多 调⽤链路⽐较复杂 每次都要⼿动传递事务id 很显然不够优雅 此时我们可以使⽤springCloud提供的seata包 来优化传递id 因为springCloud包 已经把传递事务id完成了 我们不需要关注事务ID的传递

并且在实际开发中 我们不仅可以使⽤Ribbon远程调⽤负载均衡也可以使⽤OpenFeign调⽤

改造项⽬ 使⽤cloud包 以及OpenFeign远程调⽤

见 gitee : https://gitee.com/houchen1996/seata-shangma

6、异常处理事务失效

以上的案例 之所以事务会回滚 是因为代码出现异常 TM通知TC让RM回滚事务

如果代码使⽤全局异常处理不让代码出异常,则seata是⽆法感知我们项⽬是出错的 此时全局事务就会失效

解决⽅式
⽅式⼀: 底层的各个服务异常不处理,只需要在业务系统处理异常,因为业务系统⾯向的是⽤户,所以不能看到错误⻚⾯

⽅式⼆: 在全局异常⾥⾯⼿动回滚在这里插入图片描述

7、服务降级事务失效

1) 代码演示

  • 通⽤⼯程添加依赖
<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
  • 定义降级fallback类
@Component
public class AccountFeignClientFallback implements AccountFeignClient{


    @Override
    public Account findAccountById(int id) {
        return null;
    }

    @Override
    public String reduceMoney(int accountId, double money) {
        System.out.println("reduceMoney 方法降级 !!!!");
        return null;
    }
}

  • feign接口指定降级策略
@FeignClient(value = "seata-xa-account",fallback = AccountFeignClientFallback.class)
public interface AccountFeignClient {

    @GetMapping("/account/{id}")
    Account findAccountById(@PathVariable("id") int id);


    @GetMapping("/account/reduceMoney")
    String reduceMoney(@RequestParam("accountId")int accountId, @RequestParam("money")double money);
}

  • 业务系统添加feign相关配置
feign:
  client:
    config:
      default:
        connect-timeout: 5000
        read-timeout: 5000
  sentinel:
    enabled: true
  • 测试
    停止account服务,请求 http://localhost:8103/placeOrder?accountId=1&goodId=1&num=10
    可以看到订单创建和库存扣减,但是账户余额没有扣减,分布式事务失效 。
    在这里插入图片描述

2) ⼿动回滚

@Component
public class AccountFeignClientFallback implements AccountFeignClient{


    @Override
    public Account findAccountById(int id) {
        return null;
    }

    @Override
    public String reduceMoney(int accountId, double money) {
        if(RootContext.inGlobalTransaction()) {
            try {
                GlobalTransactionContext.reload(RootContext.getXID()).rollback();
            } catch (TransactionException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("reduceMoney 方法降级 !!!!");
        return null;
    }
}

在这里插入图片描述

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值