分布式事务解决方案Seata之Saga模式
背景:
随着各行各业数字化转型的深入,以及技术的持续迭代更新,互联网公司的技术也逐渐落地到传统行业,例如金融业。由于业务的快速增长以及为了快速抢占市场等因素,以前单体架构的应用变得不再符合需求。因此,由以前的传统单体架构的系统或应用,演变成面向服务架构的分布式系统。
构建分布式系统,除了根据业务划分服务等偏业务问题,还有随之而来的技术难点,例如分布式事务。在金融行业中,业务系统往往需要聚合多个下游系统或者多个本系统服务,这种长事务如何保证数据一致性。市面上有许多解决方案,但是阿里开源的分布式事务解决方案seata是最优秀和最知名的,其中的saga模式即可解决长事务场景。
分布式事务
1.事务
• 事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
• 原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。
• 一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。
• 隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。
• 持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
2.分布式事务
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务。
3.分布式事务有哪些场景
• 电商系统中的下单扣库存场景,在电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成。
• 金融系统中的银行卡充值场景,在金融系统中通过银行卡向平台充值需要通过银行系统和金融系统协同完成。
• 教育系统中下单选课业务场景,在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统和选课系统协同完成。
• SNS系统的消息发送场景,在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统和手机通信系统协同完成。
Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Saga模式
概述:Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
从上图可知,每个正常节点事务,对应一个补偿节点事务,一个事务可由多个节点组成,在某个节点事务失败,补偿事务从失败节点对应补偿节点执行,将此前成功的事务全部补偿,保证数据最终一致。
适用场景:
●业务流程长、业务流程多
●参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
●一阶段提交本地事务,无锁,高性能
●事件驱动架构,参与者可异步执行,高吞吐
●补偿服务易于实现
缺点:
●不保证隔离性
Saga实现:
基于状态机引擎的 Saga 实现:
目前SEATA提供的Saga模式是基于状态机引擎来实现的,机制是:
通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
注意: 异常发生时是否进行补偿也可由用户自定义决定
可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
如下示例状态图:
状态机设计器
Seata Saga 提供了一个可视化的状态机设计器方便用户使用,代码和运行指南请参考:https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer,外网用户可通过如下地址设计:
状态机设计器演示地址:http://seata.io/saga_designer/index.html
Saga 服务设计的实践经验
允许空补偿
空补偿:原服务未执行,补偿服务执行了
出现原因:
原服务 超时(丢包)
Saga 事务触发 回滚
未收到 原服务请求,先收到 补偿请求
所以服务设计时需要允许空补偿, 即没有找到要补偿的业务主键时返回补偿成功并将原业务主键记录下来
防悬挂控制
悬挂:补偿服务 比 原服务 先执行
出现原因:
原服务 超时(拥堵)
Saga 事务回滚,触发 回滚
拥堵的 原服务 到达
所以要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝服务的执行
幂等控制
原服务与补偿服务都需要保证幂等性, 由于网络可能超时, 可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新
缺乏隔离性的应对
由于 Saga 事务不保证隔离性, 在极端情况下可能由于脏写无法完成回滚操作, 比如举一个极端的例子, 分布式事务内先给用户A充值, 然后给用户B扣减余额, 如果在给A用户充值成功, 在事务提交以前, A用户把余额消费掉了, 如果事务发生回滚, 这时则没有办法进行补偿了。这就是缺乏隔离性造成的典型的问题, 实践中一般的应对方法是:
业务流程设计时遵循“宁可长款, 不可短款”的原则, 长款意思是客户少了钱机构多了钱, 以机构信誉可以给客户退款, 反之则是短款, 少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。
有些业务场景可以允许让业务最终成功, 在回滚不了的情况下可以继续重试完成后面的流程, 所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力, 让业务最终执行成功, 达到最终一致性的目的。
Saga示例
示例说明:
本示例使用的技术栈为蚂蚁金服的开源版sofa框架、druid、mysql、sofaboot和Seata。实现扣减金额同时扣减库存需求。例如,扣减一个库存同时扣减一块钱。Saga实现里的状态机示例图为本demo的示例图。
环境准备:启动seata-sever,官网可下载,启动zookeeper。
工程结构如下:
其中状态机使用json文件在statelang目录下,provider下是服务提供文件,将服务注册到zk,consumer是消费端文件。
数据库表如下:
其中seata状态机初始化和运行三张表,业务处理的库存表和金额表。
正常运行示例如下:
首先启动SofaRpcSagaProviderApplication类,将服务注册到zk上,然后启动starter木下的消费类。
扣减金额时异常情况,开始补偿扣减金额事务,再执行补偿库存扣减事务,金额和库存数据均无变化。
此demo均已上传github,地址为:https://github.com/zbf2016/sofa-seata-saga-example.git