摘要


Seata是Alibaba开源的一款分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,本文将通过一个简单的下单业务场景来对其用法进行详细介绍。

什么是分布式事务问题?


单体应用

单体应用中,一个业务操作需要调用三个模块完成,此时数据的一致性由本地事务来保证。

微服务应用

随着业务需求的变化,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

小结

在微服务架构中由于全局数据一致性没法保证产生的问题就是分布式事务问题。简单来说,一次业务操作需要操作多个数据源或需要进行远程调用,就会产生分布式事务问题。

Seata简介


定义一个分布式事务

我们可以把一个分布式事务理解成一个包含了若干分支事务的全局事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足ACID的本地事务。这是我们对分布式事务结构的基本认识,与 XA 是一致的

协议分布式事务处理过程的三个组件
  • Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
  • Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个典型的分布式事务过程
  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
  • XID 在微服务调用链路的上下文中传播;
  • RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  • TM 向 TC 发起针对 XID 的全局提交或回滚决议;
  • TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

seata-server的安装与配置


  • 我们先从官网下载seata-server,这里下载的是seata-server-0.9.0.zip,下载地址:seata-server

  • 这里我们使用Nacos作为注册中心,Nacos的安装及使用可以参考:Spring Cloud Alibaba:Nacos 作为注册中心和配置中心使用;

  • 解压seata-server安装包到指定目录,修改conf目录下的file.conf配置文件,主要修改自定义事务组名称,事务日志存储模式为db及数据库连接信息;

service {#vgroup->rgroupvgroup_mapping.fsp_tx_group = "default" #修改事务组名称为:fsp_tx_group,和客户端自定义的名称对应#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"
}## transaction log store
store {## store mode: file、dbmode = "db" #修改此处将事务信息存储到数据库中## database storedb {## 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://localhost:3306/seat-server" #修改数据库连接地址user = "root" #修改数据库用户名password = "root" #修改数据库密码min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
  • 由于我们使用了db模式存储事务日志,所以我们需要创建一个seat-server数据库,建表sql在seata-server的/conf/db_store.sql中;
  • 修改conf目录下的registry.conf配置文件,指明注册中心为nacos,及修改nacos连接信息即可;
registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos" #改为nacosnacos {serverAddr = "localhost:8848" #改为nacos的连接地址namespace = ""cluster = "default"}
}
  • 先启动Nacos,再使用seata-server中/bin/seata-server.bat文件启动seata-server。

数据库准备


创建业务数据库
  • seat-order:存储订单的数据库;
  • seat-storage:存储库存的数据库;
  • seat-account:存储账户信息的数据库。
初始化业务表

order表

CREATE TABLE `order` (`id` bigint(11) NOT NULL AUTO_INCREMENT,`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',`count` int(11) DEFAULT NULL COMMENT '数量',`money` decimal(11,0) DEFAULT NULL COMMENT '金额',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;

storage表

CREATE TABLE `storage` (`id` bigint(11) NOT NULL AUTO_INCREMENT,`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',`total` int(11) DEFAULT NULL COMMENT '总库存',`used` int(11) DEFAULT NULL COMMENT '已用库存',`residue` int(11) DEFAULT NULL COMMENT '剩余库存',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO `seat-storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');

account表

CREATE TABLE `account` (`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO `seat-account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
创建日志回滚表

使用Seata还需要在每个数据库中创建日志表,建表sql在seata-server的/conf/db_undo_log.sql中。

完整数据库示意图

制造一个分布式事务问题


这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

客户端配置


  • 对seata-order-service、seata-storage-service和seata-account-service三个seata的客户端进行配置,它们配置大致相同,我们下面以seata-order-service的配置为例;

  • 修改application.yml文件,自定义事务组的名称;

spring:cloud:alibaba:seata:tx-service-group: fsp_tx_group #自定义事务组名称需要与seata-server中的对应
  • 添加并修改file.conf配置文件,主要是修改自定义事务组名称;
service {#vgroup->rgroupvgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"disableGlobalTransaction = false
}
  • 添加并修改registry.conf配置文件,主要是将注册中心改为nacos;
registry {# file 、nacos 、eureka、redis、zktype = "nacos" #修改为nacosnacos {serverAddr = "localhost:8848" #修改为nacos的连接地址namespace = ""cluster = "default"}
}
  • 在启动类中取消数据源的自动创建:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderServiceApplication {public static void main(String[] args) {SpringApplication.run(SeataOrderServiceApplication.class, args);}}
  • 创建配置使用Seata对数据源进行代理:
/*** 使用Seata对数据源进行代理* Created by macro on 2019/11/11.*/
@Configuration
public class DataSourceProxyConfig {@Value("${mybatis.mapperLocations}")private String mapperLocations;@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource druidDataSource(){return new DruidDataSource();}@Beanpublic DataSourceProxy dataSourceProxy(DataSource dataSource) {return new DataSourceProxy(dataSource);}@Beanpublic SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSourceProxy);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());return sqlSessionFactoryBean.getObject();}}
  • 使用@GlobalTransactional注解开启分布式事务:
package com.macro.cloud.service.impl;import com.macro.cloud.dao.OrderDao;
import com.macro.cloud.domain.Order;
import com.macro.cloud.service.AccountService;
import com.macro.cloud.service.OrderService;
import com.macro.cloud.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;/*** 订单业务实现类* Created by macro on 2019/11/11.*/
@Service
public class OrderServiceImpl implements OrderService {private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);@Autowiredprivate OrderDao orderDao;@Autowiredprivate StorageService storageService;@Autowiredprivate AccountService accountService;/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态*/@Override@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)public void create(Order order) {LOGGER.info("------->下单开始");//本应用创建订单orderDao.create(order);//远程调用库存服务扣减库存LOGGER.info("------->order-service中扣减库存开始");storageService.decrease(order.getProductId(),order.getCount());LOGGER.info("------->order-service中扣减库存结束:{}",order.getId());//远程调用账户服务扣减余额LOGGER.info("------->order-service中扣减余额开始");accountService.decrease(order.getUserId(),order.getMoney());LOGGER.info("------->order-service中扣减余额结束");//修改订单状态为已完成LOGGER.info("------->order-service中修改订单状态开始");orderDao.update(order.getUserId(),0);LOGGER.info("------->order-service中修改订单状态结束");LOGGER.info("------->下单结束");}
}

分布式事务功能演示


  • 运行seata-order-service、seata-storage-service和seata-account-service三个服务;

  • 数据库初始信息状态:

  • 我们在seata-account-service中制造一个超时异常后,调用下单接口:

/*** 账户业务实现类* Created by macro on 2019/11/11.*/
@Service
public class AccountServiceImpl implements AccountService {private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);@Autowiredprivate AccountDao accountDao;/*** 扣减账户余额*/@Overridepublic void decrease(Long userId, BigDecimal money) {LOGGER.info("------->account-service中扣减账户余额开始");//模拟超时异常,全局事务回滚try {Thread.sleep(30*1000);} catch (InterruptedException e) {e.printStackTrace();}accountDao.decrease(userId,money);LOGGER.info("------->account-service中扣减账户余额结束");}
}
  • 此时我们可以发现下单后数据库数据并没有任何改变;
  • 我们可以在seata-order-service中注释掉@GlobalTransactional来看看没有Seata的分布式事务管理会发生什么情况:
/*** 订单业务实现类* Created by macro on 2019/11/11.*/
@Service
public class OrderServiceImpl implements OrderService {/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态*/@Override
//    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)public void create(Order order) {LOGGER.info("------->下单开始");//省略代码...LOGGER.info("------->下单结束");}
}
  • 由于seata-account-service的超时会导致当库存和账户金额扣减后订单状态并没有设置为已经完成,而且由于远程调用的重试机制,账户余额还会被多次扣减。

参考资料


Seata官方文档:https://github.com/seata/seata/wiki

使用到的模块


springcloud-learning
├── seata-order-service -- 整合了seata的订单服务
├── seata-storage-service -- 整合了seata的库存服务
└── seata-account-service -- 整合了seata的账户服务

使用Seata彻底解决Spring Cloud中的分布式事务问题!相关推荐

  1. seata xid是什么_使用Seata彻底解决Spring Cloud中的分布式事务问题!

    Seata是Alibaba开源的一款分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,本文将通过一个简单的下单业务场景来对其用法进行详细介绍. 什么是分布式事务问题? 单体应用 单体应用 ...

  2. 手写基于Spring Cloud的TCC分布式事务框架

    如何简单实现TCC分布式事务框架 最近听到很多其他公司的小伙伴谈分布式事务的问题,各种业务场景都有,可能就是这两年很多公司都在往微服务发展,现在各个子系统都拆分.建设的差不多了,实现了模块化开发,但是 ...

  3. 解决Spring Cloud中Feign/Ribbon第一次请求失败的方法

    前言 在Spring Cloud中,Feign和Ribbon在整合了Hystrix后,可能会出现首次调用失败的问题,要如何解决该问题呢? 造成该问题的原因 Hystrix默认的超时时间是1秒,如果超过 ...

  4. Spring Cloud中的分布式组件五花八门,我到底该怎么学?

    点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/zwx900102/article/ details/121727985 分布式架构的演进 在软件行业,一个应用服务随着功能越来 ...

  5. spring Cloud中,解决Feign/Ribbon整合Hystrix第一次请求失败的问题?

    Spring Cloud中,Feign和Ribbon在整合了Hystrix后,可能会出现首次调用失败的问题,要如何解决该问题呢? 造成该问题的原因 Hystrix默认的超时时间是1秒,如果超过这个时间 ...

  6. Spring Cloud中Hystrix仪表盘与Turbine集群监控

    Hystrix仪表盘,就像汽车的仪表盘实时显示汽车的各项数据一样,Hystrix仪表盘主要用来监控Hystrix的实时运行状态,通过它我们可以看到Hystrix的各项指标信息,从而快速发现系统中存在的 ...

  7. Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失(续)

    前言 上篇文章<Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失>我们对ThreadLocal数据丢失进行了详细的分析,并通过代码的方式复现了这个问题. ...

  8. Spring Cloud中,Eureka常见问题总结

    Spring Cloud中,Eureka常见问题总结. 1 eureka.environment: 指定环境 参考文档: 1 eureka.datacenter: 指定数据中心 参考文档: 使用配置项 ...

  9. Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失

    在Spring Cloud中我们用Hystrix来实现断路器,Zuul中默认是用信号量(Hystrix默认是线程)来进行隔离的,我们可以通过配置使用线程方式隔离. 在使用线程隔离的时候,有个问题是必须 ...

最新文章

  1. 【冷门实用小工具】JAVA和C#轻量级的UML图绘制工具NClass,UML类图编辑器免安装版【亲测有效】
  2. Android 分享机顶盒项目的封装类《GridView》
  3. 爬取jd商城手机类商品图片
  4. FIELD SYMBOL和TYPE REF TO的用法和比较
  5. img图片下多余空白的BUG解决方案
  6. 音频处理基本概念及音频重采样
  7. Exchange 邮件投递被拒的问题分析
  8. 云计算与分布式系统课程学习笔记(一)——云计算简介
  9. 信息学奥赛一本通(1132:石头剪子布)
  10. devops .net_DevOps vs. Agile:它们有什么共同点吗?
  11. 程序员应具备的素质[转帖]
  12. 哄女票超简单程序代码(日常小惊喜)
  13. VS2012(Visual Studio 2012)官方免费中文旗舰版下载(含激活密钥)
  14. 联想lenovo G40-70M 无线网卡白名单跳过
  15. 华为B610-4E光猫固件Shell补全及升级最新的050固件
  16. VSCode C++ control reaches end of non-void function [-Wreturn-type]
  17. About Endian
  18. mac下安装和使用brew
  19. 获取新浪开放平台的Access token
  20. 全球与中国无线门铃对讲设备市场深度研究分析报告

热门文章

  1. Maven命令行使用:mvn clean package(打包)
  2. 当singleton Bean依赖propotype Bean,可以使用在配置Bean添加look-method来解决
  3. Android 不同View ID相同
  4. iframe 的一点经历
  5. pq 中m函数判断嵌套_Python中numpy的布尔判断、切片、维度变化、合并、通用函数...
  6. 直线电机原理动画_每周一品 · 直线电机(Linear Motor)中的磁性材料
  7. java chars_Java getChars() 方法 - Java 基础教程
  8. java删除不,Java文件不能删除,该怎么解决
  9. java swing form_在java swing中创建表单最简单的方法是什么?
  10. java 计算器 junit测试_Java中Junit4测试实例