在分布式应用场景中,分布式事务问题是不可回避的,在目前风行的微服务场景下更是如此。比方在咱们的商城零碎中,下单操作波及创立订单和库存扣减操作两个操作,而订单服务和商品服务是两个独立的微服务,因为每个微服务独占一个数据库实例,所以下单操作就波及到分布式事务问题,即要把整个下单操作看成一个整体,要么都胜利要么都不胜利。本篇文章咱们就一起来学习下分布式事务的相干常识。
基于音讯实现最终一致性
咱们去店里就餐的时候,付钱点餐后往往服务员会先给咱们一张小票,而后拿着小票去出餐口期待出餐。为什么要把付钱和取餐两个动作离开呢?很重要的一个起因是使他们的接客能力更强,对应到服务来说就是使并发解决能力更强。只有咱们拿着小票,最终咱们是能够拿到咱们点的餐的,依附小票这个凭证(音讯)实现最终一致性。
对应到咱们的下单操作来说,当用户下单后,咱们能够学生成订单,而后发一条扣减库存的音讯到音讯队列中,这时候订单就算实现,但理论还没有扣减库存,因为库存的扣减和下单操作是异步的,也就是这个时候产生了数据的不统一。当生产到了扣减库存的音讯后进行库存扣减操作,这个时候数据实现了最终一致性。
基于音讯实现最终一致性这种策略实用于并发量比拟高同时对于数据一致性要求不高的场景。咱们商城中的一些非骨干逻辑能够采纳这种形式来晋升吞吐,比方购买商品后获取优惠券等非核心逻辑并不需要数据的强统一,能够异步的给用户发放优惠券。
如果在生产到音讯后,执行操作的时候失败了该怎么办呢?首先须要做重试,如果重试屡次后依然失败,这个时候须要收回告警或者记录日志,须要人工染指解决。
如果对数据有强统一要求的话,那这种形式是不实用的,请看下上面的两阶段提交协定。
XA 协定
说起 XA 协定,这个名词你未必据说过,但一提到 2PC 你必定据说过,这套计划依赖于底层数据库的反对,DB 这层首先得要实现 XA 协定。比方 MySQL InnoDB 就是反对 XA 协定的数据库计划,能够把 XA 了解为一个 强统一的中心化原子提交协定。
原子性的概念就是把一系列操作合并成一个整体,要么都执行,要么都不执行。而所谓的 2PC 就是把一个事务分成两步来提交,第一步做筹备动作,第二步做提交 / 回滚,这两步之间的协调是由一个中心化的 Coordinator 来治理,保障多步操作的原子性。
第一步(Prepare):Coordinator 向各个分布式事务的参与者下达 Prepare 指令,各个事务别离将 SQL 语句在数据库执行但不提交,并且将准备就绪状态上报给 Coordinator。
第二步(Commit/Rollback):如果所有节点都已就绪,那么 Coordinator 就下达 Commit 指令,各参与者提交本地事务,如果有任何一个节点不能就绪,Coordinator 则下达 Rollback 指令进行本地回滚。
在咱们的下单操作中,咱们须要创立订单同时商品须要扣减库存,接下来咱们来看下 2PC 是怎么解决这个问题的。2PC 引入了一个事务协调者的角色,来协调订单和商品服务。所谓的两阶段是指筹备阶段和提交阶段,在筹备阶段,协调者别离给订单服务和商品服务发送筹备命令,订单和商品服务收到筹备命令后,开始执行筹备操作,筹备阶段须要做哪些事件呢?你能够了解为,除了提交数据库事务以外的所有工作,都要在筹备阶段实现。比方订单服务在筹备阶段须要实现:
- 在订单库开启一个数据库事务;
- 在订单表中写入订单数据
留神这里咱们没有提交订单数据库事务,最初给书屋协调者返回筹备胜利。协调者在收到两个服务筹备胜利的响应后,开始进入第二阶段。进入提交阶段,提交阶段就比较简单了,协调者再给这两个零碎发送提交命令,每个零碎提交本人的数据库事务而后给协调者返回提交胜利响应,协调者收到有响应之后,给客户端返回胜利的响应,整个分布式事务就完结了,以下是这个过程的时序图:
以上是失常状况,接下来才是重点,异常情况怎么办呢?咱们还是分两阶段来阐明,在筹备阶段,如果任何异步呈现谬误或者超时,协调者就会给两个服务发送回滚事务申请,两个服务在收到申请之后,回滚本人的数据库事务,分布式事务执行失败,两个服务的数据库事务都回滚了,相干的所有数据回滚到分布式事务执行之前的状态,就像这个分布式事务没有执行一样,以下是异常情况的时序图:
如果筹备阶段胜利,进入提交阶段,这个时候整个分布式事务就 只能胜利,不能失败。如果产生网络传输失败的状况,须要重复重试,直到提交胜利为止,如果这个阶段产生宕机,包含两个数据库宕机或者订单服务、商品服务宕机,还是可能呈现订单库实现了提交,但商品库因为宕机主动回滚,导致数据不统一的状况,然而,因为提交的过程非常简单,执行十分迅速,呈现这种状况的概率比拟低,所以,从实用的角度来说,2PC 这种分布式事务办法,理论的数据一致性还是十分好的。
但这种分布式事务有一个人造缺点,导致 XA 特地不适宜用在互联网高并发的场景外面,因为每个本地事务在 Prepare 阶段,都要始终占用一个数据库的连贯资源,这个资源直到第二阶段 Commit 或者 Rollback 之后才会被开释。但互联网场景的个性是什么?是高并发,因为并发量特地高,所以每个事务必须尽快开释掉所持有的数据库连贯资源。事务执行工夫越短越好,这样能力让别的事务尽快被执行。
所以,只有在须要强统一,并且并发量不大的场景下,才思考 2PC。
2PC 也有一些改良版本,比方 3PC,大体思维和 2PC 是差不多的,解决了 2PC 的一些问题,然而也会带来新的问题,实现起来也更简单,限于篇幅咱们没法每个都具体的去解说,在了解了 2PC 的根底上,大家能够自行搜寻相干材料进行学习。
分布式事务框架
想要本人实现一套比较完善且没有 bug 的分布式事务逻辑还是比较复杂的,好在咱们不必反复造轮子,曾经有一些现成的框架能够帮咱们实现分布式事务,这里次要介绍应用和 go-zero 联合比拟好的 DTM。
援用 DTM 官网的的介绍,DTM 是一款变革性的分布式事务框架,提供了傻瓜式的应用形式,极大地升高了分布式事务的应用门槛,改了变了”能不必分布式事务就不必“的行业现状,优雅的解决了服务间的数据一致性问题。
本文作者在写这篇文章之前听过 DTM,但素来没有应用过,大略花了十几分钟看了下官网文档,就能照葫芦画瓢地应用起来了,也足以阐明 DTM 的应用是非常简单的,置信聪慧的你必定也是一看就会。接下来咱们就应用 DTM 基于 TCC 来实现分布式事务。
首先须要装置 dtm,我应用的是 mac,间接应用如下命令装置:
brew install dtm
给 DTM 创立配置文件 dtm.yml,内容如下:
MicroService:
Driver: 'dtm-driver-gozero' # 配置 dtm 应用 go-zero 的微服务协定
Target: 'etcd://localhost:2379/dtmservice' # 把 dtm 注册到 etcd 的这个地址
EndPoint: 'localhost:36790' # dtm 的本地地址
# 启动 dtm
dtm -c /opt/homebrew/etc/dtm.yml
在 seckill-rmq 中生产到订单数据后进行下单和扣库存操作,这里改成基于 TCC 的分布式事务形式,留神 dtmServer 和 DTM 配置文件中的 Target 对应:
var dtmServer = "etcd://localhost:2379/dtmservice"
因为 TCC 由三个局部组成,别离是 Try、Confirm 和 Cancel,所以在订单服务和商品服务中咱们给这三个阶段别离提供了对应的 RPC 办法,
在 Try 对应的办法中次要做一些数据的 Check 操作,Check 数据满足下单要求后,执行 Confirm 对应的办法,Confirm 对应的办法是真正实现业务逻辑的,如果失败回滚则执行 Cancel 对应的办法,Cancel 办法次要是对 Confirm 办法的数据进行弥补。代码如下:
var dtmServer = "etcd://localhost:2379/dtmservice"
func (s *Service) consumeDTM(ch chan *KafkaData) {defer s.waiter.Done()
productServer, err := s.c.ProductRPC.BuildTarget()
if err != nil {log.Fatalf("s.c.ProductRPC.BuildTarget error: %v", err)
}
orderServer, err := s.c.OrderRPC.BuildTarget()
if err != nil {log.Fatalf("s.c.OrderRPC.BuildTarget error: %v", err)
}
for {
m, ok := <-ch
if !ok {log.Fatal("seckill rmq exit")
}
fmt.Printf("consume msg: %+v\n", m)
gid := dtmgrpc.MustGenGid(dtmServer)
err := dtmgrpc.TccGlobalTransaction(dtmServer, gid, func(tcc *dtmgrpc.TccGrpc) error {
if e := tcc.CallBranch(&product.UpdateProductStockRequest{ProductId: m.Pid, Num: 1},
productServer+"/product.Product/CheckProductStock",
productServer+"/product.Product/UpdateProductStock",
productServer+"/product.Product/RollbackProductStock",
&product.UpdateProductStockRequest{}); err != nil {logx.Errorf("tcc.CallBranch server: %s error: %v", productServer, err)
return e
}
if e := tcc.CallBranch(&order.CreateOrderRequest{Uid: m.Uid, Pid: m.Pid},
orderServer+"/order.Order/CreateOrderCheck",
orderServer+"/order.Order/CreateOrder",
orderServer+"/order.Order/RollbackOrder",
&order.CreateOrderResponse{},); err != nil {logx.Errorf("tcc.CallBranch server: %s error: %v", orderServer, err)
return e
}
return nil
})
logger.FatalIfError(err)
}
}
结束语
本篇文章次要和大家一起学习了分布式事务相干的常识。在并发比拟高且对数据没有强一致性要求的场景下咱们能够通过音讯队列的形式实现分布式事务达到最终一致性,如果对数据有强一致性的要求,能够应用 2PC,然而数据强统一的保障必然会损失性能,所以个别只有在并发量不大,且对数据有强一致性要求时才会应用 2PC。3PC、TCC 等都是针对 2PC 的一些毛病进行了优化革新,因为篇幅限度所以这里没有具体开展来讲,感兴趣的敌人能够自行搜寻相干材料进行学习。最初基于 TCC 应用 DTM 实现了一个下单过程分布式事务的例子,代码实现也非常简单易懂。对于分布式事务心愿大家能先搞明确其中的原理,理解了原理后,不论应用什么框架那都不在话下了。
心愿本篇文章对你有所帮忙,谢谢。
每周一、周四更新
代码仓库: https://github.com/zhoushuguang/lebron
我的项目地址
https://github.com/zeromicro/go-zero
欢送应用 go-zero
并 star 反对咱们!
微信交换群
关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。