Seata1.5.2解决分布式事务问题

39 篇文章 2 订阅
19 篇文章 2 订阅

分布式事务–Seata

​ 前面了解到一些分布式事务的解决方案,业内也涌现出不少解决分布式事务的优秀框架,如Atomikos、Seata等,本章来了解使用下Seata。

​ Seata的前身是Fescar,而后改名Seata,简单可扩展的自治分布式事务框架。Seata为用户提供了AT、TCC、SAGA和XA事务模式(默认使用AT),致力打造的一站式分布式解决方案。

​ Seata是在传统的2PC方案上进行演进,它把一个分布式事务拆分为若干个分支事务的全局事务,全局事务协调管理若干个分支事务,使其达到一致,实现整个事务那么全部成功,要么全部失败,并且在项目中整合Seata几乎没有侵入性。

基本概念

Seata有几个基本的概念:

  • Transaction ID XID:全局唯一事务ID
  • TC(Transaction Coordinator):事务协调者,维护全局事务运行,驱动全局事务的提交和回滚
  • TM(Transaction Manager):事务管理器,定义全局事务边界,负责开启全局事务,发起全局事务的提交或回滚决议
  • RM(Resource Manager):资源管理器,管理分支事务,与TC(事务协调者)通信,决定对分支事务提交或回滚

Seata的下载和安装

Seata像Nacos一样,也有自己的服务端,需要下载服务端程序,地址是:https://github.com/seata/seata/releases

那么如何选择版本呢,还是要按照SpringCloudAlibaba的组件版本对应关系来使用.

在这里插入图片描述

笔者使用的alibaba是2021.0.4.0,seata就使用1.5.2版本即可,下载速度会挺慢,国外网址可以翻墙或者用迅雷等软件下载都可。

conf目录中,有一个application.example.yml文件,内容是关于seata的注册和配置的示例,可以修改下且改名为application.yml。

  • 可以看到配置文件中实现Seata的注册和配置方式有几种:File、Nacos、Eureka、Redis、Consul等等。我们使用Nacos即可。application.yml配置如下:
server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata

# 新增加的console控制台,
# 可通过访问http://localhost:7091进行登录,账号如下seata/seata
console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos 、 consul 、 apollo 、 zk  、 etcd3
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ded91f4b-04df-4c19-8006-755505a27c5e
      group: SEATA_GROUP
      username: nacos
      password: nacos
      # data-id: seataServer.properties
  registry:
    # support: nacos 、 eureka 、 redis 、 zk  、 consul 、 etcd3 、 sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: ded91f4b-04df-4c19-8006-755505a27c5e
      cluster: default
      username: nacos
      password: nacos
  # seata的安全配置
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
  • 到数据库中新增需要的数据表【Seata 1.5.2版本mysql脚本】导入压缩包目录seata/script/db/mysql.sql

在这里插入图片描述

数据表创建完毕

  • 启动nacos,创建一个dev的命名空间方便测试

在这里插入图片描述

  • 修改压缩包目录seata/script/config-center/config.txt文件中几处内容:
# 存储模式
store.mode=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
# 注意根据生产实际情况调整参数host和port
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
# 数据库用户名密码
store.db.user=root
store.db.password=123456
# 微服务里配置与这里一致
service.vgroupMapping.dev_tx_group=default

TIPS📢:
配置事务分组service.vgroupMapping.dev_tx_group=default
dev_tx_group:需要与客户端保持一致 ,可以自定义
default:需要跟客户端和application.yml中的cluster保持一致
default 必须要等于 registry.conf cluster = “default”

  • 官方推荐通过压缩包目录seatascript/config-center/nacos/nacos-config.sh将修改后的config.txt发布到nacos上
# 运行指令 ,通过 Git Bash Here
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t ded91f4b-04df-4c19-8006-755505a27c5e

# 具体说明参见:http://seata.io/zh-cn/docs/user/configurations.html
# -h: nacos host,默认localhost
# -p: nacos端口,默认8848
# -g: nacos分组,默认'SEATA_GROUP'.
# -t: 租户信息Tenant information,对应nacos namespace ID,默认''
# -u: nacos用户名,默认''
# -w: nacos用户密码,默认''

可以看到,配置自动导入到了nacos中

在这里插入图片描述

到seata的bin目录下执行seata-server.sh或bat即可执行服务端。

客户端

那么还是按照上篇文章中的订单服务配送服务来实现。

  • 项目中导入依赖

父工程

<properties>
    <java.version>8</java.version>
    <boot.version>2.6.11</boot.version>
    <cloud.version>2021.0.4</cloud.version>
    <cloud.alibaba.version>2021.0.4.0</cloud.alibaba.version>
</properties>
<dependencyManagement>
    <dependencies>
        <!--springboot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--cloud的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--cloud.alibaba依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${cloud.alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  • 子工程(订单服务和配送服务都要)
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--seata starter -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
</dependency>
  • 订单数据库配送数据库增加undo_log表脚本地址
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

注意📢:每个业务数据库都要有UNDO_LOG

  • 订单服务和配送服务的yml文件主要引入的配置
    • seata.enabled:是否开启自动装配seata
    • seata.application-id:应用id
    • seata.tx-service-group:事务分组
    • seata.enable-auto-datasouce-proxy:数据源自动代理
spring:
  cloud:
    nacos:
      discovery:
        group: SEATA_GROUP
        server-addr: http://localhost:8848
        # 必须填命名空间的ID
        namespace: ded91f4b-04df-4c19-8006-755505a27c5e

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///order?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456

distribution:
  name: distribution


# Seata 配置
seata:
  application-id: order-server  # 自定义
  # 是否启用数据源bean的自动代理
  enable-auto-data-source-proxy: false
  tx-service-group: dev_tx_group  # 必须和服务器配置一样
  registry:
    type: nacos
    nacos:
      # Nacos 服务地址
      server-addr: http://localhost:8848
      group: SEATA_GROUP
      namespace: ded91f4b-04df-4c19-8006-755505a27c5e
      application: seata-server # 必须和服务器配置一样
      username: nacos
      password: nacos
      cluster: default
  config:
    type: nacos
    nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      group: SEATA_GROUP
      namespace: ded91f4b-04df-4c19-8006-755505a27c5e
  service:
    vgroup-mapping:
      tx-service-group: dev_tx_group # 必须和服务器配置一样
    disable-global-transaction: false
  client:
    rm:
      # 是否上报成功状态
      report-success-enable: true
      # 重试次数
      report-retry-count: 5

feign:
  client:
    config:
      default:
        connectTimeout: 2000 # 建立连接超时时间
        readTimeout: 2000   # 读取资源超时时间
  • 配置代理数据源
@Primary
@Bean
public DataSource dataSource() {
    DruidDataSource druidDataSource = new DruidDataSource();
    druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    druidDataSource.setUrl("jdbc:mysql:///distribution?useUnicode=true&characterEncoding=utf-8");
    druidDataSource.setUsername("root");
    druidDataSource.setPassword("123456");
    DataSourceProxy dsp = new DataSourceProxy(druidDataSource);
    return dsp;
}
  • 在事务开始的地方使用注解@GlobalTransactional
@Service
public class OrderServiceImpl implements OrderService {
    @Resource
    JdbcTemplate jdbcTemplate;  // 操作数据库
    @Resource
    DistributionService distributionService;    // 远程调用

    // 模拟菜品数据
    private final Map<Integer, String> shopMap = new HashMap<Integer, String>(){{
       put(1,"菜品1");
       put(2,"菜品2");
       put(3,"菜品3");
    }};

    @Override
    @Transactional  // 加上事务
    @GlobalTransactional // seata全局事务
    public Integer createOrder(Integer id) {
        if (shopMap.containsKey(id)) {
            String orderId = UUID.randomUUID().toString().replace("-","");
            // 增加订单
            int update = jdbcTemplate.update("insert into t_order(order_id, shop_id) values(?,?)",
                    new Object[]{orderId, id});
            // 调用配送服务
            Integer result = distributionService.distribution(orderId);
            if (result <= 0) {
                throw new RuntimeException("分配配送员失败");
            }
            return update;
        }
        return null;
    }
}

踩坑

  • 前面在seata的yml中可以看到使用的是druid的连接池,mybatisPlus默认集成了druid和hikari两种连接池,而mybatis不是,因此需要在使用mybatis的项目中,另外集成druid,在application.yml中声明datasource.type为"com.alibaba.druid.pool.DruidDataSource"

  • seata服务端的配置尽量和客户端做到一致,如driver_class_name:“com.mysql.cj.jdbc.Driver”

测试

curl localhost:9002/createOrder?id=1
{"timestamp":"2023-09-16T04:59:01.614+00:00","status":500,"error":"Internal Server Error","path":"/createOrder"}

可以看到返回报错,同样去看下两个服务的日志情况:订单服务调用配送服务

在这里插入图片描述

可看到,订单服务在调用配送服务5s后,直接超时并且开始回滚数据。

配送服务日志:

在这里插入图片描述

由于本案例是一个超时情况,所以说这里报错是正常的,因为是配送服务还没执行完毕,订单服务就已经去回滚数据了,配送服务执行完毕后收到回滚的信号,也去进行回滚,发现这条xid的数据已经被订单服务回滚过了,报错没找到此XID的数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值