zoukankan      html  css  js  c++  java
  • 分布式事务框架seata入门

    一、简介

    在近几年流行的微服务架构中,由于对服务和数据库进行了拆分,原来的一个单进程本地事务变成多个进程的本地事务,这时要保证数据的一致性,就需要用到分布式事务了。分布式事务的解决方案有很多,其中国内比较主流的框架就是Seata了。

    Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

    这里推荐使用AT模式,该模式具备代码侵入性小,性能高等优点,前提是:

    1. 基于支持本地 ACID 事务的关系型数据库

    2. Java 应用,通过 JDBC 访问数据库。

    二、环境搭建

    版本清单:

    名称 版本
    Nacos 1.3.2
    Seata 1.4.2
    spring-boot 2.2.6.RELEASE
    spring-cloud-starter-alibaba-nacos-discovery 2.2.6.RELEASE
    spring-cloud-starter-openfeign 2.2.6.RELEASE
    seata-spring-boot-starter 1.4.2
    spring-cloud-starter-alibaba-seata 2.2.1.RELEASE

    2.1 Nacos安装

    本地搭建nacos比较简单,首先通过github下载nacos(我的是1.3.2版本,下载地址),然后解压缩进入bin目录,打开命令行工具运行如下命令即可启动。

    startup.sh -m standalone
    

    启动后访问http://localhost:8848/nacos/index.html即可,默认账号密码是nacos/nacos。

    2.2 部署seata-server

    • 1.下载解压

      进入seata的发行页面,选择需要的版本下载,然后解压。

    • 2.修改配置文件

      进入conf目录(如/Users/ship/program/seata/seata-server-1.4.2/conf),可以看到有file.conf和registry.conf两个配置文件。

      首先打开file.conf文件,修改配置如下

      ## transaction log store, only used in seata-server
      store {
        ## store mode: file、db、redis
        mode = "file" // 改为db
        ## rsa decryption public key
        publicKey = ""
        ## file store property
        file {
          ## store location dir
          dir = "sessionStore"
          # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
          maxBranchSessionSize = 16384
          # globe session size , if exceeded throws exceptions
          maxGlobalSessionSize = 512
          # file buffer size , if exceeded allocate new buffer
          fileWriteBufferCacheSize = 16384
          # when recover batch read size
          sessionReloadReadSize = 100
          # async, sync
          flushDiskMode = async
        }
      
        ## database store property
        db {
          ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
          datasource = "druid"
          ## 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://127.0.0.1:3306/seata?rewriteBatchedStatements=true"// 改成自己的数据库地址
          user = "mysql"   // 改成自己的数据库用户名
          password = "mysql"// 改成自己的数据库密码
          minConn = 5
          maxConn = 100
          globalTable = "global_table"
          branchTable = "branch_table"
          lockTable = "lock_table"
          queryLimit = 100
          maxWait = 5000
        }
      
        ...
      }
      
      

      这个数据库会在下面的第四步再创建。

      然后打开registry.conf文件,修改注册中心为nacos

      registry {
        # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
        type = "file" // 改为nacos
      
        nacos {
          application = "seata-server"
          serverAddr = "127.0.0.1:8848"
          group = "SEATA_GROUP"
          namespace = "" // 默认名称空间为public
          cluster = "default"
          username = "" // 默认是不需要密码的,如果开启了安全验证则要填写
          password = ""
        }
        eureka {
          serviceUrl = "http://localhost:8761/eureka"
          application = "default"
          weight = "1"
        }
        ...
      }
      
      config {
        # file、nacos 、apollo、zk、consul、etcd3
        type = "file" 
      
        nacos {
          serverAddr = "127.0.0.1:8848"
          namespace = ""
          group = "SEATA_GROUP"  
          username = ""
          password = ""
          dataId = "seataServer.properties"
        }
        ...
      }
      
      
    • 3.同步配置到nacos

      低版本的seata需要在项目的resource目录下创建file.conf和registry.conf文件,高版本的只需要将配置信息同步到nacos,然后读取即可。

      首先需要在conf的同级目录(如/Users/ship/program/seata/seata-server-1.4.2)下创建config.txt(下载地址)文件,然后修改数据库配置信息。

      store.db.datasource=druid
      store.db.dbType=mysql
      store.db.driverClassName=com.mysql.jdbc.Driver
      store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
      store.db.user=seata  // 改为自己的账号
      store.db.password=nova2020 // 改为自己的密码
      store.db.minConn=5
      store.db.maxConn=30
      

      最后进入conf目录(如/Users/ship/program/seata/seata-server-1.4.2/conf),下载nacos-config.sh(下载地址)并使用nacos-config.sh文件同步上传配置到Nacos,命令如下:

      sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 023933c2-2825-46b2-a05a-4a407b479877
      

      命令参数介绍:

      -h : 指定nacos的ip地址。

      -p: 指定nacos的端口号。

      -g: 指定分组

      -t: 指定nacos的名称空间,建议为seata单独创建一个名称空间。

      -u: nacos的用户名,开启认证才需要。

      -w: nacos的密码,开启认证才需要。

      打开nacos即可在对应的名称空间下看到那些配置信息,如图:

      不得不吐槽一下,像config.text和nacos-config.sh这些文件在0.9.0版本的seata都是和安装包放一起的,高版本的还需要自己找真的坑。

    • 4.创建数据库

      为seata-server创建seata库并执行db_store.sql ,下载地址

      为每个业务库执行db_undo_log.sql以添加回滚日志的表,下载地址

    • 5.启动

    进入bin目录,输入sh seata-server.sh即可启动,部分启动日志如下:

    SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
    SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
    SLF4J: See also http://www.slf4j.org/codes.html#replay
    16:45:02.688  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
    16:45:02.693  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /Users/ship/program/seata/seata-server-1.4.2/conf/registry.conf
    16:45:02.785  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
    16:45:02.785  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /Users/ship/program/seata/seata-server-1.4.2/conf/file.conf
    16:45:03.411  INFO --- [                     main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
    16:45:03.843  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091
    

    三、实战

    前面环境已经搭好了,现在通过一个示例来验证分布式事务的AT模式。

    场景是创建订单时会根据商品的金额来扣减用户余额,分别对应order服务和account服务。

    表结构设计如下:

    DROP TABLE IF EXISTS `order_tbl`;
    CREATE TABLE `order_tbl` (
                                 `id` int(11) NOT NULL AUTO_INCREMENT,
                                 `user_id` varchar(255) DEFAULT NULL,
                                 `commodity_code` varchar(255) DEFAULT NULL,
                                 `count` int(11) DEFAULT 0,
                                 `money` int(11) DEFAULT 0,
                                 PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    
    DROP TABLE IF EXISTS `account_tbl`;
    CREATE TABLE `account_tbl` (
                                   `id` int(11) NOT NULL AUTO_INCREMENT,
                                   `user_id` varchar(255) DEFAULT NULL,
                                   `money` int(11) DEFAULT 0,
                                   PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    3.1order服务

    创建一个order项目并添加nacos、fegin和seata的依赖,pom.xml部分如下:

     <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <version>2.2.6.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
                <version>2.2.6.RELEASE</version>
            </dependency>
    
            <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-spring-boot-starter,就在spring-cloud-starter-alibaba-seata作了排除,也是官方推荐的方式。如果发现你的项目启动不起来或者有其他问题,可能是版本依赖有问题。

    启动类OrderApplication.java添加需要的注解

    @EnableAutoDataSourceProxy //这个一定要加,如果不加通过配置文件开启也可以
    @EnableFeignClients
    @EnableDiscoveryClient
    @SpringBootApplication
    public class OrderApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderApplication.class, args);
        }
    
    }
    

    核心部分OrderService

    /**
     * @Author: Ship
     * @Description:
     * @Date: Created in 2021/8/23
     */
    @Service
    public class OrderService {
    
        @Autowired
        private OrderDao orderDao;
    
        @Autowired
        private AccountClient accountClient;
    
        @GlobalTransactional
        @Transactional(rollbackFor = Exception.class)
        public OrderVO create(OrderDTO orderDTO) {
            // 创建订单
            Order order = new Order();
            BeanUtils.copyProperties(orderDTO,order);
            orderDao.insert(order);
            // 扣除账户余额
            AccountDeductDTO accountDeductDTO = new AccountDeductDTO();
            Integer total = orderDTO.getMoney() * orderDTO.getCount();
            accountDeductDTO.setMoney(total);
            accountDeductDTO.setUserId(orderDTO.getUserId());
            accountClient.deduct(accountDeductDTO);
            return new OrderVO(order.getId());
        }
    }
    

    只需一个@GlobalTransactional注解即可,用在分布式事务开启的方法上。

    修改配置文件bootstrap.yml

    spring:
      application:
        name: order
      datasource:
        username: root
        password: 1234
        url: jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
        type: com.alibaba.druid.pool.DruidDataSource
    
    server:
      port: 9900
    seata:
      registry:
        type: nacos
        nacos:
          application: seata-server  # 不配置名称空间,默认public
          server-addr: 127.0.0.1:8848
          group: "SEATA_GROUP"
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group: "SEATA_GROUP"
          namespace: 023933c2-2825-46b2-a05a-4a407b479877 # 配置名称空间为seata
      enabled: true
      tx-service-group: my_test_tx_group  # 要与nacos上的配置一致
      service:
        vgroup-mapping:
          my_test_tx_group: default # 要与nacos上的配置一致
        disable-global-transaction: false
    

    注意:seata.tx-service-group属性值要和seata-server的config.txt文件中的service.vgroupMapping.${分组名}=default分组名一一对应,这里我用的默认配置my_test_tx_group。关于事务分组还有很多种玩法,可以参考这里https://seata.io/zh-cn/docs/user/txgroup/transaction-group.html。

    3.2 account服务

    account服务项目结构基本与order服务一致,只是service代码不同。

    /**
     * @Author: Ship
     * @Description:
     * @Date: Created in 2021/8/23
     */
    @Service
    public class AccountService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Transactional(rollbackFor = Exception.class)
        public void deduct(AccountDeductDTO accountDeductDTO) {
            QueryWrapper<Account> wrapper = new QueryWrapper();
            wrapper.lambda().eq(Account::getUserId, accountDeductDTO.getUserId());
            Account account = accountDao.selectOne(wrapper);
            Integer money = account.getMoney() - accountDeductDTO.getMoney();
            account.setMoney(money);
    
            // 更新余额
            accountDao.updateById(account);
    
    //        int i = 1 / 0;
        }
    }
    

    3.3 测试

    1. 启动seata-server

    2. 启动order服务和account服务,并能成功在nacos上看到注册实例。

    1. 首先给用户1111的账户初始100元的余额,sql如下

      insert into account_tbl(user_id,money) VALUES(1111,100);
      
    2. 请求下单接口http://localhost:9900/order/create,POST body参数如下:

      {
      	"userId":1111,
      	"commodityCode":"code",
      	"count":2,
      	"money":10
      } 
      

      查询数据库可以发现,order_tbl表已经有一条订单数据了,并且用户的1111的余额变成了80,说明事务提交成功

    1. 这时将account服务的AccountService中的int i = 1 / 0;这行代码取消注释,并在重启account服务之后再次请求下单接口。

    2. 查看控制台日志发现抛异常了,再次查询order_tbl表发现还是一条订单数据,account_tbl表的用户余额也还是80,说明发生了全局事务回滚。 通过order服务的日志也可以看出,account服务扣减余额接口异常导致了回滚。

      2021-09-04 21:18:28.323  INFO 72668 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.3.253:8091:3900290643103043585,branchId=3900290643103043591,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_order,applicationData=null
      2021-09-04 21:18:28.326  INFO 72668 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.3.253:8091:3900290643103043585 3900290643103043591 jdbc:mysql://127.0.0.1:3306/seata_order null
      2021-09-04 21:18:28.327  INFO 72668 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
      2021-09-04 21:36:17.280  INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.3.253:8091:3900290643103043595]
      2021-09-04 21:36:17.282 ERROR 72668 --- [nio-9900-exec-3] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, jdbcUrl : jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, lastPacketReceivedIdleMillis : 1068019
      2021-09-04 21:36:17.782  INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : Suspending current transaction, xid = 192.168.3.253:8091:3900290643103043595
      2021-09-04 21:36:17.782  INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.3.253:8091:3900290643103043595] rollback status: Rollbacked //回滚
      

      至此说明我们的分布式事务控制生效了,示例代码包括脚本都已经提交到我的github上,需要的请点击

    四、总结

    Seata框架上手不难,重点还是理解其实现原理和做到灵活使用,比如事务分组的设计就很巧妙,其AT模式比起之前用过的TCC框架好太多,阿里出品还是厉害啊。

    参考资料:

    seata官方文档,https://seata.io/zh-cn/docs/overview/what-is-seata.html

    https://github.com/seata/seata-samples

  • 相关阅读:
    实验 3:Mininet 实验——测量路径的损耗率
    福州大学软件工程实践个人编程作业
    软件定义网络实验 2:Mininet 实验——拓扑的命令脚本生成
    软件定义网络实验 1:Mininet 源码安装和可视化拓扑工具
    第一组冲刺收尾作业——团队总结
    第十天alpha冲刺(11月29日)
    alpha汇总博客
    第九天alpha冲刺(11月26日)
    第八天alpha冲刺(11月25日)
    第七天alpha冲刺(11月23日)
  • 原文地址:https://www.cnblogs.com/2YSP/p/15228036.html
Copyright © 2011-2022 走看看