分布式事务 - Seata
案例: 微服务下单业务,下单时会调用订单服务,创建订单并写入数据库。然后订单服务调用账户服务和库存服务。
不同的业务,不同的微服务有各自的数据库,各个事务之间无法感知互相的状态。
假如库存不够,调用库存服务失败,这个库存服务报错不会影响账户成功扣款。
理论基础
CAP定理
分布式系统有三个指标
- Consistency 一致性
- 用户访问分布式系统中的任意节点,得到的数据必须一致
- Availability 可用性
- 用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
- Partition tolerance 分区容错性
- Partition 分区: 因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区。
- Tolerance 容错: 在集群出现分区时,整个系统也要持续对外提供服务
- 这时要保持一致性只能强行等待网络恢复后同步数据
- 如果保证可用性就只能牺牲一致性
分布式系统无法同时满足这三个指标
BASE理论
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State (软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent (最终一致性):虽然无法保证强一致性。但是在软状态结束后,最终达到数据一致。
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP理论和BASE理论:
- AP模式: 各子事务分别执行和提交,允许出现结果不一致,然后采取措弥补措施恢复数据即可,实现最终一致。
- CP模式: 各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
分布式事务模型
解决分布式事务,各个子系统之间必须能感知到彼此的事务状态,才能保持一致。
因此需要一个事务协调者来协调每一个事务的参与者
Seata的架构
Seata事务管理中有三个重要的角色
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务,提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
Seata提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分布式事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致性的分布式事务模式,有业务侵入
- AT模式:最终一致性的分布式事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
XA: 指的是已实现XA接口的数据库的XA模式
AT: Automatic Transaction
TCC: Try Confirm Cancel
XA模式原理
XA协议是X/Open组织定义的分布式事务处理(DTP, Distributed Transaction Processing)标准,XA协议描述了全局的TM与局部的RM之间的接口,几乎所有的数据库厂商都对XA协议提供了支持。(基于数据库本身的特性来实现分布式事务)
seata的XA模式
RM—资源的工作:
- 注册分支事务到TC
- 执行分支业务sql但不提交
- 报告执行状态到TC
TC—协调的工作:
- TC根据分支事务执行状态
a. 如果都成功,通知所有RM提交事务
b. 如果有失败,通知所有RM回滚事务
RM—资源的工作:
- 接收TC指令,提交或回滚事务
AT模式的优点:
- 强一致性,满足ACID原则
- 常用数据库都支持,实现简单,没有代码侵入
AT模式的缺点:
- 一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
实现
-
修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata: data-source-proxy-mode: XA # 开启数据源代理的XA模式
-
给涉及全局事务的入口方法标记
@GlobalTransactional
注解,本例中是OrderServiceImpl中的create方法:
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
// 扣余额
// 扣减库存
return order.getId();
}
- 重启服务或测试
AT模式原理
AT模式同样是分阶段提交的事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷
阶段一RM的工作:
- 注册分支事务
- 记录Undo-log (数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二 提交
时RM的工作:
- 删除undo-log
阶段二 回滚
RM的工作:
- 根据undo-log恢复数据到更新前
AT模式和XA模式最大的区别是什么?
- XA模式一阶段不提交事务,确保资源;AT模式一阶段直接提交,不锁定资源。
- XA模式在依赖数据库层面才能实现回滚;AT模式在利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致。
AT模式的脏写问题
2.1 会用100来恢复数据,事务2的更新会丢失
AT模式的写隔离
全局锁: 由TC记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权
与XA模式的锁不同,XA会锁住数据库资源,AT模式的锁只针对Seata管理的事务
为了防止死锁,全局锁默认最高重试30次,间隔10毫秒。
如果事务2是非seata管理的全局事务还是会覆盖。
这里借助乐观锁的思路,在2.1基于快照恢复数据前比较快照值和数据库里实际存在的值
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好得多
实现
-
lock_table导入到TC服务关联的数据时,undo_log表导入到微服务关联的数据库
-
修改application.yml文件,开启AT模式:
seata: data-source-proxy-mode: XA # 开启数据源代理的XA模式
-
重启服务并测试
TCC模式原理
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法
- Try: 资源的检测和预留
- Confirm: 完成资源操作业务;要求Try成功Confirm一定要成功
- Cancel: 预留资源释放,可以理解为Try的反向操作
例子: 一个扣减用户余额的业务。假设账户A原来余额是100元,需要余额扣减30元。
- 阶段一(Try): 检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣减30元
- 阶段二(Confirm): 冻结金额扣减30元
- 阶段二(Cancel): 冻结金额扣减30元,可用余额增加30元
TCC靠资源的预留,不用加锁就实现了隔离,性能好
TCC模式的优点:
- 一阶段完成提交提交事务,释放数据操作资源,性能好
- 相比AT模型,无需生成快照、无需使用全局锁、性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC模式的缺点:
- 有代码侵入、需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态, 事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
TCC的空回滚和业务悬挂
空回滚: 某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚
需要对下面分支事务的cancel失败做处理,不单独处理会一直卡在报错
业务悬挂: 对于已经空回滚的业务,如果以后继续执行try,就永远不可能confirm或cancel。应阻止执行空回滚后的Try操作,避免悬挂。
解决:
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态, 0:try, 1:confirm, 2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
Try 业务:
- 记录冻结金额和事务状态到
account_freeze
表 - 扣减
account
表可用余额
Confirm业务:
- 根据
xid
删除account_freeze
表的冻结记录
Cancel业务:
- 修改
account_freeze
表,冻结金额为0,state为2 - 修改
account
表,恢复可用金额
如何判断是否空回滚
- cancel业务中,根据
xid
查询account_freeze
,如果为null则说明try还没有做,需要空回滚
如何判断业务悬挂
- try业务中,根据
xid
查询account_freeze
,如果已经存在则说明Cancel已经执行,拒绝执行try业务
实现
TCC的Try、Confirm、Cancel方法都需要在接口中基于注释来声明
@LocalTCC
public interface TCCService {
/**
* Try阶段,@TwoPhaseBusinessAction中的name属性定义当前业务方法名字,用于指定try阶段对应的方法
*/
@TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")
void prepare(@BusinessActionContextParameter(paramName = "param") String param);
/**
* 二阶段confirm确认方法,可以与prepare,但是要定义与commitMethod一致
*
* @param context 上下文,可以传递try方法的参数
* @return boolean 执行是否成功
*/
boolean confirm(BusinessActionContext context);
/**
* 二阶段cancel取消方法,要定义与rollbackMethod一致
*/
boolean cancel(BusinessActionContext context);
}
SAGA模式原理
Saga模式是Seata提供的长事务解决方案。
- 一阶段: 直接提交本地事务
- 二阶段: 成功则什么都不做,失败则通过编写补偿业务来回滚
事务由事件驱动会阻塞等待
SAGA模式优点:
- 事务参与者可以通过基于时间驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
SAGA模式缺点:
- 软状态时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
四种模式对比
XA | AT | TCC | SAGA | |
---|---|---|---|---|
一致性 | 强一致 | 弱一致 | 弱一致 | 最终一致 |
隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源锁预留隔离 | 无隔离 |
持续性 | 无 | 无 | 有,要编写三个接口 | 有,要编写状态机和补偿业务 |
性能 | 差 | 好 | 非常好 | 非常好 |
描述 | 对一致性、隔离性有高要求的业务 | 基于关系型数据库的大多数分布式事务场景都可以 | 对性能要求极高的事务。有非关系型数据库等场景 | 业务流程长、业务流程多、参与者包含其它公司或遗留系统服务,无法提供TCC模式要求的三个接口 |
高可用
高可用集群结构
TC服务作为Seata的核心业务,一定要保证高可用和异地容灾
确定集群:
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping:
seata-demo: SH # 事务组对应cluster的映射关系
service:
vgroup-mapping:
seata-demo: SH
这部分是在本地配置文件的,如果想映射到nacos上
新建配置
Data ID: client.properties
Group: SEATA_GROUP
配置内容
# 事务组映射关系
service.vgroupMapping.seata-demo=SH
...
# 与TC服务的通信配置
...
# RM配置
...
微服务读取nacos配置
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
data-id: client.properties