数据一致性(一)
Posted by 付辉 on Saturday, October 20, 2018 共1587字MySQL
的事务是数据一致性的典范,事务内的执行要么都成功,要么都失败。但业务系统涉及系统间的相互调用,涉及的数据库也不尽相同,所以实现数据一致性还是有挑战的。
首先了解强一致性和弱一致性。在微服务中,系统间通过HTTP
的方式相互调用,很难实现数据的强一致。我们这里主要说弱一致性,也就是数据最终一致性。
数据一致性还有个重要的前提:支持幂等。也就是说,只要请求参数不变,那么无论重复请求多少次,结果都一样。在对接第三方支付时,这个词出现的频率还是老高的。
基础理论
ACID
-
原子性
Atomicity
-
一致性
Consistency
-
隔离性
Isolation
-
持久性
Durability
如果涉及业务逻辑的数据被设计到同一个数据库中,借助事务的ACID
可以很方便地解决。但现实情况是,要保证数据一致性的数据被分布在多个不同的数据库里。
CAP
CAP
理论说明任何分布式系统只可同时满足两点,无法三点都满足。
-
一致性
Consistency
-
可用性
Avaliability
阮一峰
老师的解释说:只要收到用户的请求,服务器就必须给出回应。
- 分区容错
Partition tolerance
BASE
BASE
模型实现的系统不保证强一致性,在处理请求的过程中,允许存在短暂的数据不一致。系统在处理流程的各个阶段,会更新记录的对应状态,后续通过状态来修复数据,最终达到数据一致。
- 基本可用
Basically Available
- 软状态
Soft State
- 最终一致
Eventually Consistency
购买业务
蜗牛要在一家电商网站买电子书,整个购买流程和涉及的系统虚构如下图。过程涉及检查它是否已经买过,然后是生成订单号、支付、交付(实际上订单系统不包含支付功能,这里简化处理)。
交付涉及三个系统,在任何一个系统内,数据库的事务都只能保证它服务内的数据一致。而且,如果在事务过程中引入了调用第三方的HTTP
请求,数据库的事务执行结果甚至有可能会被污染。比如,HTTP
请求超时返回失败,但实际上请求却执行成功的场景。
代码设计
参考之前写的 Saga Pattern模式,对任何一个外部服务的调用都引入两个行为:执行和补偿。补偿是对执行结果的修正。比如对于用户支付失败的场景,补偿行为可以是接口重试、可以是直接退款、还可以推送MQ
异步修复等。
统一使用interface
来定义一套规范。每一种支付方式以及购买产品所调用的外部服务可能不尽相同,用interface
来达到统一调用的目的。补偿的行为都基于执行动作返回的错误,所以我们需要实现自己的错误码。
type DeliverPattern interface {
//是否需要执行交付流程
Check(ctx *context.Context) (bool, error)
//支付及支付补偿
DoPay(ctx *context.Context) error
PayCompensate(ctx *context.Context, doErr error) error
//交付及对应的补偿
DoDeliver(ctx *context.Context)
DeliverCompensate(ctx *context.Context, doErr error) error
}
如何补偿
对于如何补偿,不同的业务有不同的补偿方式,当让不能一概而论。但整体的思想,我觉得还是不外乎两种。当然,下面的两种描述是自己这样称呼的。
事务类
首先便是数据库事务
类,任何一个流程失败,整个事务内的操作全部反向回滚。沿着这样的思路,接口定义中PayCompensate
应该实现DoPay
的回滚操作,而DeliverCompensate
应该实现DoPay
以及DoDeliver
的回滚操作。
我们需要在操作的同时维护一个回滚操作的队列,任何一个Do
行为的完成,都需要在回滚队列中插入对应的回滚方法。当后面任何一个Do
操作失败,统一执行回滚队列的方法。
这样的困境在于你不能完全保证回滚方法一定成功执行。而且出于性能考虑,还需要结合异步队列,通过后台重试来保证整个业务流程彻底回滚成功或回滚失败。
状态类
每个业务都会拆分成各个更小的块,就跟写代码空行一样,这里的DeliverPattern
也是根据业务流程拆分成更小的执行粒度。我们可以为每个Do
行为都设置一个状态码,类似于状态机,记录每一次购买的各个状态。
const (
StatusDoPaySuccess = 1
StatusDoPayCompensateSuccess = 2
StatusDoPayCompensateFailure = 3
)
这样我们补偿方法中执行的不再是回滚操作,而是Do
方法的重试。如果补偿成功,继续执行后续的操作,如果补偿失败,记录下该状态,后续看看怎么补偿。