分布式解决方案

在分布式系统中,订单服务和库存服务在不同的服务器中

那么在用户下订单时,我们的服务应该怎么做呢?

  1. 先扣库存,再建订单

  2. 先建订单,再扣库存

这两种做法都有问题:

  • 先扣库存,在建订单的问题是

    • 库存扣减成功,但是订单没有新建成功(有问题,没法归还库存)
  • 先建订单,再扣库存的问题是:

      1. 订单创建成功,通知库存扣减失败,回滚创建订单(没有问题)
      1. 订单创建成功,库存扣减也成功了,但库存服务没法通知订单服务,导致订单服务认为库存服务扣减失败,回滚创建订单(有问题,订单没创建,但是库存被扣减了)

所以这里无论是先扣减库存还是后扣减库存,都会遇到库存扣减和订单表不一致的问题

那么我们应该怎么做呢?

解决这个问题的方法是使用分布式事务

事务

什么是事务

一组 sql 语句操作一个单元,组内所有的 sql 语句完成一个业务

如果这组 sql 全部执行成功,这个业务是有意义,否则任何一个 sql 执行失败,那么这个业务就是无意义的,组内执行成功的 sql 也是无意义

这是数据库应该会滚到最初的状态,这就是事务

为什么要用事务

  1. 如果执行失败了,需要回到最初的状态
  2. 在执行成功之前,对其他用户是不可见的

事务的四大特性

事务的四大特性是 ACID

  • 原子性(Atomicity
    • 不可分割,要么都成功,要么都失败
  • 一致性(Consistency
    • 指数据处于一种语义上有意义且正确的状态
    • 是对数据可见性的约束,保证在一个事务中的多次操作的数据数据中间状态对其他事务是不可见的
  • 隔离性(Isolation
    • 事务之间是隔离的,互不干扰
    • 如果不考虑隔离性,就会存在:脏读、不可重复读、幻读/虚读
    • mysql 的隔离级别有:读未提交,读已提交,可重复读,串行化
  • 持久性(Durability
    • 事务一旦提交,就是永久性的,不会回滚

如何理解事务的一致性,通过一个例子来说明:A 给 B 转账,会存在 3 状态:

  1. A 未转账,B 未收到
  2. A 已转账,B 未收到
  3. A 已转账,B 已收到

为什么会出现 3 种状态呢?因为 A 转账给 B,需要经过两个步骤:

  1. A 扣钱
  2. B 加钱

如果 A 扣钱成功,B 加钱失败,那么就会出现第 2 种状态,但第 2 种状态是不可接受的,因为 A 扣钱了,B 却没收到钱,这就不一致了

也就是说,如果 A 扣钱成功,B 加钱失败,那么 A 扣钱的操作应该回滚,回到第 1 种状态

这么说,那原子性和一致性有什么区别?

  • 原子性:关注的是状态,要么都成功,要么都失败
  • 一致性:关注的是数据的可见性,即中间状态不可见,只有最初状态和最终状态是可见的

ps:对于单机事务来说,大部分情况无法满足 AICD,不然怎么会有隔离级别呢?

分布式事务

分布式事务基本不能满足 AICD

为什么会出现分布式事务

  1. 网络问题:没有发送出去、发送出去了但没收到回复,以为出错了
    • 硬件问题
    • 网络抖动
    • 网络拥塞
  2. 程序错误
    • 代码错误
    • 宕机
      • 断电
      • 系统问题:磁盘满了,硬件坏了

CAP 理论

cap 理论是分布式系统的理论基石:

  • 一致性(Consistency
    • 更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这是分布式事务的一致性
    • 一致性在并发系统中不可避免
      • 对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题
      • 对于服务端来说,更新后如何复制到整个系统,以保证数据的一致性
  • 可用性(Availability
    • 服务一直可用,不出现访问超时或者不能访问的情况
  • 分区容错性(Partition Tolerance
    • 分布式系统在遇到某个节点或者网络故障时,仍然能够对外提供满足一致性和可用性的服务
    • 要求虽然是一个分布式系统,但看上去好像是一个整体
    • 如果分布式系统中出现一个或几个服务不可用,剩余的服务仍需满足外部系统的需求

如果是一个分布式系统,一定要满足:分区容错性

一致性和可用性是互斥的,为什么?

一致性可以通过锁来解决问题,但是锁会导致可用性降低,比如同步数据需要 1s,但是可用性的要求是不能超过 100ms,这就出现了矛盾

所以就出现了取舍策略:

  • CA:单机数据库,满足一致性和可用性
    • OracleMySQL
  • CP:解决的是一致性问题,数据的一致性很重要,当写入的时候一定要等到所有节点都成功
    • 比如 RedisMongoDBHBase
  • AP:解决的是可用性问题,如果读不到最新的数据,等一段时间在去读
    • Coach DBCassandraDynamoDB

base 理论

base 理论是对 cap 中一致性和可用性权衡的结果,来源于大规模互联网分布式实践的总结,是从 cap 中演化而来的

其核心思想是:即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性

  • 基本可用(Basically Available
    • 在出现不可预知故障的时候,允许损失部分可用性(这不等价于系统不可用)
      • 响应时间上的损失
      • 功能上的损失:比如降级
  • 软状态(Soft state
    • 中间状态,且中间状态不影响整体的可用性
  • 最终一致性(Eventual consistency
    • 经过一段时间同步后,最终达到一致的状态

还是用转钱举例子:

A 向 B 转账,告诉 B 钱转成功了,你过一会再查询一下

这时候就引入了中间状态(转账中),这个状态被称为软状态

告诉用户 2 小时后到账,如果没有到账,钱会回到 A 账户中

只要用户接受了这种方式,那么就可以达到最终一致性

分布式系统解决方案

在分布式系统中要保证同时成功和同时失败

常见的分布式事务解决方案:

  • 两阶段提交
  • TCC 补偿模式
  • 基于本地消息表实现最终一致性
  • 最大努力通知
  • 基于可靠消息最终一致性方案(最常用)

两阶段提交

两阶段提交又称为 2PC,是一个非常经典的中心化的原子提交协议

2PC 利用数据库的事务进行实现,有两个阶段:

  • 第一阶段: 投票阶段
    • 先调用服务,开启事务
  • 第二阶段: 提交阶段
    • 如果所有服务都成功,那么就提交事务
    • 如果有一个服务失败,那么就回滚事务

缺点:

  • 性能问题:数据库的事务是会被锁住的,对性能影响比较大
  • 单节点故障:一个节点出问题了,导致其他节点也会被卡住

TCC 分布式事务

TCC 分布式事务和 2PC 是一样的

2PC 是基于数据库的事务来实现的,而事务是会加锁的

TCC 就是基于业务实现锁,不使用数据库的事务,这样并发就会比较高

TCCtryconfirmcannel 的缩写

TCC 分布式事务步骤:

  1. 首先需要选择某种 TCC 分布式事务框架,各个微服务就会在这个 TCC 分布式事务框架中运行
  2. 然后原本微服务的一个接口,要改造为 3 个逻辑:try-confirm-cancel
    • 先是服务调用链路依次执行 try 函数
    • 如果都正常的话,TCC 分布式事务框架推进执行 confirm 函数,完成整个事务
    • 如果某个服务 try 函数有问题,TCC 分布式事务框架感知到之后,就会推荐执行各个微服务的 cancel 函数,撤销之前执行的各种操作

TCC 分布式事务说白了就是遇到下面情况时:

  • 某个服务的数据库宕机了
  • 某个服务自己挂了
  • 某个服务的 RedisElasticSearchMQ 等基础设施故障了
  • 某些资源不足了,比如说库存不够了

先来 try 一下,但不把业务逻辑完成,先试试看,各个服务能不能正常运转,能不能先冻结我需要的资源

如果 tryok,也就是说底层的数据库、RedisElasticSearchMQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一些库存)

最后,如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行?

  1. TCC 要记录一些分布式事务的活动日志,可以放在磁盘上,也可以放在数据库中。保存分布式事务运行的各个阶段的状态
  2. 万一某个服务的 cancelconfirm 逻辑执行一直失败?TCC 通过保存下来的日志不停的调用 cancel 或者 confirm,务必要它成功
  3. 如果代码没有什么 bug,有充足的测试的话,一般 try 函数正常的话,confirmcancel 是可以成功的
    • 业务逻辑一般是在 try
    • confirmcancel 只是确认和取消
  4. 万一实在不成功,这是一个小概率事件,那就用邮件通知进行人工干预

优点:

  1. 解决了跨服务的业务操作原子性问题,例如组合支付,订单减库存等场景
  2. TCC 的本质原理是把数据库的二阶段提交上升到微服务来实现,从而避免了数据库二阶段中锁冲突的长事务低性能风险
  3. TCC 异步性能高,它采用了 try 先检查,然后异步实现 confirm,真正提交是在 confirm

缺点:

  1. 对微服务侵入性强,微服务每个事务都必须实现 tryconfirmcancel 3 个方法,维护成本高
  2. 为了达到事务的一致性要求,tryconfirmcancel 接口必须实现幂等操作
  3. 由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个 TCC 事务时间拉长,建议采用 redis 的方式来实现
  4. TCC 需要通过锁来确保数据的一致性,加锁会导致性能不高

基于本地消息表的最终一致性

本地消息表最终一致性的一致性比 TCC 的一致性要弱一点,它是保证最终一致性,不像 TCC 同时成功或者同时失败

下图是基于 MQ 实现最终一致性:

  1. 订单服务执行自己的逻辑,执行完了之后,把库存扣减和通知的任务放入 MQ
  2. 然后各微服务不停的从 MQ 中拉取任务,执行任务
  3. 如果各微服务执行任务成功了,通知 MQ 把对应的任务删除
    • 各微服务执行失败了,不会通知 MQ 那么还会继续从 MQ 中拉取任务

这里有一个问题,当订单创建成功,但对应的微服务还没有执行完(比如库存扣减服务,通知服务),数据出现了不一致

这就是它的特性:虽然当前数据还没有一致,但最终一定会一致的

这时还有问题:

回到上面图,我们这里是先创建订单,再发送 MQ 消息,那么就有一种情况:

如果订单创建成功了,MQ 消息也发送成功了,但由于网络问题,订单服务没有收到 MQ 的回复,导致订单服务认为 MQ 消息发送失败,回滚了订单

解决这个问题的方法就是本地消息表的最终一致性

引入一张本地表,记录 MQ 的消息发送状态,用定时任务定时扫描这张表查看 MQ 发送状态,这样就可以解决上面的问题

定时扫描会导致消息重复发送给 MQ,所以各微服务需要实现幂等性

这种方案也是有缺点的:由于关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的

基于可靠消息的最终一致性

此方案和基于本地消息表的最终一致性类似,它用的是 RocketMQ 提供的事务消息

通过上面我们知道,发送消息是不可靠的

我们想要的效果是,消息发送出去后,只要本地服务没有确认,那发送出去的消息不会被消费(这就可以达到,如果本地服务出问题了,随时可取消消息)

此方法解决的是:本地发送出去的消息是可靠的

图中 half message 可以称为半消息,它是不可消费的,只有本地服务确认了,才会变成可消费的消息

比如图中第 4 步,也会遇到网络问题导致 commit/rollback 发送失败

为解决这个问题 Rocket MQ 提供了回查机制,即 Rocket MQ 会定时扫描消息表,如果发现消息状态是 commit/rollback,但是本地服务没有确认,那么就会回查本地服务,如果本地服务还是没有确认,那么就会重新发送 commit/rollback 消息

当本地服务收到回查消息后,查询本地事务状态,如果本地事务已经成功,那么就返回 commit,否则返回 rollback

这就保证了消息的可靠性

最大努力通知

此方案一般用来对接第三方服务,因为你不知道第三方服务是怎么实现的,所以只能尽力通知

比如支付宝支付,你支付成功了,但是支付宝支付回调通知你的时候,你的服务挂了,那么支付宝就会不停的通知你,直到你的服务恢复正常

但你的服务什么时候好,支付宝是不知道的,所以支付宝在通知你几次之后就不在通知了

这时候你说,那我怎么知道支付成功了呢?

所以支付宝还需要提供一个查询接口,你可以通过这个接口查询支付状态

就这事最大努力通知

ps:支付宝每次通知的时间不是固定的,它有自己的算法,比如第一次是隔 1s,第二次隔 2s,第三次隔 5s ...,前几次通知不成功,大概率是不会成功了,所以后面间隔的时间会越来越长

相关推荐
神秘打工猴1 小时前
Kafka 监控都有哪些?
分布式·kafka
三天不学习1 小时前
C# 中的记录类型简介 【代码之美系列】
后端·c#·微软技术·record·记录类型
任小永的博客1 小时前
VUE3+django接口自动化部署平台部署说明文档(使用说明,需要私信)
后端·python·django
凡人的AI工具箱1 小时前
每天40分玩转Django:Django类视图
数据库·人工智能·后端·python·django·sqlite
凡人的AI工具箱2 小时前
每天40分玩转Django:实操图片分享社区
数据库·人工智能·后端·python·django
Q_19284999062 小时前
基于Spring Boot的个人健康管理系统
java·spring boot·后端
liutaiyi82 小时前
Redis可视化工具 RDM mac安装使用
redis·后端·macos
Kobebryant-Manba2 小时前
kafka基本概念
分布式·学习·kafka
Q_19284999062 小时前
基于Springcloud的智能社区服务系统
后端·spring·spring cloud
xiaocaibao7772 小时前
Java语言的网络编程
开发语言·后端·golang