Seata:一个简单易用的分布式事务解决方案
什么是Seata?
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata的前身是Fescar,是阿里系内部一直扮演着应用架构层数据一致性的中间件角色,帮助经济体平稳的度过历年的双11,对上层业务进行了有力的技术支撑。2019年1月,Seata正式对外开源,未来将以社区共建的形式帮助用户快速落地分布式事务解决方案。
为什么需要Seata?
在微服务架构中,为了提高系统的可用性和可扩展性,通常会将一个大型的单体应用拆分成多个小型的微服务,每个微服务都有自己独立的数据库。这样做的好处是可以实现服务之间的解耦和自治,但也带来了一个新的问题:如何保证跨多个微服务的业务操作具有原子性和一致性?传统的本地事务已经无法满足这种场景,因为本地事务只能保证单个数据库的数据一致性,而不能保证多个数据库之间的数据一致性。这就需要引入分布式事务来解决这个问题。
分布式事务是指跨多个资源管理器(如数据库、消息队列等)执行的事务,它需要保证所有参与者都能达成一致的最终状态。分布式事务需要满足ACID属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。然而,在分布式环境中,要实现ACID属性是非常困难和代价昂贵的,因为需要考虑网络延迟、故障、并发等各种复杂因素。因此,很多分布式事务解决方案都是基于某种妥协或权衡来实现的。
Seata就是一个基于妥协或权衡来实现分布式事务的框架,它提供了多种分布式事务模式来适应不同场景和需求。目前已支持Dubbo、Spring Cloud、Sofa-RPC、Motan 和 gRPC 等RPC框架,其他框架持续集成中。
Seata有哪些特色功能?
Seata目前支持以下四种分布式事务模式:
- AT 模式:提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB。 H2、DB2、SQLServer、达梦开发中。
- TCC 模式:支持 TCC 模式并可与 AT 混用,灵活度更高。
- SAGA 模式:为长事务提供有效的解决方案,提供编排式与注解式 (开发中)。
- XA 模式:支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB。
Seata还具有以下特点:
- 高可用:支持计算分离集群模式,水平扩展能力强的数据库和 Redis 存储模式.Raft模式Preview阶段。
- 易用性:提供了丰富的文档和示例,以及友好的社区支持。
- 生态兼容:与其他阿里系开源项目如Nacos、Sentinel、RocketMQ等有良好的集成和协作。
总结
Seata是一个简单易用的分布式事务解决方案,它可以帮助开发者在微服务架构下实现数据一致性,提高系统的可靠性和稳定性。Seata目前已经有很多知名的企业在使用,如阿里巴巴、蚂蚁金服、美团、京东、滴滴等。
示例
下面是一个使用Seata AT 模式实现分布式事务的简单示例,假设有两个微服务:订单服务和库存服务,订单服务负责创建订单,库存服务负责扣减库存。当用户下单时,需要同时调用两个服务,并保证数据一致性。如果其中一个服务失败了,需要回滚另一个服务的操作。
步骤一:引入Seata依赖
在两个微服务的pom.xml文件中,添加Seata依赖:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
步骤二:配置Seata
在两个微服务的application.properties文件中,添加Seata相关配置:
# Seata 服务端地址
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
# Seata 应用ID
seata.application.id=order-service # 或者 inventory-service
# Seata 事务组ID
seata.tx-service-group=my_test_tx_group
# Seata 数据源代理模式
seata.datasource.proxy-mode=AT
# Seata 数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false # 或者 inventory_db
spring.datasource.username=root
spring.datasource.password=root
步骤三:创建数据库表
在两个微服务对应的数据库中,创建业务表和undo_log表:
-- 订单表
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`),
UNIQUE KEY `unique_user_commodity` (`user_id`,`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- 库存表
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- undo_log 表(两个数据库都需要)
CREATE TABLE `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context`
`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 NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
`ext` VARCHAR(100) DEFAULT NULL COMMENT 'reserved field',
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
步骤四:编写业务代码
在两个微服务中,分别编写订单服务和库存服务的业务代码,使用@GlobalTransactional注解标注分布式事务的入口方法,使用@DataSourceProxy注解标注数据源代理对象,使用JdbcTemplate或者Mybatis等方式操作数据库:
// 订单服务
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private InventoryService inventoryService;
@GlobalTransactional // 分布式事务入口
public void createOrder(String userId, String commodityCode, int orderCount) {
// 扣减库存
inventoryService.deduct(commodityCode, orderCount);
// 创建订单
int orderMoney = calculate(commodityCode, orderCount);
jdbcTemplate.update("insert into order_tbl(user_id, commodity_code, count, money) values (?, ?, ?, ?)",
new Object[]{userId, commodityCode, orderCount, orderMoney});
// 模拟异常,测试回滚
if (orderCount == 2) {
throw new RuntimeException("create order failed");
}
}
private int calculate(String commodityCode, int orderCount) {
return 100 * orderCount;
}
}
// 库存服务
@Service
public class InventoryService {
@Autowired
private JdbcTemplate jdbcTemplate;
@DataSourceProxy // 数据源代理
public void deduct(String commodityCode, int count) {
jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
new Object[]{count, commodityCode});
}
}
步骤五:启动Seata服务端和客户端
下载Seata服务端的压缩包,解压后修改conf目录下的file.conf和registry.conf文件,配置好数据库和注册中心(如Nacos)等信息,然后运行bin目录下的seata-server.bat或者seata-server.sh文件,启动Seata服务端。
在两个微服务的项目目录下,运行mvn spring-boot:run命令,启动Seata客户端。
步骤六:测试分布式事务
使用Postman或者curl等工具,向订单服务发送请求,创建订单:
curl -X POST http://localhost:8081/order/create?userId=U100001&commodityCode=C00321&orderCount=2
观察控制台输出和数据库变化,可以发现当orderCount为2时,会触发异常,并回滚订单服务和库存服务的操作,保证数据一致性。当orderCount为其他值时,会正常执行,并提交订单服务和库存服务的操作。