Technology

Chart Type 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go 内存管理 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

Architecture

实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 在离线业务混部 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF 生命周期管理 openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 概率论 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列


分布式事务

2017年07月18日

简介

开发运维割裂,云原生也不是万金油分布式事务框架都跟两阶段协议有千丝万缕的联系,比如 TCC,也是分成两阶段,第一阶段大家各自预留资源,向协调器反馈结果。第二阶段根据反馈结果执行确认或者回滚。再比如 Saga,进一步把预留资源和确认操作两步合一步,变成了直接执行,失败了再重试或者回滚作为补偿。这其实是一种两阶段协议更进一步的乐观主义的实现。PS: 分布式的几个实现方案跟悲观锁和乐观锁有点异曲同工的意思

原子提交协议

ACID中的一致性,是个很偏应用层的概念。这跟ACID中的原子性、隔离性和持久性有很大的不同。原子性、隔离性和持久性,都是数据库本身所提供的技术特性;而一致性,则是由特定的业务场景规定的。要真正做到ACID中的一致性,它是要依赖数据库的原子性和隔离性的,但是,就算数据库提供了所有你所需要的技术特性,也不一定能保证ACID的一致性。这还取决于你在应用层对于事务本身的实现逻辑是否正确无误。PS:ACID 中的一致性与事务一致性要解决的问题是不同的。

ACID中的原子性,要求事务的执行要么全部成功,要么全部失败,而不允许出现“部分成功”的情况。在分布式事务中,这要求参与事务的所有节点,要么全部执行Commit操作,要么全部执行Abort操作。换句话说,参与事务的所有节点,需要在“执行Commit还是Abort”这一点上达成一致(其实就是共识)。这个问题在学术界被称为原子提交问题(Atomic Commitment Problem),而能够解决原子提交问题的算法,则被称为原子提交协议(Atomic Commitment Protocal,简称ACP)。2PC和3PC,属于原子提交协议两种不同的具体实现。原子提交问题是共识问题的一个特例

但是,分布式系统的诡异之处就要体现在这里,一些细节的不同,可能导致非常大的差异。如果你仔细看前文的描述,会发现这样一个细节:当我们描述共识问题的时候,我们说的是在多个节点之间达成共识;而当我们描述原子提交问题的时候,我们说的是在所有节点之间达成共识。这个细微的差别,让这两类问题,几乎变成了完全不同的问题(谁也替代不了谁)。

论文Uniform Consensus Is Harder Than Consensus 进一步澄清了这一问题,原子提交问题被抽象成一个新的一致性问题,称为uniform consensus问题,它是与通常的共识问题(consensus problem)不同的问题,而且是更难的问题。uniform consensus,要求所有节点(包括故障节点)都要达成共识;而consensus问题只关注没有发生故障的节点达成共识。

解决方案

基于Saga的分布式事务调度落地我们常见的分布式事务来保证数据的一致性的方法分为两类:强一致性、最终一致性。

  1. 采用强一致性的分布式事务的方案:通常采用两段式提交协议2PC、三段式提交协议3PC。在微服务架构中,该种方式不太适合,原因如下:
    1. 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC或Http API进行,所以已经无法使用TM统一管理微服务的RM
    2. 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不原生支持事务的数据库,业务的事务很难实现
    3. 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能
  2. 常见的最终一致性的分布式事务解决方案有:事件通知模式(本地异步事件服务模式、外部事件服务模式、MQ事务消息模式、最大努力通知模式)、事务补偿模式(Saga、TCC)。 Saga、TCC是一种补偿性事务的思想,对业务入侵较大,需要业务方实现对应的方法。
    1. 对于TCC模式来说,要做一个分布式事务,业务中的一个接口需要完成3个逻辑的改造,Try-Confirm-Cancel。对于业务的侵入性比较强,流程比较繁琐。例如:给用户添加积分,我们不直接添加积分,先预处理添加积分。扣物品库存我们也不直接扣库存,先冻结将扣掉的库存。
    2. Saga是一种纯业务补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。
    3. 本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。

谈谈对分布式事务的一点理解和解决方案目前业界主流的分布式事务解决方案主要有:

  1. 多阶段提交方案。常见的有2pc和3pc,需要额外的资源管理器来协调事务,数据一致性强,但是实现方案比较复杂,对性能的牺牲比较大(主要是需要对资源锁定,等待所有事务提交才能解锁),不适用于高并发的场景,目前比较知名的有阿里开源的fescar。
  2. 补偿事务。一般也叫TCC,因为每个事务操作都需要提供三个操作尝试(Try)、确认(Confirm)和补偿/撤销(Cancel),数据一致性的强度比多阶段提交方案低,但是实现的复杂度会有所降低,比较明显的缺陷是每个业务事务需要实现三组操作,有可能出现过多的补偿方案的代码;另外有很多输完液场景TCC是不合适的。
  3. 消息事务。这里只谈RocketMQ的实现,一个事务的执行流程包括:发送预消息、执行本地事务、确认消息发送成功。它的消息中间件存储了下游无法消费成功的消息,并且不断重试推送下游消费消息,而生产者(上游)需要提供一个check接口,用于检查成功发送预消息但是未确认最终消息发送状态的事务的状态。

2pc 和 3pc

2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,只能用在数据库层面。

过程

正常情况

异常情况

无论事务提交,还是事务回滚,都是两个阶段

2pc有很多问题,比如单点、同步阻塞等,此处我只讨论数据一致性问题:在二阶段提交协议的阶段二,即执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求。于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其他没有收到Commit请求的参与者则无法进行事物提交,于是整个分布式系统便出现了数据不一致的现象。2PC除本身的算法局限外,还有一个使用上的限制,就是它主要用在两个数据库之间(XA是一种数据库实现的、基于2PC协议的规范)。但以支付宝的转账为例,是两个系统之间的转账,而不是底层两个数据库之间直接交互,所以没有办法使用2PC。

正常情况

异常情况

在分布式环境下,分布式系统的每一次请求和响应,存在特有的三态概念:即成功、失败、超时。相对于2pc,3pc处理了timeout问题。但3pc在数据一致性上也有问题:在参与者接收到preCommit消息后,如果出现网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,参与者依然会进行事物的提交,就可能出现不同节点成功or失败,这必然出现数据的不一致。3PC并没有解决2PC的根本问题,它只是在2PC的基础上做了一些优化,它增加了一个阶段(也增加了1个RTT)来提高对方可用性的概率,这本质跟TCP的三次握手一样,同样也改为四次握手,五次握手等等

在代码上的表现

  1. 声明式事务
  2. 编程式事务

本地事务处理

Connection conn = null; 
try{
    //若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
    conn.setAutoCommit(false);
    // 将 A 账户中的金额减少 500 
    // 将 B 账户中的金额增加 500 
    conn.commit();
}catch(){
     conn.rollback();
}

跨数据库事务处理

UserTransaction userTx = null; 
Connection connA = null; 
Connection connB = null; 
try{
    userTx.begin();
    // 将 A 账户中的金额减少 500 
    // 将 B 账户中的金额增加 500 
    userTx.commit();
}catch(){
     userTx.rollback();
}

代码的封装跟2pc的实际执行过程有所不同,可以参见JTA与TCC

TCC

聊一聊分布式事务TCC (Try-Confirm-Cancel)事务模型采用的是补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿操作。相当于XA来说,TCC可以不依赖于资源管理器,即数据库,它是通过业务逻辑来控制确认和补偿操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一个应用层面的2PC。

TCC借鉴2PC的思路,对比上一小节可以看到,调用方充当了协调者的角色。那么它是如何解决2PC的问题的呢?也就是说,在阶段2,如果Try成功了,那么Confirm阶段异常了就一直重试,直到成功。调用方发生宕机,或者某个服务超时了,不断重试Cancel!不管是Confirm失败了,还是Cancel失败了,都不断重试。这就要求Confirm和Cancel都必须是幂等操作。注意,这里的重试是由TCC的框架来执行的,而不是让业务方自己去做。

分布式事务,阿里为什么钟爱TCC以电商系统为例,假如有订单、库存和账户3个服务,客户购买一件商品,订单服务增加订单,库存服务扣减库存,账户服务扣减金额,这三个操作必须是原子性的,要么全部成功,要么全部失败。订单、库存和账户这三个服务作为整个分布式事务的分支事务,在try阶段都是要提交本地事务的。上面库存和账户说的冻结,就是说这个订单对应的库存和金额已经不能再被其他事务使用了,所以必须提交本地事务。但这个提交并不是真正的提交全局事务,而是把资源转到中间态,这个中间态需要在try方法的业务代码中实现,比如账户扣除的金额可以先存放到一个中间账户。如果try阶段不提交本地事务会有什么问题呢?有可能其他事务在try阶段发现用户账户里面的金额还够,但是commit的时候发现金额不够了,commit阶段扣款只能失败,这时其他两个分支事务提交成功而账户服务的分支事务提交失败,最终数据就不一致了。commit阶段,数据从中间态转入终态,比如订单金额从中间账户转到最终账户。

TCC的try/commit/cancel,对业务代码都有侵入,而且每个方法都是一个本地事务。再加上需要考虑幂等、空回滚、悬挂等,代码侵入会更高。不过除了侵入业务代码这一个问题,其他问题都有对应的解决方案。阿里针对TCC做了一些优化,包括第二阶段异步提交和同库模式,性能提升很明显。

github 案例项目

  1. QNJR-GROUP/EasyTransaction
  2. moonufo/galaxyLight

Saga

基于Saga的分布式事务调度落地

Saga的基本概念:

  1. saga:长事务,long live transaction
  2. 每个本地事务有对应的补偿事务
  3. 执行情况
    1. 正常:T1 -> T2 -> T3 -> … -> Tn
    2. 异常:T1 -> T2 -> T3(异常)-> C3 -> C2 -> C1

Saga两种恢复策略:

  1. backward recovery,向后恢复,补偿所有已完成的事务(回滚操作)
  2. forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功(重试操作)

Saga事务的优缺点:

  1. 优点:模型比TCC更简单,只需业务方提供事务执行接口transaction、事务取消补偿接口cancel
  2. 缺点:直接执行事务执行接口transaction,可能有副作用(无论是否回滚,都会执行事务接口的逻辑,举例:A账号向B账号转账,T1事务对A用户扣款,T2事务对B用户加款,T1执行成功,同时产生了一条扣款记录,T2执行失败需要回滚T2和T1,在这个过程中的副作用是A账号能感知到金额变化和扣款记录)。PS:“A账号能感知到金额变化和扣款记录”是tcc的时候,try-confirm 逻辑的差别

最终一致性+消息中间件

实践丨分布式事务解决方案汇总:2PC、消息中间件、TCC、状态机+重试+幂等一般的思路是通过消息中间件来实现“最终一致性”

错误的方案:网络调用(rpc/db/mq)和更新DB放在同一个事务里面

这个方案有两个问题

  1. 发送消息失败,发送方并不知道是消息中间件没有收到消息,还是消息已经收到了,只是返回response的时候失败了?
  2. 把网络调用放在数据库事务里面,可能会因为网络的延时导致数据库长事务。严重的会阻塞整个数据库,风险很大。

最终一致性:第1种实现方式(业务方自己实现)

  1. 系统A不再直接给消息中间件发送消息,而是把消息写入到消息表中。把DB1的扣钱操作(表1)和写入消息表(表2)这两个操作放在一个数据库事务里,保证两者的原子性。
  2. 系统A准备一个后台程序,源源不断地把消息表中的消息传送给消息中间件。如果失败了,也不断尝试重传。因为网络的2将军问题,系统A发送给消息中间件的消息网络超时了,消息中间件可能已经收到了消息,也可能没有收到。系统A会再次发送该消息,直到消息中间件返回成功。所以,系统A允许消息重复,但消息不会丢失,顺序也不会打乱。
  3. 系统A保证了消息不丢失

系统B对消息的消费要解决下面两个问题:

  1. 丢失消费,系统B从消息中间件取出消息(此时还在内存里面),如果处理了一半,系统B宕机并再次重启,此时这条消息未处理成功,怎么办?答案是通过消息中间件的ACK机制,凡是系统B(向mq)发送ACK的消息,系统B重启之后消息中间件不会再次推送;
  2. 重复消费。除了ACK机制可能会引起重复消费,系统A的后台任务也可能给消息中间件重复发送消息。为此,系统B增加一个判重表(业务允许的话也可以直接业务判重)

下游服务消费消息成功可以回调一个确认到上游服务,这样就可以从上游服务的本地消息表删除对应的消息记录。如果消费失败了,则可以重试,但还一直失败怎么办?是否要自动回滚整个流程?答案是人工介入。从工程实践角度来讲,这种整个流程自动回滚的代价是非常巨大的,不但实现起来很复杂,还会引入新的问题。比如自动回滚失败,又如何处理?对应这种发生概率极低的事件,采取人工处理会比实现一个高复杂的自动化回滚系统更加可靠,也更加简单。

弱一致性+基于状态的补偿,有点saga 的意思

如果用最终一致性方案,因为是异步操作,如果库存扣减不及时会导致超卖,因此最终一致性的方案不可行;如果用TCC方案,则意味着一个用户请求要调用两次(Try和Confirm)订单服务、两次(Try和Confirm)库存服务,性能又达不到要求。如果用事务状态表,要写事务状态,也存在性能问题。

既要满足高并发,又要达到一致性,鱼和熊掌不能兼得。可以利用业务的特性,采用一种弱一致的方案。

比如对于电商的购物来讲,允许少卖,但不能超卖。

先扣库存再创建订单

扣库存 提交订单 调用方处理
成功 成功 返回成功
成功 失败 告诉用户失败,用户重试(扣库存+提交订单),可能会导致多扣库存
失败 不提交订单 告诉用户失败,用户重试(扣库存+提交订单),可能会导致多扣库存

先创建订单再扣库存

提交订单 扣库存 调用方处理
成功 成功 返回成功
成功 失败 告诉用户失败,用户重试(提交订单+扣库存),可能会导致多扣库存
失败 不提扣库存 告诉用户失败,用户重试(提交订单+扣库存)

也就是数据库中可能存在 库存记录无法对应订单,但不可能存在订单记录无法对应库存。

库存每扣一次,都会生成一条流水记录。这条记录的初始状态是“占用”,等订单支付成功后,会把状态改成“释放”。通过比对,得到库存系统的“占用又没有释放的库存流水”与订单系统的未支付的订单,就可以回收这些库存,同时把对应的订单取消。类似12306网站,过一定时间不支付,订单会取消,将库存释放。

不管事务,事后处理——对账

岂止事务有状态,系统中的各种数据对象都有状态,或者说都有各自完整的生命周期,同时数据与数据之间存在着关联关系。我们可以很好地利用这种完整的生命周期和数据之间的关联关系,来实现系统的一致性,这就是“对账”。

在前面的方案中,无论最终一致性,还是TCC、事务状态表,都是为了保证“过程的原子性”,也就是多个系统操作(或系统调用),要么全部成功,要么全部失败。但所有的“过程”都必然产生“结果”,过程是我们所说的“事务”,结果就是业务数据。一个过程如果部分执行成功、部分执行失败,则意味着结果是不完整的。从结果也可以反推出过程出了问题,从而对数据进行修补,这就是“对账”的思路!

假定从“已支付”到“下发给仓库”最多用1个小时;从“下发给仓库”到“出仓完成”最多用8个小时。意味着只要发现1个订单的状态过了1个小时之后还处于“已支付”状态,就认为订单下发没有成功,需要重新下发,也就是“重试”。同样,只要发现订单过了8个小时还未出仓,这时可能会发出报警,仓库的作业系统是否出了问题……诸如此类。

千万级支付对账系统是怎么设计的?

AT

分布式事务 GTS 的价值和原理浅析 代码已开源

执行阶段:GTS 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在

完成阶段:如果 TM 发出的决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),完成阶段 可以非常快速地完成。

完成阶段:如果 TM 发出的决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

综上,GTS 创新地基于 SQL 解析实现对业务无侵入的自动补偿回滚机制。说人话就是:由GTS 本身负责 事务过程中间数据的产生(比如undo log table)和处理(比如callback)

微服务事务一致性

聊聊微服务架构中的事务处理未掌握

再有人问你分布式事务和解决方案,把这篇文章扔给他!未读

分布式事务解决方案汇总:2PC、消息中间件、TCC、状态机+重试+幂等

事件驱动的分布式事务架构设计 在传统的软件架构中,应用逻辑是通过请求、过程驱动的。一个请求执行一段逻辑同步返回一个响应,在业务逻辑中,将要执行的代码按照过程顺序进行编排。而事件驱动架构中,事件消费者会以异步的方式处理事件生产者产生的事件,原来过程当中的逻辑交给事件消费者去处理,解开服务之间的耦合,使应用的逻辑聚焦,应用的职责单一,代码更加简洁,也能提升系统的响应能力。

随着对云原生技术的理解深入,从 Kubernetes Control-Loop 思想中获得灵感,全新设计了高性能、无侵入、事件驱动的 Go 语言分布式事务框架 hptx,以及支持跨语言分布式事务、读写分离、分库分表的 Mesh 方案 DBPack。下图展示了 hptx 和 dbpack 的事务协调逻辑,事务发起者 AggregationSvc 发起全局事务提交、回滚,仅仅是修改 ETCD 中的数据状态,然后立即返回。订单服务和商品服务使用前缀 bs/${appid} Watch 存储在 ETCD 中的分支事务数据,当分支事务的数据发生过变更后,ETCD 马上推送一个变更事件给相应服务,订单服务和商品服务收到变更事件后,将数据加入 workqueue 去执行提交或回滚的逻辑。AggregationSvc 提交、回滚时不会调用 OrderSvc、ProductSvc 的接口,整个过程通过 ETCD 解耦后异步执行。事务分支提交或者回滚失败后,会重新进入到 workqueue 当中继续消费,直至提交、回滚成功,或回滚超时。

在这个架构中,已经没有中心化事务协调者 TC Server,用户只需要关心自身应用的高可用,如果应用多副本部署,hptx 和 dbpack 会通过 etcd 选主,只有选为 master 的副本才能 watch 自身产生的分支事务数据去做提交、回滚,避免了提交、回滚逻辑重复执行的问题。集成 hptx,只需要依赖相应的 sdk,而不需要部署额外的 TC Server,但状态数据的存储由原来的 Mysql 换成了 ETCD。

小结

2018.9.26 补充:《左耳听风》中提到:

  1. 对于应用层上的分布式事务一致性

    • 吞吐量大的最终一致性方案:消息队列补偿等
    • 吞吐量小的强一致性方案:两阶段提交
  2. 数据存储层解决这个问题的方式 是通过一些像paxos、raft或是nwr这样的算法和模型来解决。 PS:这可能是因为存储层 主要是副本一致性问题
  3. 在实践中,还是尽量在数据存储层解决分布式事务问题。比如TiDB、OceanBase等,类似于一个无限容量的数据库 ==> 无需跨库操作 ==> 减少分布式事务问题。由此可见,一致性问题可以 “转嫁”