引言
Seata 的前身 Fescar 刚开源的时候,就看过相关的文章和代码,代码写得很好,我还在另一个自己的项目中,借鉴了它的很多设计风格。最近想总结一篇关于分布式事务的文章,所以就想以 Seata 为中心,围绕它来细述分布式事务的点点滴滴。本文作为该系列文章的开篇,先简单地介绍一下 Seata 的背景和使用方式,其他 Seata 相关文章均收录于 <Seata系列文章>中。
背景
互联网系统最初的设计一般都是单库单表,但随着业务数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。所以,在这个阶段一般都会对数据库进行水平拆分,将原单库单表拆分成多个数据库分片。
如下图所示,分库分表之后,原来在一个数据库上就能完成的写操作,可能就会跨多个数据库,这就产生了跨数据库事务问题。
此外,在系统设计初期,一般都是将所有业务放在一个服务中,但是随着业务的快速发展,系统的访问量和业务复杂程度都在快速增长,这种单系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合、可伸缩问题的需求越来越强烈。
如上图所示,本着面向服务(SOA)的设计原则,会将单系统拆分成多个业务系统,这降低了各个系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于各个子业务的发展和子系统容量的伸缩。但是,业务系统按照服务拆分之后,一个完整的业务往往需要调用多个服务,如何保证多个服务间的数据一致性成为一个难题。
为了应对这种需求,在数据库领域发展出了一个 XA 协议,在数据库层面基于 2PC 来实现分布式事务,但是并不是所有的数据库都支持该协议,如果有任意一个子系统使用的数据库不支持 XA 协议的话,就无法保证整个业务流程的一致性。此外,XA 的执行效率也很差,而且因为它处于数据库实现领域,所以数据库的使用者对此毫无办法,当然您也可以直接定制化数据库实现,但是成本很大。
为了解决上述问题,专注于分布式事务的应用层中间件就应运而生,它们有的是 2PC 型无侵入方案,有的是 TCC 方案。而我们今天介绍的主角 Seata,将各种方案都整合到了一起,并针对性能进行了很多优化,可谓是集大成者。接下来就让我们一起看一看 Seata 带给我们的便利,以及 Seata 的实现原理。
试玩
前面提到了 Seata 会给开发带来极大的便利,这里就以官方Demo为例,先给大家演示一下如何使用 Seata。这里我使用了官方 Demo 中的 springboot-dubbo-seata
,因为它涉及到前面提到的多服务场景,每个服务操作各自的 DB(名义上),使用 dubbo 进行 RPC,从而完成整个业务流程。
我的试玩流程:
- 下载代码
- 导入 IntelliJ IDEA,并下载 maven 依赖
- 下载
1.1.0
版本的 nacos,后面会用它进行 RPC 的服务发现 - 下载
0.8.0
版本的 seata-server,这里它扮演分布式事务协调者 - 在本地的 MySQL 服务中创建
seata
数据库,并将springboot-dubbo-seata
中的样例 SQL 导入到seata
数据库,完成后可以发现数据库中有t_account
,t_order
,t_storage
,undo_log
这四个表,并且t_account
中有一条用户数据,t_storage
中有一条商品信息。 - 然后分别启动 3 个子服务
samples-account
、samples-order
、samples-storage
,最后启动业务入口服务samples-business
- 通过
curl
调用业务入口服务
⚠️注意:我当时测试时,官方 Guide 中使用的 curl 命令有一些问题,其中使用的商品码有误,这里需要将其改为 DB 中保存的商品码
C201901140001
,我已经给官方仓库提了
PR,但是可能还未合并。总之,最终完整的 curl 命令如下:
curl -H "Content-Type:application/json" -X POST -d '{"userId":"1","commodityCode":"C201901140001","name":"风扇","count":2,"amount":"100"}' localhost:8104/business/dubbo/buy
事务提交
当业务流程成功完成时,请求结果是成功。查看 DB 你会发现 t_account
中用户的余额减少了:4000 -> 3900,t_storage
中商品的数量减少了: 1000 -> 998,t_order
中新增了一条订单。
事务回滚
接下来我们测试一下事务回滚的效果,首先打开回滚异常。
然后重启入口服务 samples-business
,并重新发送请求,请求结果变成了失败,此时查看 DB 你会发现它没有任何变化。
我们回过头来看看业务入口的代码,你会发现这一切特性的接入对开发者来说,只是将全局事务注解 GlobalTransactional
标在入口函数上,然后在所有服务中引入 Seata,并进行正确的配置,剩下的脏活累活 Seata 就自动帮我们完成了, 是不是感觉很神奇?
/**
* 处理业务逻辑
*/
@Override
// 全局事务注解
@GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
System.out.println("开始全局事务,XID = " + RootContext.getXID());
ObjectResponse<Object> objectResponse = new ObjectResponse<>();
//1、扣减库存
CommodityDTO commodityDTO = new CommodityDTO();
commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
commodityDTO.setCount(businessDTO.getCount());
ObjectResponse storageResponse = storageDubboService.decreaseStorage(commodityDTO);
//2、创建订单
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId(businessDTO.getUserId());
orderDTO.setCommodityCode(businessDTO.getCommodityCode());
orderDTO.setOrderCount(businessDTO.getCount());
orderDTO.setOrderAmount(businessDTO.getAmount());
ObjectResponse<OrderDTO> response = orderDubboService.createOrder(orderDTO);
//打开注释测试事务发生异常后,全局回滚功能
if (!flag) {
throw new RuntimeException("测试抛异常后,分布式事务回滚!");
}
if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
throw new DefaultException(RspStatusEnum.FAIL);
}
objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
objectResponse.setData(response.getData());
return objectResponse;
}
从业务服务的实现代码中可以看到,我们先调用了库存服务减少库存,然后调用订单服务创建订单,在订单服务里面,涉及到了减少用户余额(代码就不展示了),当所有的这一切都完成时,业务流程中抛出一个运行时异常,致使整个业务链路中的所有 DB 事务都回滚了。
至此,系统垂直扩展引入的分布式事务问题就解决了。
好了,实验课结束了,该学一下理论知识了。
文章说明
更多有价值的文章均收录于贝贝猫的文章目录
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。
参考内容
[1] fescar锁设计和隔离级别的理解
[2] 分布式事务中间件 Fescar - RM 模块源码解读
[3] Fescar分布式事务实现原理解析探秘
[4] Seata TCC 分布式事务源码分析
[5] 深度剖析一站式分布式事务方案 Seata-Server
[6] 分布式事务 Seata Saga 模式首秀以及三种模式详解
[7] 蚂蚁金服大规模分布式事务实践和开源详解
[8] 分布式事务 Seata TCC 模式深度解析
[9] Fescar (Seata)0.4.0 中文文档教程
[10] Seata Github Wiki
[11] 深度剖析一站式分布式事务方案Seata(Fescar)-Server