分布式事务系列(二):Seata的AT模式使用示例

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata的AT模式基于支持ACID事务的关系型数据库,是2PC(两阶段提交协议)的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离

一阶段本地事务提交前,需要确保先拿到 全局锁 。拿不到 全局锁 ,不能提交本地事务。拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

使用示例

由于Seata在Github提供的Demo包含了多个Moudle,接下来对其中的Seata-JPA模块抽取,方便以后的学习和使用。

Github的代码地址:https://github.com/zuotaorui/seata-jpa

启动Seata-server

Seata-Server在Seata Framework中担任Transaction Coordinator(TC 事务协调者)的角色,负责维护全局和分支事务的状态,驱动全局事务提交或回滚。

采用docker命令启动Seata-Server,并将Seata-Server的配置文件挂载到宿主机

docker run --name seata-server -p 8091:8091   -e SEATA_CONFIG_NAME=file:/root/seata-config/registry -v /Users/albert/Docker/seata:/root/seata-config  fancyfong/seata:1.4.1_arm64

配置文件registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "consul"
  #注册中心采用Consul 实现Seata-Server的高可用
  consul {
    cluster = "seata-server"
    serverAddr = "192.168.0.107:8500"
    aclToken = ""
  }

}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"
  # 配置中心
  file {
    name = "file:/root/seata-config/file.conf"
  }
}

配置文件file.conf

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "hikari"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://192.168.0.107:3306/seata?rewriteBatchedStatements=true&autoReconnect=true"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

示例项目

示例项目采用Spring Boot ,包含Business-Service 、Order-Service、Account-Service、Stock-Service,用来模拟电商系统的下单流程,保证订单创建、账户余额扣减、库存更新等操作的原子性。

 引入Seata依赖

 <dependency>
       <groupId>io.seata</groupId>
       <artifactId>seata-spring-boot-starter</artifactId>
       <version>1.4.2</version>
</dependency>
<dependency>
       <groupId>com.alibaba.cloud</groupId>
       <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
       <version>2.2.1.RELEASE</version>
       <exclusions>
          <exclusion>
             <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
             </exclusion>
        </exclusions>
</dependency>

 添加Seata配置

示例项目只使用了简单的配置,具体的配置参数可查看官方文档:docs/user/configurations.html

spring.application.name=order-service
#数据库访问采用spring-data-jpa
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_order?useSSL=false&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.minimum-idle=20
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace

#seata事务分组
#1.首先应用程序(客户端)中配置了事务分组(GlobalTransactionScanner构造方法的txServiceGroup参数)
#2.应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping.[事务分组配置项],
#   取得配置项的值就是TC集群的名称
#3.拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同(前提是Seata-Server
# 已经完成服务注册,且Seata-Server向注册中心报告cluster名与应用程序(客户端)配置的集群名称一致)
#4.拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表(即Seata-Server集群节点列表)
seata.tx-service-group=my_test_tx_group
#尝试本地采用consul配置中心后,由于采用docker部署,导致获取到的Seata-Server的IP地址不正确
#目前采用的注册类型为file,未实现高可用
seata.registry.type=file
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091

代理DataSource 

 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚

@Configuration
public class DataSourceConfig {

    protected static <T> T createDataSource(DataSourceProperties properties,
            Class<? extends DataSource> type) {
        return (T) properties.initializeDataSourceBuilder().type(type).build();
    }

    /**
     ** SpringBoot 2.0以后,默认的数据库连接池Hikari
     **/
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = createDataSource(properties,
                HikariDataSource.class);
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }

    /**
     ** 使用Seata代理DataSource,使用@Primary赋予bean更高的优先级
     **/
    @Primary
    @Bean("dataSource")
    public DataSource dataSource(HikariDataSource hikariDataSource) {
        return new DataSourceProxy(hikariDataSource);
    }

}

开启分布式事务 

分布式事务的发起者在Seata中担任Transaction Manager ( TM 事务管理器) 的角色,定义全局事务的范围,开始全局事务、提交或回滚全局事务,只需在方法上面加上注解@GlobalTransactional

@Service
public class BusinessService {

    @Autowired
    private StockFeignClient stockFeignClient;
    @Autowired
    private OrderFeignClient orderFeignClient;

    /**
     * 减库存,下订单
     *
     * @param userId
     * @param commodityCode
     * @param orderCount
     */
    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        stockFeignClient.deduct(commodityCode, orderCount);

        orderFeignClient.create(userId, commodityCode, orderCount);
    }
}

其他分布式事务的参与者在Seata中担任Resource Manager ( RM 资源管理器) 的角色,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚,只需在方法上面加上注解@Transactional

效果显示

调用business-service的 localhost:9000/purchase/rollback  的API之后,触发在Account-Service中模拟的异常。

@Transactional(rollbackFor = Exception.class)
public void debit(String userId, BigDecimal num) {
    Account account = accountDAO.findByUserId(userId);
    account.setMoney(account.getMoney().subtract(num));
    accountDAO.save(account);

    if (ERROR_USER_ID.equals(userId)) {
            throw new RuntimeException("account branch exception");
    }
}

在account-service中可以看到库存更新操作的回滚(分布式事务实现2PC阶段的第二阶段 )

 示例项目的代码已上传至GitHub,有兴趣的同学可以下载下来自己验证。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值