Spring Cloud Alibaba 5 seata搭建


Spring Cloud Alibaba seata

seata简介

  • Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

  • 官网文档:https://seata.io/zh-cn

  • Seata的三大角色

  • TC(Transaction Coordinator)- 事务协调者
    维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM(Transaction Manager)- 事务管理器
    定义全局事务的范围:开始全局事务、提交或回滚全局事务

  • RM(Resource Manager)- 资源管理器
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

  • 其中,TC为单独部署的Server服务端,TM和RM为嵌入到应用中的Client客户端。

  • 事务模式

  • AT模式,用户只需关注自己的业务sql,用户的业务sql作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。

  • AT模式如何做到对业务的无侵入:

    • 一阶段:在一阶段,Seata会拦截业务sql,首先解析sql语义,找到业务sql要更新的业务数据,在业务数据被更新之前,将其保存成before image,然后执行业务sql更新业务数据,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
    • 二阶段提交:二阶段如果是提交的话,因为业务sql在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删除,完成数据清理即可。
    • 二阶段回滚:二阶段如果是回滚的话,Seata就需要回滚一阶段已执行的业务sql,还原业务数据。回滚方式便是用before image 还原业务数据,但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据。如果不一致,就说明有脏写,出现脏写就需要转人工处理。
  • TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作。事务发起方在一阶段执行Try方法,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法。

  • 优点:在整个过程中基本没有锁,性能更强。

  • 缺点: 侵入性比较强,并且需要用户自己实现相关事务控制逻辑。

安装配置seata

下载

解压配置registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = "seata"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
...
...
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "seata"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }

Nacos中添加配置文件

  • 新建命名空间seata
  • 在seata下创建配置文件seataServer.properties
  • 分组为SEATA_GROUP(本次使用的是redis,需要seata1.3版本以上)
    store.redis.host=127.0.0.1
    store.redis.port=6379
    store.redis.maxConn=10
    store.redis.minConn=1
    store.redis.database=9
    store.redis.queryLimit=100
  • 其中也能使用mysql:
    service.vgroupMapping.my_tx_group=default
    store.mode=redis
    -----db-----
    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=root
    store.db.password=root
    ----client----
    client.undo.logTable=undo_log

启动

  • windows:bin目录下双击seata-server.bat启动
  • linux:命令行启动 seata-server.sh -h 127.0.0.1 -p 8091

搭建项目

添加依赖

  • 在执行业务的项目下添加依赖,参与业务的项目也需要
    <dependency>
        groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
        <version>1.4.0</version>
    </dependency>

seata配置文件

seata:
  enabled: true
  #seata注册的服务名称
  application-id: seata-service
  #此处配置自定义的seata事务分组名称
  tx-service-group: my_tx_group
  #开启数据库代理
  enable-auto-data-source-proxy: false
  config:
    type: nacos
    nacos:
      #nacos地址
      server-addr: 127.0.0.1:8848
      #配置文件的命名空间
      namespace: seata
      dataId: "seataServer.properties"
      #配置文件的分组名
      group: SEATA_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      #注册服务的命名空间
      namespace: seata
      #注册服务的名称
      application: seata-server
      #注册服务的分组名
      group: SEATA_GROUP
      username: nacos
      password: nacos
  • 然后在nacos控制台添加一个配置文件(seata命名空间下)
  • Data ID: service.vgroupMapping.my_tx_group
  • group: SEATA_GROUP
  • 内容:
    default
  • 添加Seata需要用到的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;
    
  • 在调用的service上添加@GlobalTransactional即可开启事务

测试

  • 测试seata的事务是否有效

数据库

  • 随便创建几个测试的数据库,比如user, good, user_good, record
  • 其中可以在每个项目上配置不同的数据库,比如项目1配置1数据库(user表),项目2配置2数据库(good, user_good表),项目3配置3数据库(record表)
  • 三个业务,扣除用户gold,user_good新增记录,record新增记录
  • 抛出异常查看是否回滚
  • 数据库:
    CREATE TABLE `user`  (
      `user_id` int(11) NOT NULL AUTO_INCREMENT,
      `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `user_gold` decimal(6, 1) NULL DEFAULT NULL,
      PRIMARY KEY (`user_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    INSERT INTO `user` VALUES (1, 'zwq', 136.1);
    
    CREATE TABLE `good`  (
      `good_id` int(11) NOT NULL AUTO_INCREMENT,
      `good_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `price` decimal(10, 1) NULL DEFAULT NULL,
      PRIMARY KEY (`good_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    INSERT INTO `good` VALUES (1, '测试商品', 34.5);
    
    CREATE TABLE `user_good`  (
      `ug_id` int(11) NOT NULL AUTO_INCREMENT,
      `ug_user_id` int(11) NOT NULL,
      `ug_good_id` int(11) NOT NULL,
      PRIMARY KEY (`ug_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1691336706 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    CREATE TABLE `record`  (
      `record_id` int(11) NOT NULL AUTO_INCREMENT,
      `record_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `record_user_id` int(11) NULL DEFAULT NULL,
      `record_good_id` int(11) NULL DEFAULT NULL,
      `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
      PRIMARY KEY (`record_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1519374338 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

项目代码

  • 结构
  • nacos, sentinel, record分别放用户,用户商品,记录的业务。
  • nacos
    @Override
    public UserDO getUser(int id) {
    	log.info("Seata全局事务id=================>{}", RootContext.getXID());
    	com.zzz.pped.first.pojo.dos.UserDO userDO = userMapper.selectById(id);
    	com.zzz.dubboapi.test.pojo.dos.UserDO userDOTarget = new com.zzz.dubboapi.test.pojo.dos.UserDO();
    	BeanUtils.copyProperties(userDO, userDOTarget);
    	return userDOTarget;
    }
  • sentinel
    @Override
    public GoodDO getGood(int id) {
    	com.zzz.pped.test.pojo.dos.GoodDO goodDO = goodMapper.selectById(id);
    	GoodDO target = new GoodDO();
    	BeanUtils.copyProperties(goodDO, target);
    	return target;
    }
    
    @Override
    public int insertUserGood(UserGoodDO userGoodDO) {
    	com.zzz.pped.test.pojo.dos.UserGoodDO target = new com.zzz.pped.test.pojo.dos.UserGoodDO();
    	BeanUtils.copyProperties(userGoodDO, target);
    	return userGoodMapper.insert(target);
    }
  • record
    @Override
    public int insertRecord(RecordDO recordDO) {
    	com.zzz.pped.test.pojo.dos.RecordDO target = new com.zzz.pped.test.pojo.dos.RecordDO();
    	BeanUtils.copyProperties(recordDO, target);
    	return recordMapper.insert(target);
    }
  • 测试整个业务的接口
    @Override
    @GlobalTransactional
    public String test() {
    	GoodDO goodDO = goodService.getGood(1);
    	UserDO userDO = userService.getUser(1);
    	userDO.setGold(userDO.getGold().subtract(goodDO.getPrice()));
    	userService.updateUser(userDO);
    	if (userDO != null) {
    		throw new RuntimeException("回滚");
    	}
    	
    	RecordDO recordDO = new RecordDO();
    	recordDO.setUserId(userDO.getId());
    	recordDO.setGoodId(goodDO.getId());
    	recordDO.setContent(new Date() + userDO.getName() + "买了" + goodDO.getName());
    	recordService.insertRecord(recordDO);
    
    	UserGoodDO userGoodDO = new UserGoodDO();
    	userGoodDO.setUserId(userDO.getId());
    	userGoodDO.setGoodId(goodDO.getId());
    	userGoodService.insertUserGood(userGoodDO);
    	return userDO.toString();
    }
  • 调用接口,查看正常调用和抛出异常的区别

常见问题

  1. linux启动seata在nacos显示的IP不对,微服务访问不到,修改linux hostname不能为localhost, hostname test
  2. seata控制台rollback successful 但是数据库未回滚,启动类加上@EnableAutoDataSourceProxy可能解决

  目录