订单服务—分布式事务
本地事务
事务的基本性质
- 数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性或者独立性(Lsolation)和持久性(Durabilily),简称就是ACID原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败一致性:数据在事务的前后,业务整体一致转账:A:1000;B:1000;转 200;事务成功:A:800;B:1200隔离性:事务之间互相隔离持久性:一旦事务成功,数据一定会落盘在数据库
- 在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据库表,一旦有异常,我们可以很容易地整体回滚
undo和redo
- 其中原子性和持久性就要 靠undo和redo日志来实现
- 在数据库系统中,既有存放数据的文件,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log fileMySQL中的日志文件,有这么两种与事务有关:undo日志与redo日志
undo日志
- 数据库事务具备原子性(Atomicity),如果事务执行失败,需要把数据回滚;事务同时还具备持久性(Durability),事务对数据所做的变更就完全保存在了数据库,不能因为故障而丢失
- 原子性可以利用undo日志来实现
- undo日志的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到undo log;然后进行数据的修改,如果出现了错误或者用户执行了rollback语句,系统可以利用undo log中的备份将数据恢复到事务开始之前的状态数据库写入数据到磁盘之前,会把 数据先缓存在内存中 , 事务提交时才会写入磁盘中用undo log实现原子性和持久化的事务简单过程:假设有A、B两个数据,值分别为1,21、事务开始2、记录A=1到undo log3、修改A=34、记录B=2到undo log5、修改B=46、将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,21、事务开始2、记录A=1到undo log buffer3、修改A=34、记录A=3到redo log buffer5、记录B=2到undo log buffer6、修改B=47、记录B=4 到 redo log buffer8、将undo log 写入redo log9、将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类似的操作
本地事务失效问题
- 同一个对象内事务方法互调默认失效,原因:绕过了代理对象;事务使用代理对象来控制的
- 解决方案:使用同一个代理对象来调用事务方法引入 aop (aop-starter);引入 aspectj@EnableAspectJAutoProxy 注解:开启 aspectj 动态代理功能,以后所有的动态代理都是aspectj创建的,即使没有接口,也可以创建动态代理@EnableAspectJAutoProxy(exposePeoxy=true) :对外暴露代理对象用代理对象本类互调: AopContext.currentProxy()
分布式事务
- 分布式事务,就是指不是在单个服务或者单个数据库架构下,产生的事务:跨数据源的分布式事务跨服务的分布式事务综合情况
本地事务在分布式下的问题
- 事务保证:1、订单服务异常,库存锁定不运行,全部回滚,撤销操作2、库存服务事务自治,锁定失败全部回滚订单服务感受到,继续回滚3、库存服务锁定成功了,但是网络原因返回数据途中出现问题?4、库存服务锁定成功了,库存服务下面的逻辑发生故障,订单服务回滚,怎么处理?
- 问题:1、远程服务假失败:远程服务其实成功了,由于网络故障等没有返回;导致:订单回滚、库存却扣减了2、远程服务执行完成,下面的其他方法出现问题;导致已执行的远程请求,肯定不能回滚本地事务,在分布式系统下,只能控制住自己的回滚,控制不了其他服务的回滚分布式事务:最大原因 -- 网络问题 + 分布式机器利用消息队列实现最终一致性库存服务锁定成功后发送给消息队列(当前库存工作单),过段时间自动解锁,解锁时先查询订单的支付状态,解锁成功修改库存工作单详情项状态为已解锁
为什么有分布式事务
- 分布式系统经常出现的异常机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失...分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免
CAP定理与BASE理论
CAP定理
- CAP原则又叫CAP定理,值得是一个分布式系统中一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(partition),分区容错的意思是,区间通信可能失败;比如:一台服务器放在中国,另一台服务器放在美国,这就叫两个区,它们之间可能无法通信
- CAP原则指的是:这三个要素最多只能同时实现两点, 不可能三者兼得一般来说,分区容错无法避免,因此可以任务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 TransactionsMySQL 从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7开始支持,其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可用提交第二阶段:事务协调器要求每个数据库提交数据其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分消息XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低XA 性能不理想 ,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景XA 目前在商业数据库支持的比较理想, 在mysql数据库中支持的不太理想 ,mysql的XA实现,没有记录prepare阶段日志,主备切换会导致主库与备库数据不一致许多nosql也没用支持XA,这让XA的应用场景变得非常狭隘也有3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)
柔性事务 - TCC事务补偿型方案
- 刚性事务:遵循ACID原则,强一致性
- 柔性事务:遵循BASE理论,最终一致性与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致一阶段 prepare 行为:调用自定义的prepare逻辑二阶段 commit 行为:调用自定义的commit逻辑二阶段 rollback 行为:调用自定义的rollback逻辑所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中
柔性事务 - 最大努力通知方案
- 按规律进行通知, 不保证数据一定能通知成功,但会提供可查询操作接口进行核对 。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知,这种方案也是结合 MQ 进行实现;例如:通过 MQ 发送http请求,设置最大通知次数,达到通知次数后即不再通知案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
柔性事务 - 可靠消息+最终一致性方案(异步确保型)
- 这种实现方式的思路:其实是源于ebay,其基本的设计思路是将远程分布式事务拆分成一系列的本地事务
基本原理
一般分为事务的发起者A和事务的其它参与者B:
- 事务发起者A执行本地事务
- 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B
- 事务参与者B接收消息后执行本地事务
这个过程有点像你去学校食堂吃饭:
- 拿着钱去收银处,点一份红烧牛肉面,付钱
- 收银处给你发一个小票,还有一个号牌,你别把票弄丢了!
- 你凭小票和号牌一定能领到一份红烧牛肉面,不管要多久,重试几次
几个注意事项:
- 事务发起者A必须确保本地事务成功后,消息一定发送成功
- MQ必须确保消息正确投递和持久化保存
- 事务参与者B必须确保消息最终一定能消费,如果失败需要多次重试
- 事务B执行失败,会重试,但不会导致事务A回滚
本地消息表
- 为了避免消息发送失败或者丢失,我们可以把消息持久化到数据库中,实现时有简化版本和解耦合版本两种方式
- 1、简化版本事务发起者:开启本地事务执行事务相关业务发送消息到MQ把消息持久化到数据库,标记为已发送提交本地事务事务接收者:接收消息开启本地事务处理事务相关业务修改数据库消息状态为已消费提交本地事务额外的定时任务定时扫描表中超时未消费消息,重新发送优点:与TCC相比,实现方式较为简单,开发成本低缺点:数据一致性完全依赖于消息服务,因此消息服务必须可靠的需要处理被动业务方的幂等问题被动业务失败不会导致主动业务的回滚,而是重试被动的业务事务业务与消息发送业务耦合 、业务数据与消息表要在一起
- 2、独立消息服务为了解决上述问题,我们引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为,大概模型如下:一次消息发送的时序图:优点:解除了事务业务与消息相关业务的耦合缺点:实现起来比较复杂
RocketMQ事务消息
- RocketMQ本身自带了事务消息,可以保证消息的可靠性,原理其实就是自带了本地消息表,与上面的思路类似
RabbitMQ的消息确认
- RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制ack生产者确认机制:确保消息从生产者到达MQ不会有问题消息生产者发送消息到RabbitMQ时,可以设置一个异步的监听器,监听来自MQ的ACKMQ接收到消息后,会返回一个回执给生产者:消息到达交换机后路由失败,会返回失败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的执行很像,都是分两个阶段一阶段:执行本地事务,并返回执行结果二阶段:根据一阶段的结果,判断二阶段做法:提交或回滚
- 但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部由seata自己实现了,也就是说:我们写的 代码与本地事务时代码一样 ,无需手动处理分布式事务
- 那么,AT模式如何实现无代码侵入,如何帮我们自动实现二阶段代码的呢?一阶段:在一阶段,Seata会拦截业务SQL,首先解析SQL语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成 before image,然后执行业务SQL更新业务数据,在业务数据更新之后,再将其保存成 after image,最后获取全局行锁, 提交事务 ,以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性这里的 before image 和 after image 类似于数据库的undo和redo日志,但其实是用数据库模拟的二阶段提交:二阶段是提交的话,因为业务SQL在一阶段已经提交至数据库,所以seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可二阶段回滚:二阶段如果是回滚的话,seata就需要回滚一阶段已经执行的业务SQL,还原业务数据,回滚方式便是用before image还原业务数据;但在还原前首先要校验脏写,对比数据库当前业务数据和 after image,如果两份数据完全一致就说明没有脏写。可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转换人工处理不过因为有全局锁机制,所以可以降低出现脏写的概率AT模式的一阶段、二阶段提交和回滚均由seata框架自动生成,用户只需要编写业务SQL,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案
Seata的分布式交易解决方案
- 整体机制一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源二阶段:提交异步化,非常快速的完成回滚通过一阶段的回滚日志进行反向补偿
- 示例:TC:事务协调者维护全局和分支事务状态,驱动全局事务提交或回滚TM:事务管理器定义全局事务的范围,开始全局事务、提交或回滚全局事务RM:资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
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 - serverseata 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 - Serverregistry.conf :注册中心配置指明使用nacos作为配置中心,并指定nacos地址指明seata配置所在位置file.conf :指定事务日志存储在哪里启动Seata服务,启动成功并注册进nacos服务中心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} 复制代码引入file.conf文件关于将配置参数改为yml后的问题service { disableGlobalTransaction = false } 复制代码3.5、项目启动订单服务注册成功库存服务注册成功seata-server 日志
分布式事务效果演示(AT模式)
未开启分布式全局事务
- 模拟在生成订单、扣减库存之后的积分扣减过程中发生异常,本地事务的情况下订单服务会回滚,但扣减库存并没有回滚两个商品库存锁定 ,目前都是0,订单数据也清空了结果:订单成功回滚,但库存依旧被锁定 10和5
开启分布式全局事务
- 积分扣减过程中发生异常,订单和扣减库存都会回滚两个商品库存锁定为本地事务未全部回滚导致的 10和5结果:全部回滚,库存锁定量依旧是10和5
- 开启全局事务
可靠消息模式(最终一致性)演示(*)
- AT模式(强一致性,效率较慢),适用于非高并发场景,订单下单服务属于高并发,因此选用 可靠消息服务+最终一致性的方案
- 可靠消息模式下的下单流程
RabbitMQ延时队列(实现定时任务)
- 场景:未付款订单,超时一定时间后,系统自动取消订单并释放占用物品
- 常用解决方案:spring的schedule定时任务轮询数据库缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差解决:rabbitmq的消息TTL和死信Exchange结合
消息的TTL(Time To Live)
- 消息的TTL就是消息的 存活时间
- RabbitMQ可以对 队列 和 消息 分别设置TTL对队列设置就是队列没有消费者连着的保留时间, 也可以对每一个单独的消息做单独的设置。超过了这个时间,我认为这个消息就死了,称之为死信如果队列设置了,消息也设置了,那么会 取小的 。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置),这个消息死亡的时间有可能不一样(不同的队列设置),这里单讲单个消息的TTL,因为它才是实现延迟任务的关键,可以通过 设置消息的expiration字段或者x-message-ttl属性来设置时间 ,两者一样的效果
Dead Letter Exchanges(DLX)
- 一个消息在满足如下条件下,会进 死信路由 ,记住这里是路由而不是队列,一个路由可以对应很多队列(什么是死信)(basic.reject/basic.ack)requeue=false
- Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去
- 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机, 结合二者,就可以实现一个延时队列
延时队列实现(*)
- 第一种实现方式:给队列设置过期时间
- 第二种实现方式:给消息设置过期时间
演示场景
- 设计规范建议(基于事件模型的交换机设计)1、交换机命名:业务+exchange;交换机为Topic2、路由键:事件.需要感知的业务(可以不写)3、队列名称:事件+想要监听的服务名+queue4、绑定关系:事件.感知的业务创建所需的队列、交换机以及绑定关系第一种方式:AmqpAdmin(消息队列章节有详细使用方式)第二种方式:spring允许直接使用 @Bean 的方式声明 Binding、Queue、Exchange注意:自动创建队列、交换机 是在第一次连上mq, 并开启监听的时候
- 模拟下单成功、订单超时未支付、消息进入死信、取消订单
库存解锁
- 第一步:创建所需的交换机和队列
- 第二步:库存解锁的场景1、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消2、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚,之前锁定的库存就需要解锁;之前使用的 seata的AT模式,属于强一致性,效率较慢 ,因此使用 可靠消息模式 ,库存自动解锁库存锁定流程
定时关单
- 通过延时队列
- 存在的问题:由于网络原因,或者机器故障、卡顿,订单一分钟后开始关单,但这个过程耗时超过了一分钟,这时到达两分钟的节点,库存开始解锁。导致卡顿的订单永远无法解锁库存
- 解决:不仅仅依靠延时队列,关单成功追加主动通知解锁/** * 订单释放直接和库存释放进行绑定 * @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); 复制代码
- 可靠消息模式最终效果
如何保证消息可靠
消息丢失
- 消息发送出去,由于网络问题没有抵达服务器做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录数据库,采用定期扫描重发的方式做好日志记录,每个消息状态是否都被服务器收到都应该记录做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
- 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功,此时Broker尚未持久化完成,宕机
- 自动ACK的状态下,消费者收到消息,但没来得及消费然后宕机一定要开启手动ACK,消费成功才移除,失败或者没来得及处理就NoAck并重新入队
消息重复
- 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消费重新由unack变为ready,并发送给其他消费者
- 消息消费失败,由于重试机制,自动又将消息发送出去
- 成功消费,ack宕机时,消息由unack变为ready,Broker又重新发送消费者的业务消费接口应该设计为幂等性的,比如扣库存有工作单的状态标识使用 防重表 (redis/mysql),发送消息每一个都有业务的唯一标识,处理过的就不用处理rabbitMQ的每一个消息都有readLivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
消息积压
- 消费者宕机积压
- 消费者消费能力不足积压
- 发送者发送流量太大上线更多的消费者,进行正常消费上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
以上就是有关分布式的学习笔记,希望可以对大家学习分布式有帮助,喜欢的小伙伴可以帮忙转发+关注,感谢大家!后期也会不定时的更新干货的!