数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation) 和持久性(Durabilily),简称就是 ACID;
在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常, 我们可以很容易的整体回滚;
例如,买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败 一个事务开始,代表以下的所有操作都在同一个连接里面;
READ UNCOMMITTED(读未提交)
:该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。READ COMMITTED(读提交)
: 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重 复读问题,Oracle 和 SQL Server 的默认隔离级别。REPEATABLE READ(可重复读)
:该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间 点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。SERIALIZABLE(序列化)
: 在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式 加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。PROPAGATION_REQUIRED
:如果当前没有事务,就创建一个新事务,如果当前存在事务, 就加入该事务,该设置是最常用的设置。PROPAGATION_SUPPORTS
:支持当前事务,如果当前存在事务,就加入该事务,如果当 前不存在事务,就以非事务执行。PROPAGATION_MANDATORY
:支持当前事务,如果当前存在事务,就加入该事务,如果 当前不存在事务,就抛出异常。PROPAGATION_REQUIRES_NEW
:创建新事务,无论当前存不存在事务,都创建新事务。PROPAGATION_NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,就把当 前事务挂起。PROPAGATION_NEVER
:以非事务方式执行,如果当前存在事务,则抛出异常。PROPAGATION_NESTED
:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务, 则执行与 PROPAGATION_REQUIRED 类似的操作。在SpringBoot项目的同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。
分布式系统经常出现的异常 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失…
CAP 原则又称 CAP 定理,指的是在一个分布式系统中一致性(Consistency)、 可用性(Availability)、容错性(Partition tolerance)
一致性(Consistency)
:在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访 问同一份最新的数据副本)可用性(Availability)
: 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据 更新具备高可用性)分区容错性(Partition tolerance)
:大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。 分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务 器放在美国,这就是两个区,它们之间可能无法通信。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们, 剩下的 C 和 A 无法同时做到。
是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可 以采用适当的采取弱一致性,即最终一致性。
BASE 是指 基本可用(Basically Available) 、软状态( Soft State)、最终一致性( Eventual Consistency)
基本可用(Basically Available)
:基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、 功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。 例如
软状态( Soft State)
:软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体 现。mysql replication 的异步复制也是一种体现。
最终一致性( Eventual Consistency)
:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状 态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性
。如果能容忍后续的部分或者全部访问不到,则是弱一致性
。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性
NWR是一种在分布式存储系统中用于控制一致性级别的一种策略。在亚马逊的云存储系统中,就应用NWR来控制一致性。
NWR值的不同组合会产生不同的一致性效果,当W+R>N的时候,整个系统对于客户端来讲能保证强一致性。
以常见的N=3、W=2、R=2为例:
在分布式系统中,数据的单点是不允许存在的。即线上正常存在的备份数量N设置1的情况是非常危险的,因为一旦这个备份发生错误,就 >可能发生数据的永久性错误。假如我们把N设置成为2,那么,只要有一个存储节点发生损坏,就会有单点的存在。所以N必须大于2。
N越高,系统的维护和整体 成本就越高。工业界通常把N设置为3。
在上图中,如果R+W>N,则读取操作和写入操作成功的数据一定会有交集(如图中的Node2),这样就可以保证一定能够读取到最新版本的更新数据,数据的强一致性得到了保证。在满足数据一致性协议的前提下,R或者W设置的越大,则系统延迟越大,因为这取决于最慢的那份备份数据的响应时间。思考:读取到两个数据,怎么去判断哪一个是最新的值?
因为成功写和成功读集合可能不存在交集,这样读操作无法读取到最新的更新数值,也就无法保证数据的强一致性。
Gossip 协议也叫 Epidemic 协议 (流行病协议)。原本用于分布式数据库中节点同步数据使用,后被广泛用于数据库复制、信息扩散、集群成员身份确认、故障探测等。
从 gossip 单词就可以看到,其中文意思是八卦、流言等意思,我们可以想象下绯闻的传播(或者流行病的传播);gossip 协议的工作原理就类似于这个。gossip 协议利用一种随机的方式将信息传播到整个网络中,并在一定时间内使得系统内的所有节点数据一致。Gossip 其实是一种去中心化思路的分布式协议,解决状态在集群中的传播和状态一致性的保证两个问题。
Gossip 协议的消息传播方式有两种:反熵传播
和 谣言传播
Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式、推/拉模式
综上所述,我们可以得出 Gossip 是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播。因为是指数级传播,整体传播速度非常快。
Gossip 协议由于以上的优缺点,所以适合于 AP 场景的数据一致性处理,常见应用有:P2P 网络通信、Redis Cluster、Consul。
Paxos协议其实说的就是Paxos算法, Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
自Paxos问世以来就持续垄断了分布式一致性算法,Paxos这个名词几乎等同于分布式一致性。Google的很多大型分布式系统都采用了Paxos算法来解决分布式一致性问题,如Chubby、Megastore以及Spanner等。开源的ZooKeeper,以及MySQL 5.7推出的用来取代传统的主从复制的MySQL Group Replication等纷纷采用Paxos算法解决分布式一致性问题。然而,Paxos的最大特点就是难,不仅难以理解,更难以实现。
在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。
注:这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命令(command)。。。根据应用场景不同,某个数据的值有不同的含义。
在之前的2PC 和 3PC的时候在一定程度上是可以解决数据一致性问题的. 但是并没有完全解决就是协调者宕机的情况
Paxos的版本有: Basic Paxos , Multi Paxos, Fast-Paxos, 具体落地有Raft 和zk的ZAB协议
basic paxos流程一共分为4个步骤:
针对活锁问题解决起来非常简单: 只需要在每个Proposer再去提案的时候随机加上一个等待时间即可.
针对basic Paxos是存在一定得问题,首先就是流程复杂,实现及其困难, 其次效率低(达成一致性需要2轮 RPC调用),针对basic Paxos流程进行拆分为选举
和复制
的过程.
Multi-Paxos在实施的时候会将Proposer,Acceptor和Learner的角色合并统称为“服务器”。因此,最后只有“客户端”和“服务器”。
Paxos 是论证了一致性协议的可行性,但是论证的过程据说晦涩难懂,缺少必要的实现细节,而且工程实现难度比较高, 广为人知实现只有 zk 的实现 zab 协议,zab 协议详情参考该文章
Paxos协议的出现为分布式强一致性提供了很好的理论基础,但是Paxos协议理解起来较为困难,实现比较复杂。然后斯坦福大学RamCloud项目中提出了易实现,易理解的分布式一致性复制协议 Raft。Java,C++,Go 等都有其对应的实现之后出现的Raft相对要简洁很多。
分布式一致性raft算法动画演示:http://thesecretlivesofdata.com/raft/
raft是一个实现分布式一致性的协议,主要有领导选举、日志复制两个机制维持数据的一致性
每个节点都有三个状态:
以及两个时间:
举leader完成后,更新数据时,接下来所有的数据都要先给leader发送,leader先在自己的log中写入更新数据变化,然后leader在下一次向follower发送心跳的时候,也会把日志信息发送给follower,follower更新完成后,会向leader发送确认信息,当leader接收到超过一半的确认信息后,leader会进行提交,写入数据,然后告诉follower也提交写入。
还有一种情况,就是当发生分区问题,导致节点断开的情况
例如:5台机器分布在2个机房,1,2节点一个,3,4,5节点一个,两个机房中间用网线来连接,因为网线断裂问题,导致两个机房局域网隔离,这个时候每个机房会根据leader选举,重新选择出自己的leader,导致生成两个leader的问题,网络好了之后,会怎么处理?
对于1,2结点那个leader,当有客户端请求数据,leader更新log后收不到大多数follower的ack,所以改log不成功,一直保存不成功
对于3,4,5结点的leader:收到消息后更新log并且收到follower的ack过半,成功保存。
此时网络又通了,以更高轮选举的leader为主,退位一个leader。那1,2节点日志都回滚,同步新leader的log。这样就都一致性了
数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。
MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。 其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务 中的那部分信息。
阶段一:
执行事务(写本地的Undo/Redo日志)
提交操作,并开始等待各参与者的响应阶段二:
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务
阶段一:
执行事务(写本地的Undo/Redo日志)
提交操作,并开始等待各参与者的响应阶段二:
XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较低。
三阶段提交协议出现背景:一致性协议中设计出了二阶段提交协议(2PC),但是2PC设计中还存在缺陷,于是就有了三阶段提交协议,这便是3PC的诞生背景。
3PC,全称"three phase commit",是2PC的改进版,将2PC的“提交事务请求”过程一分为二,共形成了由CanCommit.PreCommit和doCommit三个阶段组成的事务处理协议。
三阶段提交升级点(基于二阶段):
简单讲:就是除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit.DoCommit三个阶段
。
第一阶段(CanCommit阶段):类似于2PC的准备(第一)阶段。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
第二阶段(PreCommit阶段):协调者根据参生者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能。
第三阶段(doCqmmit阶段):该阶段进行真正的事务提交,也可以分为执行提交和中断事务两种情况。
注意:—旦进入阶段三,可能会出现2种故障
:
如果出现了任—一种情况,最终都会导致参与者无法收到doCommit请求或者abort请求,针对这种情况参与者都会在等待超时之后,继续进行事务提交
但是3PC协议并没有完全解决数据一致问题,只是比2PC要减少一些数据一致性的几率。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
Seata的AT模型
基本流程图:
一阶段,首先TM会向TC提交开启事务,TM会调用各个微服务,执行各个微服务的分支事务,RM会先向TC注册分支事务,然后再去执行分支事务,执行完毕之后直接提交事务,但是在事务提交之前会进行一个快照备份,然后再去向TC报告事务的状态。TM发现事务结束了,通知TC,然后TC会去检查分支事务的状态,如果各个分支事务都成功,在二阶段,则异步进行快照备份的删除,如果有一个失败,则进行备份文件的恢复操作,然后异步的去删除快照备份文件。
阶段一RM的工作:
在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题。
当事务1执行事务的时候,会先去获取到DB锁,保存快照,然后再去执行业务sql,提交完事务,会释放DB锁,接着应该通知TC,等待快照的恢复或者删除,但是因为整个过程的操作不是原子性的,所以在事务1释放DB锁之后,可能会被另一个事务2所抢占到,执行事务2的事务,在事务2执行完操作之后,更改完数据,提交事务之后,释放DB锁,再次被事务1所抢占到,经过TC判别,要进行快照的恢复,就导致事务2修改的数据被事务1回滚的事务进行覆盖,出现脏写的问题。
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
当事务1执行事务的时候,会先去获取到DB锁,保存快照,然后再去执行业务sql,sql执行完毕之后,会先去拿到全局锁,全局锁是由TC去记录的,然后再提交完事务,释放DB锁,接着应该通知TC,等待快照的恢复或者删除,但是因为整个过程的操作不是原子性的,所以在事务1释放DB锁之后,可能会被另一个事务2所抢占到,执行事务2的事务,在事务2进行获取DB锁,保存快照,执行sql,在执行sql之后,也会去获取全局锁,但是因为在TC中记录的全局锁,被事务1所获取,就会进行重试,默认是30次,每次间隔10毫秒,如果时间超时之后,便会进行事务的回滚,释放DB锁,之后被事务1获取到锁,根据TC的状态进行快照的恢复或者删除,利用全局锁,就保证了多线程环境下同时只有一条事务执行成功,不会出现脏写的问题。
如果事务1是被Seata管理的事务,事务2没有被Seata管理,那么事务1在保存快照的时候会保存两份快照一个是修改之前的,一个是修改之后的,当发生快照数据恢复的时候,会先比对数据库的数据与快照备份修改之后的数据是否一致,如果一致的话则照常恢复数据,不是的话则记录异常发送警告。
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
• 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初始余额:
余额充足,可以冻结:
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
• 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
• 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
我们从以下几个方面来对比四种实现:
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
快速开始:http://seata.io/zh-cn/docs/user/quickstart.html
要执行下单,
我们只需要使用一个 @GlobalTransactional 注解在业务方法上:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
SEATA AT模式需要 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;
pom
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
开始应用
从 https://github.com/seata/seata/archive/v0.7.1.zip 下载服务器软件包senta-server-0.7.1,将其解压缩。作为TC
在大事务的入口标记注解@GlobalTransactional开启全局事务,并且每个小事务标记注解@Transactional
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
}
地址:https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata
还需要注入 DataSourceProxy因为 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚,以下数据源代理的方式在springboot2.0版本以下可以使用,高版本会出现循环依赖
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
* @param druidDataSource The DruidDataSource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
file.conf 的 service.vgroup_mapping 配置必须和spring.application.name一致,在GlobalTransactionAutoConfiguration类中,
默认会使用 ${spring.application.name}-fescar-service-group作为服务名注册到 Seata Server上(每个小事务也要注册到tc上),
如果和file.conf中的配置不一致,会提示 no available server to connect错误,也可以通过配置yaml的 spring.cloud.alibaba.seata.tx-service-group修改后缀,但是必须和file.conf中的配置保持一致
与上面配置数据源的方式等价,这么配置springboot2.0版本以上使用的配置
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
在order、ware中都配置好上面的配置,然后它还要求每个微服务要有register.conf和file.conf,将register.conf和file.conf复制到需要开启分布式事务的根目录,并修改file.conf
vgroup_mapping.${application.name}-fescar-service-group = "default"
service {
#vgroup->rgroup
vgroup_mapping.gulimall-ware-fescar-service-group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
在大事务上@GlobalTransactional,小事务上@Transactional即可
seata的分布式事务不适合高并发的情况下使用,可以在后台功能使用,一般使用使用消息队列来保证最终一致性。
针对订单模块创建以上消息队列,创建订单时消息会被发送至队列order.delay.queue,经过TTL的时间后消息会变成死信,以order.release.order的路由键经交换机转发至队列order.release.order.queue,再通过监听该队列的消息来实现过期订单的处理
创建订单时,会锁定库存,在库存锁定后通过路由键stock.locked发送至延迟队列stock.delay.queue,延迟时间到,死信通过路由键stock.release转发至stock.release.stock.queue,通过监听该队列进行判断当前订单状态,来确定库存是否需要解锁。
库存的锁定、解锁:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
rabbitmq:
host: 192.168.56.10
virtual-host: /
listener:
simple:
acknowledge-mode: manual
@EnableRabbit
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
package com.song.gulimall.ware.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@EnableRabbit
@Configuration
public class MyRabbitmqConfig {
@Bean
public MessageConverter messageConverter() {
//在容器中导入Json的消息转换器
return new Jackson2JsonMessageConverter();
}
@Bean
public Exchange stockEventExchange() {
return new TopicExchange("stock-event-exchange", true, false);
}
/**
* 延迟队列
* @return
*/
@Bean
public Queue stockDelayQueue() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue", true, false, false, arguments);
}
/**
* 普通队列,用于解锁库存
* @return
*/
@Bean
public Queue stockReleaseStockQueue() {
return new Queue("stock.release.stock.queue", true, false, false, null);
}
/**
* 交换机和延迟队列绑定
* @return
*/
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
/**
* 交换机和普通队列绑定
* @return
*/
@Bean
public Binding stockReleaseBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
}
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo wareSkuLockVo) {
//因为可能出现订单回滚后,库存锁定不回滚的情况,但订单已经回滚,得不到库存锁定信息,因此要有库存工作单
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(wareSkuLockVo.getOrderSn());
taskEntity.setCreateTime(new Date());
wareOrderTaskService.save(taskEntity);
List<OrderItemVo> itemVos = wareSkuLockVo.getLocks();
List<SkuLockVo> lockVos = itemVos.stream().map((item) -> {
SkuLockVo skuLockVo = new SkuLockVo();
skuLockVo.setSkuId(item.getSkuId());
skuLockVo.setNum(item.getCount());
//找出所有库存大于商品数的仓库
List<Long> wareIds = baseMapper.listWareIdsHasStock(item.getSkuId(), item.getCount());
skuLockVo.setWareIds(wareIds);
return skuLockVo;
}).collect(Collectors.toList());
for (SkuLockVo lockVo : lockVos) {
boolean lock = true;
Long skuId = lockVo.getSkuId();
List<Long> wareIds = lockVo.getWareIds();
//如果没有满足条件的仓库,抛出异常
if (wareIds == null || wareIds.size() == 0) {
throw new NoStockException(skuId);
}else {
for (Long wareId : wareIds) {
Long count=baseMapper.lockWareSku(skuId, lockVo.getNum(), wareId);
if (count==0){
lock=false;
}else {
//锁定成功,保存工作单详情
WareOrderTaskDetailEntity detailEntity = WareOrderTaskDetailEntity.builder()
.skuId(skuId)
.skuName("")
.skuNum(lockVo.getNum())
.taskId(taskEntity.getId())
.wareId(wareId)
.lockStatus(1).build();
wareOrderTaskDetailService.save(detailEntity);
//发送库存锁定消息至延迟队列
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(detailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
lock = true;
break;
}
}
}
if (!lock) throw new NoStockException(skuId);
}
return true;
}
package com.song.gulimall.ware.listener;
import com.rabbitmq.client.Channel;
import com.song.common.to.mq.OrderTo;
import com.song.common.to.mq.StockLockedTo;
import com.song.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = {"stock.release.stock.queue"})
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
log.info("************************收到库存解锁的消息********************************");
try {
wareSkuService.unlock(stockLockedTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
/**
* 1、没有这个订单,必须解锁库存
* * 2、有这个订单,不一定解锁库存
* * 订单状态:已取消:解锁库存
* * 已支付:不能解锁库存
* 消息队列解锁库存
* @param stockLockedTo
*/
@Override
public void unlock(StockLockedTo stockLockedTo) {
StockDetailTo detailTo = stockLockedTo.getDetailTo();
WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId());
//1.如果工作单详情不为空,说明该库存锁定成功
if (detailEntity != null) {
WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(stockLockedTo.getId());
R r = orderFeignService.infoByOrderSn(taskEntity.getOrderSn());
if (r.getCode() == 0) {
OrderTo order = r.getData("order", new TypeReference<OrderTo>() {
});
//没有这个订单||订单状态已经取消 解锁库存
if (order == null||order.getStatus()== OrderStatusEnum.CANCLED.getCode()) {
//为保证幂等性,只有当工作单详情处于被锁定的情况下才进行解锁
if (detailEntity.getLockStatus()== WareTaskStatusEnum.Locked.getCode()){
unlockStock(detailTo.getSkuId(), detailTo.getSkuNum(), detailTo.getWareId(), detailEntity.getId());
}
}
}else {
throw new RuntimeException("远程调用订单服务失败");
}
}else {
//无需解锁
}
}
业务逻辑:
库存锁定
库存的解锁
订单的pom、yml、启动注解同上
配置
package com.song.gulimall.order.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class MyRabbitmqConfig {
@Bean
public Exchange orderEventExchange() {
/**
* String name,
* boolean durable,
* boolean autoDelete,
* Map arguments
*/
return new TopicExchange("order-event-exchange", true, false);
}
/**
* 延迟队列
* @return
*/
@Bean
public Queue orderDelayQueue() {
/**
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
//死信交换机
arguments.put("x-dead-letter-exchange", "order-event-exchange");
//死信路由键
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
return new Queue("order.delay.queue",true,false,false,arguments);
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* 创建订单的binding
* @return
*/
@Bean
public Binding orderCreateBinding() {
/**
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map arguments
* */
return new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
@Bean
public Binding orderReleaseOrderBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
package com.song.gulimall.order.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyAmqpConfig {
@Bean
public MessageConverter messageConverter() {
//在容器中导入Json的消息转换器
return new Jackson2JsonMessageConverter();
}
}
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
responseVo.setCode(0);
//1. 验证防重令牌
MemberResponseVo memberResponseVo = LoginInterceptor.loginUser.get();
String script= "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long execute = redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()), submitVo.getOrderToken());
if (execute == 0L) {
//1.1 防重令牌验证失败
responseVo.setCode(1);
return responseVo;
}else {
//2. 创建订单、订单项
OrderCreateTo order =createOrderTo(memberResponseVo,submitVo);
//3. 验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = submitVo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//4. 保存订单
saveOrder(order);
//5. 锁定库存
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList());
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
lockVo.setLocks(orderItemVos);
R r = wareFeignService.orderLockStock(lockVo);
//5.1 锁定库存成功
if (r.getCode()==0){
// int i = 10 / 0;
responseVo.setOrder(order.getOrder());
responseVo.setCode(0);
//发送消息到订单延迟队列,判断过期订单
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
//清除购物车记录
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + memberResponseVo.getId());
for (OrderItemEntity orderItem : order.getOrderItems()) {
ops.delete(orderItem.getSkuId().toString());
}
return responseVo;
}else {
//5.1 锁定库存失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
}else {
//验价失败
responseVo.setCode(2);
return responseVo;
}
}
}
package com.song.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import com.song.gulimall.order.entity.OrderEntity;
import com.song.gulimall.order.service.OrderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RabbitListener(queues = {"order.release.order.queue"})
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
orderService.closeOrder(orderEntity);
channel.basicAck(deliveryTag,false);
} catch (Exception e){
channel.basicReject(deliveryTag,true);
}
}
}
关闭过期的的订单,因为取消库存锁定、取消订单 都是通过消息发送,消息可能出现延迟的现象,导致取消订单的消耗的时长要比取消库存锁定的大,造成库存锁死得不到释放的现象,所以在订单关闭的时候,需要向库存发送一条消息,在库存服务判断库存是否释放。双重保障防止库存锁死
/**
* 关闭过期的的订单
* @param orderEntity
*/
@Override
public void closeOrder(OrderEntity orderEntity) {
//因为消息发送过来的订单已经是很久前的了,中间可能被改动,因此要查询最新的订单
OrderEntity newOrderEntity = this.getById(orderEntity.getId());
//如果订单还处于新创建的状态,说明超时未支付,进行关单
if (newOrderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
OrderEntity updateOrder = new OrderEntity();
updateOrder.setId(newOrderEntity.getId());
updateOrder.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(updateOrder);
//关单后发送消息通知其他服务进行关单相关的操作,如解锁库存
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(newOrderEntity,orderTo);
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other",orderTo);
}
}
package com.song.gulimall.ware.listener;
import com.rabbitmq.client.Channel;
import com.song.common.to.mq.OrderTo;
import com.song.common.to.mq.StockLockedTo;
import com.song.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = {"stock.release.stock.queue"})
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
log.info("************************从订单模块收到库存解锁的消息********************************");
try {
wareSkuService.unlock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}