分布式高级篇(十)- 商城业务 - 分布式事务

825 阅读30分钟

订单服务 - 分布式事务

本地事务

事务的基本性质

  • 数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性或者独立性(Lsolation)和持久性(Durabilily),简称就是ACID

    • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
    • 一致性:数据在事务的前后,业务整体一致
      • 转账:A:1000;B:1000;转 200;事务成功:A:800;B:1200
    • 隔离性:事务之间互相隔离
    • 持久性:一旦事务成功,数据一定会落盘在数据库
  • 在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据库表,一旦有异常,我们可以很容易的整体回滚

    image-20210225084738297

undo和redo

  • 其中原子性和持久性就要靠undo和redo日志来实现

  • 在数据库系统中,既有存放数据的文件,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log file

    MySQL中的日志文件,有这么两种与事务有关:undo日志与redo日志

undo日志
  • 数据库事务具备原子性(Atomicity),如果事务执行失败,需要把数据回滚;事务同时还具备持久性(Durability),事务对数据所做的变更就完全保存在了数据库,不能因为故障而丢失

  • 原子性可以利用undo日志来实现

  • undo日志的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到undo log;然后进行数据的修改,如果出现了错误或者用户执行了rollback语句,系统可以利用undo log中的备份将数据恢复到事务开始之前的状态

    数据库写入数据到磁盘之前,会把数据先缓存在内存中事务提交时才会写入磁盘中

    用undo log实现原子性和持久化的事务简单过程:

    • 假设有A、B两个数据,值分别为1,2

    • 1、事务开始

    • 2、记录A=1到undo log

    • 3、修改A=3

    • 4、记录B=2到undo log

    • 5、修改B=4

    • 6、将undo log 写到磁盘

    • 7、将数据写到磁盘

    • 8、事务提交

      • 如何保证持久性?

        事务提交前,会把修改数据提交到磁盘,也就是说只要事务提交了,数据肯定持久化了

      • 如何保证原子性?

        每次对数据库修改,都会把修改前数据记录在undo log,那么需要回滚时,可以读取undo log,恢复数据

        若系统在7和8直接崩溃,此时事务并未提交,需要回滚,而undo log已经被持久化,可以根据undo log来恢复数据

        若系统在7之前崩溃,此时数据并未持久化到磁盘,依然保持在事务之前的状态

  • 缺陷:每个事务提交前将数据和undo log写入磁盘,这样会导致大量的磁盘IO,因此性能很低

    如果能够将数据缓存一段时间,就能减少IO提高性能,但是这样就会丧失事务的持久性,因此引入了另外一种机制来实现持久化,即redo log

redo日志
  • 和undo log相反,redo log记录的是新数据的备份,在事务提交之前,只要redo log持久化即可,不需要将数据持久化(异步,数据可以在事务提交以后异步写入磁盘),减少了IO的次数

  • Undo + Redo事务的简化过程

    • 假设有A、B两个数据,值分别为1,2

    • 1、事务开始

    • 2、记录A=1到undo log buffer

    • 3、修改A=3

    • 4、记录A=3到redo log buffer

    • 5、记录B=2到undo log buffer

    • 6、修改B=4

    • 7、记录B=4 到 redo log buffer

    • 8、将undo log 写入redo log

    • 9、将redo log 写入磁盘

    • 10、事务提交

  • 安全和性能问题

    • 如何保证原子性?

      如果在事务提交前故障,通过undo log日志恢复数据,如果undo log都还没写入,那么数据就在尚未持久化,无需回滚

    • 如何保证持久化

      大家会发现,这里并没有出现数据的持久化,因为数据已经写入redo log,而redo log持久化到了硬盘,因此只有到了步骤9以后,事务是可以提交的

    • 内存中的数据库数据何时持久化到磁盘

      因为redo log已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交后也会将内存数据刷入磁盘(也可以按照设定的频率刷新内存数据到磁盘中)

      redo log何时写入磁盘

      redo log会在事务提交之前,或者redo log buffer 满了的时候写入磁盘

  • 这里存在两个问题:

    • 问题1:之前写undo和数据库数据写到硬盘,现在是写undo和redo到磁盘,似乎减少了IO次数
      • 数据库数据写入是随机IO,性能很差
      • redo log 在初始化时会开辟一段连续的空间,写入是顺序IO,性能很好
      • 实际上undo log 并不是直接写入磁盘,而是先写入到redo log buffer中,当redo log持久化时,undo log就同时持久化到硬盘了
      • 因此事务提交前,只需要对redo log持久化即可
      • 另外,redo log并不是写入一次就持久化一次,redo log在内存中也有自己的缓冲池;redo log buffer,每次写redo log都是写入到buffer,在提交时一次性持久化到磁盘,减少IO次数
    • 问题2:redo log个数据是写入内存buffer中,当buffer满或者事务提交时,将buffer数据写入磁盘;redo log中记录的数据,有可能包含尚未提交事务,如果此时数据库崩溃,那么如何完成数据恢复?
      • 数据恢复的两种策略:
        • 恢复时,只重做已经提交了的事务
        • 恢复时,重做所有事务包括未提交的事务和回滚了的事务,然后通过undo log回滚那些未提交的事务
      • InnoDB引擎采用的第二种方案,因此undo log要在redo log前持久化

事务的隔离级别

  • READ UNCOMMITTED(读未提交)

    该隔离级别的事务会读到其它提交事务的数据,此现象也称之为脏读

  • READ COMMITTED(读提交)

    一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读的问题,Oracle 和 SQLServer 的默认隔离级别

  • REPEATABLE READ(可重复读)

    该隔离级别是MySQL默认的隔离级别,在同一个事务里,select的结果是事务开始时间点的状态,因此,同样的select操作读到的结果会是一致的,但是,会有幻读现象;MySQL 的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读

  • SERIALIZABLE(序列化)

    在该隔离级别下的事务都是串行顺序执行的,MySQL 数据库的 InnoDB引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重复读和幻读问题

事务的传播行为

  • 1、PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置

  • 2、PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行

  • 3、PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常

  • 4、PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务

  • 5、PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起

  • 6、PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常

  • 7、PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

    image-20210225093006990

本地事务失效问题

  • 同一个对象内事务方法互调默认失效,原因:绕过了代理对象;事务使用代理对象来控制的

  • 解决方案:使用同一个代理对象来调用事务方法

    • 引入 aop (aop-starter);引入 aspectj

    • @EnableAspectJAutoProxy注解:开启 aspectj 动态代理功能,以后所有的动态代理都是aspectj创建的,即使没有接口,也可以创建动态代理

    • @EnableAspectJAutoProxy(exposePeoxy=true):对外暴露代理对象

    • 用代理对象本类互调:AopContext.currentProxy()

      image-20210225094028669

分布式事务

  • 分布式事务,就是指不是在单个服务或者单个数据库架构下,产生的事务:
    • 跨数据源的分布式事务
    • 跨服务的分布式事务
    • 综合情况

本地事务在分布式下的问题

image-20210224172911522

  • 事务保证:

    • 1、订单服务异常,库存锁定不运行,全部回滚,撤销操作
    • 2、库存服务事务自治,锁定失败全部回滚订单服务感受到,继续回滚
    • 3、库存服务锁定成功了,但是网络原因返回数据途中出现问题?
    • 4、库存服务锁定成功了,库存服务下面的逻辑发生故障,订单服务回滚,怎么处理?
  • 问题:

    • 1、远程服务假失败:远程服务其实成功了,由于网络故障等没有返回;导致:订单回滚、库存却扣减了
    • 2、远程服务执行完成,下面的其他方法出现问题;导致已执行的远程请求,肯定不能回滚

    本地事务,在分布式系统下,只能控制住自己的回滚,控制不了其他服务的回滚

    分布式事务:最大原因 -- 网络问题 + 分布式机器

    利用消息队列实现最终一致性

    库存服务锁定成功后发送给消息队列(当前库存工作单),过段时间自动解锁,解锁时先查询订单的支付状态,解锁成功修改库存工作单详情项状态为已解锁

为什么有分布式事务

  • 分布式系统经常出现的异常

    机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失...

    分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免

CAP定理与BASE理论

CAP定理
  • CAP原则又叫CAP定理,值得是一个分布式系统中

    • 一致性(Consistency):
      • 在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)
    • 可用性(Availability):
      • 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
    • 分区容错性(Partition tolerance):
      • 大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(partition),分区容错的意思是,区间通信可能失败;比如:一台服务器放在中国,另一台服务器放在美国,这就叫两个区,它们之间可能无法通信
  • CAP原则指的是:这三个要素最多只能同时实现两点,不可能三者兼得

    image-20210225103823387

    一般来说,分区容错无法避免,因此可以任务CAP的P总是成立。CAP定理告诉我们,剩下的C和A无法同时做到

  • 分布式系统中实现一致性的raft算法

    raft算法解读

面临的问题
  • 对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可以性达到99.9999%(N个9),即保证P和A,舍弃C
BASE理论
  • 是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性
  • BASE是指
    • 基本可用(Basically Available)
      • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用
        • 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或者断网故障),查询结果的响应时间增加到了1~2秒
        • 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面
    • 软状态(Soft State)
      • 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现,MySQL replication 的异步复制也是一种体现
    • 最终一致性(Eventual Consistency)
      • 最终一致性是指系统中的所有数据副本经过一定时间后,最终能到达到一致的状态;弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况
强一致性、弱一致性、最终一致性
  • 从客户端角度,多进程并发访问时,更新过的数据在不同的进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性

分布式事务的几种方案

2PC模式:两阶段提交
  • 数据库支持的2PC【2 phase commit 二阶提交】,又叫做 XA Transactions

    MySQL 从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7开始支持,其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:

    • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可用提交
    • 第二阶段:事务协调器要求每个数据库提交数据

    其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分消息

    image-20210225131638333

    • XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低
    • XA 性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景
    • XA 目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致
    • 许多nosql也没用支持XA,这让XA的应用场景变得非常狭隘
    • 也有3PC,引入了超时机制(无论协调者还是参与者,在想对方发送请求后,若长时间未收到回应则做出相应处理)
柔性事务 - TCC事务补偿型方案
  • 刚性事务:遵循ACID原则,强一致性

  • 柔性事务:遵循BASE理论,最终一致性

    与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致

    image-20210225132640436

    一阶段 prepare 行为:调用自定义的prepare逻辑

    二阶段 commit 行为:调用自定义的commit逻辑

    二阶段 rollback 行为:调用自定义的rollback逻辑

    所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中

柔性事务 - 最大努力通知方案
  • 按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知,这种方案也是结合 MQ 进行实现;例如:通过 MQ 发送http请求,设置最大通知次数,达到通知次数后即不再通知

    案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调

柔性事务 - 可靠消息+最终一致性方案(异步确保型)
  • 这种实现方式的思路:其实是源于ebay,其基本的设计思路是将远程分布式事务拆分成一系列的本地事务
基本原理

一般分为事务的发起者A和事务的其它参与者B:

  • 事务发起者A执行本地事务
  • 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B
  • 事务参与者B接收消息后执行本地事务

image-20210301111632799

这个过程有点像你去学校食堂吃饭:

  • 拿着钱去收银处,点一份红烧牛肉面,付钱
  • 收银处给你发一个小票,还有一个号牌,你别把票弄丢了!
  • 你凭小票和号牌一定能领导一份红烧牛肉面,不管要多久,重试几次

几个注意事项:

  • 事务发起者A必须确保本地事务成功后,消息一定发送成功
  • MQ必须确保消息正确投递和持久化保存
  • 事务参与者B必须确保消息最终一定能消费,如果失败需要多次重试
  • 事务B执行失败,会重试,但不会导致事务A回滚
本地消息表
  • 为了避免消息发送失败或者丢失,我们可以把消息持久化到数据库中,实现时有简化版本和解耦合版本两种方式

  • 1、简化版本

    image-20210301112806065

    事务发起者:

    • 开启本地事务
    • 执行事务相关业务
    • 发送消息到MQ
    • 把消息持久化到数据库,标记为已发送
    • 提交本地事务

    事务接收者:

    • 接收消息
    • 开启本地事务
    • 处理事务相关业务
    • 修改数据库消息状态为已消费
    • 提交本地事务

    额外的定时任务

    • 定时扫描表中超时未消费消息,重新发送

    优点:

    • 与TCC相比,实现方式较为简单,开发成本低

    缺点:

    • 数据一致性完全依赖于消息服务,因此消息服务必须可靠的
    • 需要处理被动业务方的幂等问题
    • 被动业务失败不会导致主动业务的回滚,而是重试被动的业务
    • 事务业务与消息发送业务耦合、业务数据与消息表要在一起
  • 2、独立消息服务

    为了解决上述问题,我们引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为,大概模型如下:

    image-20210301113446201

    一次消息发送的时序图:

    image-20210301113847334

    image-20210301113912564

    优点:

    • 解除了事务业务与消息相关业务的耦合

    缺点:

    • 实现起来比较复杂
RocketMQ事务消息
  • RocketMQ本身自带了事务消息,可以保证消息的可靠性,原理其实就是自带了本地消息表,与上面的思路类似
RabbitMQ的消息确认
  • RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制ack

    • 生产者确认机制:确保消息从生产者到达MQ不会有问题

      消息生产者发送消息到RabbitMQ时,可以设置一个异步的监听器,监听来自MQ的ACK

    • MQ接收到消息后,会返回一个回执给生产者:

      • 消息到达交换机后路由失败,会返回失败ACK
      • 消息路由成功,持久化失败,会返回失败ACK
      • 消息路由成功,持久化成功。会返回成功ACK
    • 生产者提前编写好不同回执的处理方式

      • 失败回执:等待一定时间后重新发送
      • 成功回执:记录日志等行为
    • 消费者确认机制:确保消息能够被消费者正确消费

      • 消费者需要在监听队列的时候手动ACK确认
      • RabbitMQ把消息投递给消费者后,会等待消费者ACK,接收到ACK后才删除消息。如果没有接收到ACK消息会一直保留在服务端,如果消费者断开连接或者异常后,消息会投递给其它消费者
      • 消费者处理完消息,提交事务后,手动ACK,如果执行过程中抛出异常,则不会ACK,业务处理失败,等待下一条消息
  • 经过上面的两种确认机制,可以确保从消息生产者到消费者的消息安全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最终一致性

消息事务优缺点
  • 优点:
    • 业务相对简单,不需要编写三个阶段业务
    • 是多个本地事务的结合,因此资源锁定周期短,性能好
  • 缺点:
    • 代码侵入
    • 依赖于MQ的可靠性
    • 消息发起者可以回滚,但消息参与者无法引起事务回滚
    • 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况
AT模式
  • Seata开源了AT模式,AT模式是一种无侵入的分布式事务解决方案,可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题
  • 在AT模式下,用户只需关注自己的业务SQL,用户的业务SQL作为一阶段,seata框架会自动生成事务的二阶段提交和回滚操作
  • 详细看Seata介绍

Seata

  • Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务;Seata 将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案

    Seata官方文档地址

AT模式

  • 和TCC的执行很像,都是分两个阶段

    一阶段:执行本地事务,并返回执行结果

    二阶段:根据一阶段的结果,判断二阶段做法:提交或回滚

    image-20210301145110945

  • 但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部由seata自己实现了,也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务

  • 那么,AT模式如何实现无代码侵入,如何帮我们自动实现二阶段代码的呢?

    • 一阶段:在一阶段,Seata会拦截业务SQL,首先解析SQL语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成 before image,然后执行业务SQL更新业务数据,在业务数据更新之后,再将其保存成 after image,最后获取全局行锁,提交事务,以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性

      这里的 before image 和 after image 类似于数据库的undo和redo日志,但其实是用数据库模拟的

      image-20210301150443742

    • 二阶段提交:二阶段是提交的话,因为业务SQL在一阶段已经提交至数据库,所以seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可

    • 二阶段回滚:二阶段如果是回滚的话,seata就需要回滚一阶段已经执行的业务SQL,还原业务数据,回滚方式便是用before image还原业务数据;但在还原前首先要校验脏写,对比数据库当前业务数据和 after image,如果两份数据完全一致就说明没有脏写。可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转换人工处理

      image-20210301151039448

      不过因为有全局锁机制,所以可以降低出现脏写的概率

      AT模式的一阶段、二阶段提交和回滚均由seata框架自动生成,用户只需要编写业务SQL,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案

Seata的分布式交易解决方案

  • 整体机制

    • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
    • 二阶段:
      • 提交异步化,非常快速的完成
      • 回滚通过一阶段的回滚日志进行反向补偿
  • 示例:

    • TC:事务协调者

      维护全局和分支事务状态,驱动全局事务提交或回滚

    • TM:事务管理器

      定义全局事务的范围,开始全局事务、提交或回滚全局事务

    • RM:资源管理器

      管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

    image-20210225140206470

Seata 整合(*)

  • 1、每一个微服务 必须先创建 undo_log 表,记录回滚日志

    -- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
    CREATE TABLE `undo_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `branch_id` bigint(20) NOT NULL,
      `xid` varchar(100) NOT NULL,
      `context` varchar(128) NOT NULL,
      `rollback_info` longblob NOT NULL,
      `log_status` int(11) NOT NULL,
      `log_created` datetime NOT NULL,
      `log_modified` datetime NOT NULL,
      `ext` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    
  • 2、安装事务协调器:seata - server

    seata 1.2下载地址

  • 3、整合

    • 3.1、导入依赖

       <!--分布式事务管理 Seata-->
       <dependency>
       	<groupId>com.alibaba.cloud</groupId>
       	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
       	<exclusions>
       		<exclusion>
       			<groupId>io.seata</groupId>
       			<artifactId>seata-spring-boot-starter</artifactId>
       		</exclusion>
       	</exclusions>
       </dependency>
       <dependency>
      	<groupId>io.seata</groupId>
       	<artifactId>seata-spring-boot-starter</artifactId>
       	<version>1.2.0</version>
       </dependency>
      
    • 3.2、解压并启动 Seata - Server

      • image-20210225154415824

      • registry.conf :注册中心配置

        指明使用nacos作为配置中心,并指定nacos地址

        image-20210225154255655

        指明seata配置所在位置

        image-20210225154343335

      • file.conf:指定事务日志存储在哪里

        image-20210225154520774

      • 启动Seata服务,启动成功并注册进nacos服务中心

        image-20210225154603043

        image-20210225154647982

        image-20210225154715792

    • 3.3、使用注解 @GlobalTransactional

      运行示例

    • 3.4、修改想要加入全局事务的微服务的yml文件(order订单服务和ware库存服务)

      seata:
        #是否开启spring-boot 自动装配
        enabled: true
        #自定义事务组名称
        tx-service-group: order-mall-group
        #是否开启数据源自动代理
        enable-auto-data-source-proxy: true
        service:
          vgroupMapping:
            order-mall-group: default
          grouplist:
            #服务器上seata的地址
            default: localhost:8091
          enable-degrade: false
        application-id: ${spring.application.name}
      

      image-20210226153721996

      引入file.conf文件

      关于将配置参数改为yml后的问题

      service {
        disableGlobalTransaction = false
      }
      

      image-20210226153937219

    • 3.5、项目启动

      • 订单服务注册成功

        image-20210226154545108

      • 库存服务注册成功

        image-20210226154653384

      • seata-server 日志

        image-20210226154623450

分布式事务效果演示(AT模式)

未开启分布式全局事务
  • 模拟在生成订单、扣减库存之后的积分扣减过程中发生异常,本地事务的情况下订单服务会回滚,但扣减库存并没有回滚

    两个商品库存锁定 ,目前都是0,订单数据也清空了

    结果:订单成功回滚,但库存依旧被锁定 10和5

开启分布式全局事务
  • 积分扣减过程中发生异常,订单和扣减库存都会回滚

    两个商品库存锁定为本地事务未全部回滚导致的 10和5

    结果:全部回滚,库存锁定量依旧是10和5

  • 开启全局事务

    image-20210226160349698

可靠消息模式(最终一致性)演示(*)

  • AT模式(强一致性,效率较慢),适用于非高并发场景,订单下单服务属于高并发,因此选用 可靠消息服务+最终一致性的方案

  • 可靠消息模式下的下单流程

    image-20210301154113473

    image-20210301154146259

RabbitMQ延时队列(实现定时任务)

  • 场景:未付款订单,超时一定时间后,系统自动取消订单并释放占用物品

    image-20210301155207782

  • 常用解决方案:

    • spring的schedule定时任务轮询数据库

    缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差

    解决:rabbitmq的消息TTL和死信Exchange结合

消息的TTL(Time To Live)
  • 消息的TTL就是消息的存活时间
  • RabbitMQ可以对队列消息分别设置TTL
    • 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我认为这个消息就死了,称之为死信
    • 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置),这个消息死亡的时间有可能不一样(不同的队列设置),这里单讲单个消息的TTL,因为它才是实现延迟任务的关键,可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者一样的效果
Dead Letter Exchanges(DLX)
  • 一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列(什么是死信)
    • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false,也就是说不会被再次放在队列里,被其他消费者使用;(basic.reject/basic.ack)requeue=false
    • 上面的消息的TTL到了,消息过期
    • 队列的长度限制满了,排在前面的消息会被丢弃或者扔到死信路由上
  • Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去
  • 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,就可以实现一个延时队列
延时队列实现(*)
  • 第一种实现方式:给队列设置过期时间

    image-20210301163516329

  • 第二种实现方式:给消息设置过期时间

    image-20210301163921951

演示场景

image-20210301164321913

  • 设计规范建议(基于事件模型的交换机设计)

    • 1、交换机命名:业务+exchange;交换机为Topic
    • 2、路由键:事件.需要感知的业务(可以不写)
    • 3、队列名称:事件+想要监听的服务名+queue
    • 4、绑定关系:事件.感知的业务

    image-20210302083842103

    创建所需的队列、交换机以及绑定关系

    • 第一种方式:AmqpAdmin(消息队列章节有详细使用方式)

    • 第二种方式:spring允许直接使用 @Bean 的方式声明 Binding、Queue、Exchange

      注意:自动创建队列、交换机 是在第一次连上mq,并开启监听的时候

      image-20210302100310357

      image-20210302100343028

      image-20210302100402511

  • 模拟下单成功、订单超时未支付、消息进入死信、取消订单

    image-20210302110309491

库存解锁

  • 第一步:创建所需的交换机和队列

    image-20210302164519559

    image-20210302164534137

  • 第二步:库存解锁的场景

    • 1、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消

    • 2、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚,之前锁定的库存就需要解锁;之前使用的seata的AT模式,属于强一致性,效率较慢,因此使用可靠消息模式,库存自动解锁

    • 库存锁定流程

      image-20210302170529598

定时关单

  • 通过延时队列

    image-20210303110124676

  • 存在的问题:由于网络原因,或者机器故障、卡顿,订单一分钟后开始关单,但这个过程耗时超过了一分钟,这时到达两分钟的节点,库存开始解锁。导致卡顿的订单永远无法解锁库存

    image-20210303110143233

  • 解决:不仅仅依靠延时队列,关单成功追加主动通知解锁

    image-20210303105641599

     /**
     * 订单释放直接和库存释放进行绑定
     * @return
     */
     @Bean
     public Binding orderReleaseOtherBinding(){
     	return new Binding("stock.release.stock.queue",
     	Binding.DestinationType.QUEUE,
     	"order-event-exchange",
     	"order.release.other.#",
     null);
     }
     
     //关单成功,主动通知解锁库存
     rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderEntity);
    

    image-20210303112223196

  • 可靠消息模式最终效果

如何保证消息可靠

消息丢失
  • 消息发送出去,由于网络问题没有抵达服务器
    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录数据库,采用定期扫描重发的方式
    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录
    • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功,此时Broker尚未持久化完成,宕机
  • 自动ACK的状态下,消费者收到消息,但没来得及消费然后宕机
    • 一定要开启手动ACK,消费成功才移除,失败或者没来得及处理就NoAck并重新入队
消息重复
  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消费重新有unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack宕机时,消息由unack变为ready,Broker又重新发送
    • 消费者的业务消费接口应该设计为幂等性的,比如扣库存有工作单的状态标识
    • 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过的就不用处理
    • rabbitMQ的每一个消息都有readLivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
消息积压
  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大
    • 上线更多的消费者,进行正常消费
    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理