seata-快速使用

seata-快速使用

什么是分布式事务?

随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用:
传统单体Web应用


传统单体应用

拆分后的架构


微服务架构

分布式系统会把一个应用系统拆分为可独立部署的多个服务(通常一个服务对应着一个DB),因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络协作完成事务称为分布式事务。例如,商品添加,加库存,创建订单减库存等都是分布式事务。

分布式事务产生的场景

  1. 比较典型的场景就是微服务架构,微服务之间通过远程调用完成事务操作。上边的例子是订单服务和库存服务,再比如商品服务和库存服务。也同样会出现分布式事务问题。创建商品的同时,需要请求库存服务增加商品。简而言之就是:跨JVM进程产生分布式事务。
微服务架构
  1. 单体系统访问多个数据库实例,当单体系统需要访问多个数据库实例时就会产生分布式事务。比如用户管理系统,用户信息和订单信息分别在两个MySQL实例中存储,用户关系系统删除用户信息,需要同时删除用户个人信息以及订单信息。由于数据库在不同的数据库实例,需要通过不同的数据库连接去操作数据,此时产生分布式事务。简而言之就是:跨数据库实例产生分布式事务。


    image.png
  1. 多服务访问同一数据库实例,比如商品微服务和库存微服务即使访问同一个数据库实例也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库连接进行数据库操作,此时产生分布式事务。
image.png

分布式事务解决方案

  1. 刚性事务:
    标准分布式事务(2pc/3pc)

  2. 柔性事务:
    可靠消息最终一致性
    TCC
    最大努力通知
    纯补偿性

本篇着重介绍标准分布式事务(2pc)解决方案,seata。
在开始介绍之前,先来看一个例子。

案例

案例环境

我在本地创建了3个项目,分别是product商品服务, stock库存服务,admin统一的对外服务。每个服务单独对应一个数据库。在admin中进行调用product添加商品,然后调用库存服务进行添加库存。
三个服务分别对应着不同的数据库,由于是演示出分布式事务出现的问题,所以表结构相对简单。
pm_product表

CREATE TABLE `pm_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `prod_name` varchar(255) NOT NULL COMMENT '商品名称',
  `model` varchar(255) NOT NULL COMMENT '商品型号',
  `price` decimal(20,10) NOT NULL '商品价格',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

pm_stock表

CREATE TABLE `pm_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `prod_id` bigint(20) NOT NULL COMMENT '商品ID',
  `quantity` int(11) NOT NULL '商品数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;

admin服务中核心调用逻辑如下:

public ServerResponse saveProduct(ProductAddReq productAddReq) {
        ProductReq productReq = new ProductReq();
        productReq.setModel(productAddReq.getModel());
        productReq.setPrice(productAddReq.getPrice());
        productReq.setProdName(productAddReq.getProdName());
        // 添加商品 调用商品服务
        ServerResponse<Long> serverResponse = productFeign.saveProduct(productReq);
        if (!serverResponse.isSuccess() || serverResponse.getData() == null) {
            return serverResponse;
        }
        // 添加库存 调用库存服务
        StockReq stockReq = new StockReq();
        stockReq.setProdId(serverResponse.getData());
        stockReq.setQuantity(productAddReq.getQuantity());

        ServerResponse stockResponse = stockFeign.saveStock(stockReq);
        if (!stockResponse.isSuccess()) {
            return stockResponse;
        }
        return ServerResponse.ok("商品添加成功");
}

然后启动项目进行运行,发现一切正常,表中对应的数据也都正常。
stock库中和product库中数据一致。

问题引出

然后我们开始制造一些意外,首先把表中数据情况,便于对比数据。比如此时我把stock服务关闭,然后调用admin中的接口进行添加商品,看看会发生什么情况。

POST http://localhost:8080/product
Content-Type: application/json

{
  "prodName": "商品",
  "price": 23,
  "model": "",
  "quantity": 100
}

很显然报错了,返回前端错误,但是此时数据库里面已经产生了脏数据。


报错

pm_product表


pm_product

pm_stock表


pm_stock

可以发现pm_product表中插入了一条数据,而pm_stock表中没有数据,此时明显是不正确的,pm_product表中数据属于脏数据。
产生这种情况的现象,还有很多种,例如,库存服务中有个异常,然后导致库存服务本地事务回滚,但是商品服务已经插进去了。还有就是admin服务中在调用完商品服务之后报异常,会导致报错,商品服务插入数据成功,但是库存服务没有被调用,也会产生脏数据。

解决方案

那么该如何解决这种情况呢?
解决这种情况的方案有很多种,这里只介绍基于两阶段提交的seata方案。
seata是由阿里中间件团队发起的开源项目Fescar,后更名为seata,它是一个开源的分布式事务框架、传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件,主要的优点是性能较好,且不长时间占用资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题。它的宗旨是像解决本地事务一样,来解决分布式事务。好,现在来看一下如何来快速使用seata。关于原理性的东西这里先不做过多介绍,先着重看一下如何将seata引入到项目中来。

seata方案

1.引入pom依赖。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

2.添加一个配置类

@Configuration
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
    @Primary
    @Bean("dataSourceProxy")
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
}

3.配置seata-server。将上述配置加入到每个工程中。admin服务可以不用加,只加在需要操作DB的服务中。好,下面还需要一个配置一下seata-server。可以把它当成一个全局事务的协调器。具体的下载地址如下:

https://seata.io/zh-cn/blog/download.html

下下来之后,其目录结构如下:


image.png

现在来简单说明一下每个目录下的文件,bin目录下主要是启动脚本,windows用户启动.bat文件,Linux和Mac用户启动.sh文件。conf目录主要是一些配置文件。

1. file.conf 主要是一些服务端和客户端的一些配置。
2. registry.conf 主要是seata-server的注册方式。它可以注册到
redis,zk,eureka中,来对它进行管理。

本案例主要使用eureka来进行注册。然后对这俩文件进行更改。
在registry.conf中仅需更改eureka地址,然后把应用名称也设置一下。

eureka {
    serviceUrl = "http://127.0.0.1:8761/eureka"
    application = "seata-server"
    weight = "1"
}

之后更改file文件,首先更改store部分,这是seata-server在运行过程中,需要将运行时的一些临时数据存储的地方。这里我将它们存储在DB中,需要配置一下地址等。

store {
  ## store mode: file、db 存储模式,选择DB
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
  }

  ## database store property 数据库的一些配置
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "chusen"
  }
}

4.建库建表。然后需要在数据库中创建一个seata库,之后创建一些seata-server运行时需要的一些表。

CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(128) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `lock_key` varchar(256) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(64) DEFAULT NULL,
  `transaction_service_group` varchar(64) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,
  `xid` varchar(128) DEFAULT NULL,
  `transaction_id` mediumtext,
  `branch_id` mediumtext,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(128) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

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 DEFAULT CHARSET=utf8;

数据库建表到此为止。
5.下面再来简单配置一下每个服务。在每个服务的spring配置文件中加入一行配置

spring.cloud.alibaba.seata.tx-service-group = tx_group

然后进行更改seata-server中的file配置文件

service {
  #transaction service group mapping
  ## 这里tx_group对应上面配置的tx——group。seata-server对应上面eureka中注册的应用名称
  vgroup_mapping.tx_group = "seata-server"
  #only support when registry.type=file, please don't set multiple addresses
  seata-server.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

最后将seata-server中conf下更改过的file.conf和registry.conf这两个文件复制到每个服务的resources目录下面。大功告成。首先启动seata-server。
然后分别启动每个服务。启动之后,可以在seata-server控制台打印的日志可以看到。以admin服务为例。


image.png

然后重新访问下接口,发现一切正常,之后将stock服务关掉,看pm_product中是否还会产生脏数据。结果大失所望,不生效,这是为什么呢?
因为还差很关键的一步,因为你要让seata知道,要为哪个方法添加全局事务。很简单,在刚才的方法上面添加一个注解即可。这体现了seata对业务代码0侵入,真的像使用本地事务一样。

@GlobalTransactional
public ServerResponse saveProduct(ProductAddReq productAddReq) {
        ProductReq productReq = new ProductReq();
        productReq.setModel(productAddReq.getModel());
        productReq.setPrice(productAddReq.getPrice());
        productReq.setProdName(productAddReq.getProdName());
        // 添加商品
        ServerResponse<Long> serverResponse = productFeign.saveProduct(productReq);
        if (!serverResponse.isSuccess() || serverResponse.getData() == null) {
            return serverResponse;
        }
        // 添加库存
        StockReq stockReq = new StockReq();
        stockReq.setProdId(serverResponse.getData());
        stockReq.setQuantity(productAddReq.getQuantity());

        ServerResponse stockResponse = stockFeign.saveStock(stockReq);
        if (!stockResponse.isSuccess()) {
            return stockResponse;
        }
        return ServerResponse.ok("商品添加成功");
}

然后进行测试,大功告成!当运行时,出现异常时,分支事务会被回滚。可以在seata-server的控制台看到相关日志。


image.png

当没有异常时,可以看到全局事务会被提交。


image.png

小结

1.在使用微服务架构时,既带来了很多优势,同时也带来了很多新的挑战,这其中就包括分布式事务问题。
2.解决分布式事务问题,有很多种解决方案,本篇着重介绍了基于2pc提交的seata方案的使用。
3.需要注意的是,每个服务下面的配置文件需要和seata-server中的配置文件一致。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容