暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SpringCloud-Alibaba:Seata分布式事务-AT模式

Coding On Road 2020-09-28
419


重要声明:

本公众号所有文章,均为本人生产环境原创。非本人允许,不得用于商业宣传。转发请注明出处。



Seata简介

官网地址:http://seata.io/zh-cn/

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

二、Seata快速入门

配置方案为:

Seata使用mysql保存数据。

   使用nacos1.3做的注册发现中心(关于nacos的安装与配置方法,见本人另一文档)

       SpringCloud,Spring Cloud alibaba。


步1、下载Seata-Server

下载二进制安装文件,同时下载源代码(源代码里面有script目前,包含执行sql的脚本文件)

https://github.com/seata/seata/tags

步2、启动nacos

本外配置的nacos使用的是mysql保存的元数据,关于nacos的配置,请参考本人的nacos文章(下同)。

#/app/nacos/bin/startup.sh  -m standalone

启动后,访问,本人已经修改了nacos的密码为:123456。

步3、解压并配置seata

  • 解压:

#tar -zxvf  seata-1.3.tar.gz  -C  app/

seate默认的配置将使用本地文件系统,建议使用mysql做为seata的数据保存位置。通过修改file.conf即可以将seata的数据保存到指定的mysql数据库中。

1、配置使用数据库

修改seata/conf/file.conf,对于文件中其他部分,可以删除,也可以不用理会。

store {

## store mode: file、db、redis

mode = "db"

## 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"

  url = "jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"

  user = "root"

  password = "123456"

  minConn = 5

  maxConn = 30

  globalTable = "global_table"

  branchTable = "branch_table"

  lockTable = "lock_table"

  queryLimit = 100

  maxWait = 5000

  } 

}


如上,数据库配置需要三个表:global_table,branch_table,lock_table。建表语句,可以从seata源代码的script/server中找到。

2、指定seata的注册和配置中心

配置register.conf,指定注册中心为nacos,同时也指定配置中心为nacos:

修改nacos/conf/register.conf

#注册中心为nacos
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 = ""
  cluster = "default"
  username = "nacos"
  password = "123456"
}
}
#同样的指定配置中心也是nacos
config {
 # file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
  serverAddr = "127.0.0.1:8848"
  namespace = ""
  group = "SEATA_GROUP"
  username = "nacos"
  password = "123456"
}

3、创建数据库和表

由于上面已经指定了使用mysql做为事务保存,所以必须要创建一个数据库及三张表,数据库的名称与上面配置对应即可,不一定非要取名为seata。

创建数据库:

#mysql -uroot -p123456

mysql> create database seata character set utf8;

创建表

在seata源代码中,可以找到创建表的语句如下

完整语句如下:

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
   `xid`                       VARCHAR(128) NOT NULL,
   `transaction_id`            BIGINT,
   `status`                    TINYINT      NOT NULL,
   `application_id`            VARCHAR(32),
   `transaction_service_group` VARCHAR(32),
   `transaction_name`          VARCHAR(128),
   `timeout`                   INT,
   `begin_time`                BIGINT,
   `application_data`          VARCHAR(2000),
   `gmt_create`                DATETIME,
   `gmt_modified`              DATETIME,
   PRIMARY KEY (`xid`),
   KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
   KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
 DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
   `branch_id`         BIGINT       NOT NULL,
   `xid`               VARCHAR(128) NOT NULL,
   `transaction_id`    BIGINT,
   `resource_group_id` VARCHAR(32),
   `resource_id`       VARCHAR(256),
   `branch_type`       VARCHAR(8),
   `status`            TINYINT,
   `client_id`         VARCHAR(64),
   `application_data`  VARCHAR(2000),
   `gmt_create`        DATETIME(6),
   `gmt_modified`      DATETIME(6),
   PRIMARY KEY (`branch_id`),
   KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
 DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
   `row_key`        VARCHAR(128) NOT NULL,
   `xid`            VARCHAR(96),
   `transaction_id` BIGINT,
   `branch_id`      BIGINT       NOT NULL,
   `resource_id`    VARCHAR(256),
   `table_name`     VARCHAR(32),
   `pk`             VARCHAR(36),
   `gmt_create`     DATETIME,
   `gmt_modified`   DATETIME,
   PRIMARY KEY (`row_key`),
   KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
 DEFAULT CHARSET = utf8;


4、推送配置到nacos

由于上面已经在register.conf中的config节点中,指定了seata的配置中心为nacos,所以需要将本配置信息一次推送到nacos上去。

1、在seata的源代码中找到config.txt

、修改配置信息如下

仅需要注意或是修改被中文标识的部分,在运行时,请删除#部分的说明。

3、将配置信息推送到nacos配置中心

在windows上,建议使用git bash命令行,执行seata源代码中的nacos-config.sh

查看nacos-config.sh包含以下运行参数:

USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password]

或输入-?查看参数:

现在我们将配置信息推送到nacos上去,注意,请确保nacos已经启动:

$ ./nacos-config.sh -h 192.168.56.10 -p 8848 -u nacos -w 123456
....
Set client.log.exceptionRate=100 successfully
Set transport.serialization=seata successfully
Set transport.compressor=none successfully
Set metrics.enabled=false successfully
Set metrics.registryType=compact successfully
Set metrics.exporterList=prometheus successfully
Set metrics.exporterPrometheusPort=9898 successfully
=========================================================================
Complete initialization parameters, total-count:79 , failure-count:0
=========================================================================
Init nacos config finished, please start seata-server.

上面都显示successfully,则表示成功,

4、查看nacos上的配置

查看nacos上的配置已经多了很多SEATA_GROUP的配置信息:

请主要关注:

service.vgroupMapping.my_test_tx_group 此配置中的my_test_tx_group将会在项目中做同样的配置,如果不匹配分布式事务将不会成功。

service.default.grouplist的配置必须是seata服务器所在的地址,如本处的配置为:192.168.56.10:8091。建议不要是127.0.0.1:8091.

5、启动seata

配置好seata后,就可以启动seata了,直接进入seata/bin目录下,执行:


# bin/seata-server.sh

Server started, listen port: 8091

启动后,查看nacos注册中心,应该已经有了seata的注册信息:

5、创建springboot项目

1、创建springboot项目

添加:nacos依赖(可在项目创建时直接添加),添加seata依赖(需要独立添加)。

添加seata依赖,完整的pom.xml如下:

    <properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<!-- seata依赖 -->
       <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
<!-- Seata依赖完成 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2、配置bootstrap.yml

在bootstrap.yml中主要配置

nacos注册中心

      nacos配置中心

      seata事务中心

完整配置如下:

server:
port: 7801
spring:
application:
  name: service-a
profiles:
  active:
  - prod
cloud:
 #指定此项目使用nacos注册中心和配置中心
  nacos:
    server-addr: 192.168.56.10:8848
    username: nacos
    password: 123456
    discovery:
      ip: 192.168.56.1
      port: ${server.port}
    config:
      server-addr: ${spring.cloud.nacos.server-addr}
      username: ${spring.cloud.nacos.username}
      password: ${spring.cloud.nacos.password}
      group: DEFAULT_GROUP
      refresh-enabled: true
      file-extension: properties
      prefix: ${spring.application.name}
    #配置数据源
datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://localhost:3306/demo?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  username: root
  password: 123456
  hikari:
    maximum-pool-size: 3
  initialization-mode: never
# https://www.jianshu.com/p/a572045f28a5
seata:
enabled: true
application-id: ${spring.application.name}
 #注意:这儿指琮的服务名称必须与之前在config.txt中配置的名称相同
tx-service-group: my_test_tx_group
 #自动代理DataSource,之前需还需要开发一个代理类,目前已经不再需要了
enable-auto-data-source-proxy: true
 #以下指定使用nacos的配置中心
config:
  type: nacos
  nacos:
    server-addr: 192.168.56.10:8848
    namespace:
    username: nacos
    password: 123456
#配置日志,可选
logging:
level:
  root: info
file:

  name: ./log/${spring.application.name}.log


3、创建 undo_sql

   每一个被操作的数据库中,都必须要添加一个undo_sql表,此表创建语句,可以在script/clent中找到:

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
   `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
   `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
   `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
   `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
   `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
   `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
   `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
   UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
 AUTO_INCREMENT = 1
 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

如上,项目连接的数据库为demo,则需要在demo数据库,创建undo_log表。

4、开发测试全局事务

  根据上面的过程,我们再创建一个项目,除连接不同的数据库之外,其他的配置完全一样。模拟从一个微服务调用另一个微服务,看是否会因为被调用的微服务抛出异常,两个微服务的事务都会回滚:


以下调用从service-b调用service-a:

service-b的代码:

service-b的serice代码,注意在save方法中,调用了另一个微服务:

package cn.seata.api.demo.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import cn.seata.api.demo.dao.DemoDao;
import cn.seata.vo.Response;
import io.seata.spring.annotation.GlobalTransactional;
/**
* @author 王健
* @version 1.0 2020-09-12
*/
@Service
public class DemoService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DemoDao demoDao;
@GlobalTransactional
public List<Map<String, Object>> list(){
return demoDao.list();
}
@GlobalTransactional
public void save(String name,Integer age) {
demoDao.save(name, age);
//调用另一个
String url ="http://localhost:7801/demo/save";
//必须要使用linked..
LinkedMultiValueMap<String,Object> map = new LinkedMultiValueMap<>();
map.add("name", "FromServiceB-"+name);
map.add("age", age+1);
Response res =  restTemplate.postForObject(url, map, Response.class);
System.err.println("res is:"+res);
}
}


service-a的结果类似,只是连接了不同的数据库:

且service-a的dao中故意抛出异常:

package cn.seata.api.demo.dao;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Repository
public class DemoDao {
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
public List<Map<String, Object>> list(){
String sql ="select * from t_stud";
JdbcTemplate jt = new JdbcTemplate(dataSource);
List<Map<String,Object>> list =
jt.queryForList(sql);
return list;
}
public void save(String name,Integer age) {
JdbcTemplate jt = new JdbcTemplate(dataSource);
String sql ="insert into t_stud(name,age) values(?,?)";
int cunt = jt.update(sql,name,age);
log.info("Service-A写入记录:"+cunt);
if(age>60) {
//以下故意抛出异常
throw new RuntimeException(new SQLException("Beger then 60"));
}
}
}

5、调用测试

在启动时,查看seata的后台日志:

成功的情况:

失败的情况

此,seata的分布式事务已经成功。

最后,奔跑在路上的我,这段时间一直在青岛,有来青岛的盆友,欢迎来青岛技术交流。





文章转载自Coding On Road,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论