此文章主要讲解 springcloud 中使用 Seata 处理分布式事务相关知识。
Seata
微服务模块,连接多个数据库,多个数据源,而数据库之间的数据一致性需要被保证。
Seata 概述
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata
是 Simple Extensible Autonomous Transaction Architecture
的简写,由 feascar
改名而来。
Seata 是阿里开源的分布式事务框架,属于二阶段提交模式。
目前 github 上已经有 12267
颗星了,也很活跃,最新的提交时间很多都是几天前。
首先我们回顾一下在单体应用中,例如一个业务调用了 3 个模块,他们都使用同一个数据源,是靠本地事务来保证事务一致性。
但在微服务架构中,这 3 个模块会变为 3 个独立的微服务,各自有自己的数据源,调用逻辑就变为:
Seata 如何处理呢?
Business 是业务入口,在程序中会通过注解
来说明他是一个全局事务
,这时他的角色为 TM(事务管理者)
。
Business 会请求 TC(事务协调器,一个独立运行的服务),说明自己要开启一个全局事务,TC 会生成一个全局事务 ID(XID),并返回给 Business。
Business 得到 XID 后,开始调用微服务,例如调用 Storage。
Storage 会收到 XID,知道自己的事务属于这个全局事务。Storage 执行自己的业务逻辑,操作本地数据库。
Storage 会把自己的事务注册到 TC,作为这个 XID 下面的一个分支事务
,并且把自己的事务执行结果也告诉 TC。
此时 Storage 的角色是 RM(资源管理者),资源是指本地数据库。
Order、Account 的执行逻辑与 Storage 一致。
在各个微服务都执行完成后,TC 可以知道 XID 下各个分支事务的执行结果,TM(Business) 也就知道了。
Business 如果发现各个微服务的本地事务都执行成功了,就请求 TC 对这个 XID 提交,否则回滚。
TC 收到请求后,向 XID 下的所有分支事务发起相应请求。
各个微服务收到 TC 的请求后,执行相应指令,并把执行结果上报 TC。
重要机制
(1)全局事务的回滚是如何实现的呢?
Seata 有一个重要的机制:回滚日志
。
每个分支事务对应的数据库中都需要有一个回滚日志表 UNDO_LOG
,在真正修改数据库记录之前,都会先记录修改前的记录值,以便之后回滚。
在收到回滚请求后,就会根据 UNDO_LOG
生成回滚操作的 SQL 语句来执行。
如果收到的是提交请求,就把 UNDO_LOG
中的相应记录删除掉。
(2)RM 是怎么自动和 TC 交互的?
是通过监控拦截JDBC
实现的,例如监控到开启本地事务了,就会自动向 TC 注册、生成回滚日志、向 TC 汇报执行结果。
(3)二阶段回滚失败怎么办?
例如 TC 命令各个 RM 回滚的时候,有一个微服务挂掉了,那么所有正常的微服务也都不会执行回滚,当这个微服务重新正常运行后,TC 会重新执行全局回滚。
分布式事务产生背景
分布式前
- 单机单库没有问题
- 从单机单库-> 一对多个库-> 多服务对多个库(分布式微服务)
分布式之后
单体应用被拆分成微服务应用,例如原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证
,但是全局的数据一致性问题没法保证
。
数据库拆分
- 单库单表支撑不了业务时需要对数据库进行水平拆分。分库分表后,原来在一个数据库上就能完成的写操作,可能会跨多个数据库,就产生了跨数据库事务问题
业务服务化拆分
- 业务拆分后,一个完整的业务逻辑可能会涉及多个服务,多个服务之间存在跨服务事务问题
用一句话来说:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
核心组件
一个典型的分布式事务过程:
分布式事务处理过程的-ID + 三组件模型
1 | Transaction ID XID:# 全局唯一的事务ID |
具体工作过程
再从宏观上梳理一下 Seata 的工作过程:
!
- TM 请求 TC,开始一个新的全局事务,TC 会为这个全局事务生成一个 XID。
- XID 通过微服务的调用链传递到其他微服务。
- RM 把本地事务作为这个 XID 的分支事务注册到 TC。
- TM 请求 TC 对这个 XID 进行提交或回滚。
- TC 指挥这个 XID 下面的所有分支事务进行提交、回滚。
下载安装
下载
下载地址 : 点我去下载
发布说明: 点我跳转
本地事务: @Transactional
全局事务: @GlobalTransactional
,我们只需要使用这个注解在业务方法上即可。
安装
我这里用的 0.9.0 版本。
修改 file.conf
修改 conf/file.conf
文件,修改之前先备份
。
主要修改:自定义事务组名称 + 事务日志存储模式为 db + 数据库连接信息
新建数据库
名字和 file.conf 指定一致,比如我的是 seata
新建数据表
在新建的数据库 seata 里面创建数据表,db_store.sql 脚本文件在 conf
目录下
修改 registry.conf
养成好习惯,修改前备份。
修改 conf/registry.conf
文件内容:
启动服务
先启动 nacos Server 服务,再启动 seata Server。启动文件都在对应的bin
目录下。
案例
数据库准备
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单—–> 扣库存—–>减账户(余额)
创建 3 个数据库
1 | create database seata_order |
seata_order 下建 t_order 表
1 | DROP TABLE IF EXISTS `t_order`; |
seata_storage 下建 t_storage 表
1 | DROP TABLE IF EXISTS `t_storage`; |
seata_account 下建 t_account 表
1 | DROP TABLE IF EXISTS `t_account`; |
3 库分别建对应的回滚日志表
三个数据库都创建一个回滚日志表,seata/conf/
有相应的脚本文件 db_undo_log.sql
编写微服务
业务需求: 下订单-> 减库存 -> 扣余额 -> 改(订单)状态
订单模块
新建模块
seata-order-service2001
POM 文件
1 |
|
YML 文件
1 | server: |
拷贝文件
将 seata/conf/
下的 file.conf
和 registry.conf
两个文件拷贝到 resources
目录下,做一点修改。
domain 实体类
Order
1 |
|
CommonResult
1 | /** |
Dao 接口
1 |
|
Mapper 文件
在 resources 下新建 mapper 目录,新建 mapper 文件
1 |
|
Service 接口
创建 OrderService 、StorageService、AccountService 接口
注意,红框标记的是通过 open-feign 远程调用微服务的 service
OrderService
1 | public interface OrderService { |
StorageService
1 | "seata-storage-service") (value = |
AccountService
1 | "seata-account-service") (value = |
Service 接口实现类
1 |
|
Controller
1 |
|
配置类
MybatisConfig
1 |
|
DataSourceProxyConfig
1 | package com.itjing.springcloud.config; |
主启动类
1 | //这里必须排除数据源自动配置,因为写了配置类,让 seata 管理数据源 |
库存模块
新建模块
seata-storage-service2002
POM 文件
1 |
|
YML 文件
1 | server: |
拷贝文件
同订单模块。
domain 实体类
Storage
1 |
|
CommonResult,同订单模块
Dao 接口
1 |
|
Mapper 文件
1 |
|
Service 接口
1 | public interface StorageService { |
Service 接口实现类
1 | package com.itjing.springcloud.service.impl; |
Controller
1 |
|
配置类
同订单模块。
主启动类
1 | //这里必须排除数据源自动配置,因为写了配置类,让 seata 管理数据源 |
账户模块
新建模块
seata-account-service2003
POM 文件
同库存模块。
YML 文件
1 | server: |
拷贝文件
同库存模块。
domain 实体类
Account
1 |
|
CommonResult,同库存模块。
Dao 接口
1 |
|
Mapper 文件
1 |
|
Service 接口
1 | public interface AccountService { |
1 | package com.itjing.springcloud.service.impl; |
Controller
1 |
|
配置类
同库存模块。
主启动类
1 | //这里必须排除数据源自动配置,因为写了配置类,让 seata 管理数据源 |
测试
先启动 nacos,再启动 seata,然后启动上面的 3 个微服务。
我这里直接用 GET 请求测试了,访问: http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
模拟超时异常
没加 @GlobalTransactional
注解,模拟账户超时异常
1 | /** |
可以发现:
订单成功创建,但是没有成功支付。
但是库存却被扣掉了!
账户也被扣钱了。
而且由于 feign 的超时重试机制,账户余额还有可能被多次扣减,这太可怕了!
加 @GlobalTransactional 注解
还是模拟账户超时异常,这次在 订单业务实现类 OrderServiceImpl
加上 @GlobalTransactional
注解
1 |
|
再次访问,发现页面出现异常,数据库中的数据没有改变,记录都添加不进来,测试成功。
发布时间: 2021-01-23
最后更新: 2024-06-24
本文标题: SpringCloud Alibaba入门到精通(十六)- Seata处理分布式事务
本文链接: https://blog-yilia.xiaojingge.com/posts/cbafe4d9.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!
