本地事务与分布式事务
本地事务
事务的基本性质
事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。
数据库事务的几个特性:原子性(Atomicity)、一致性( Consistency )、隔离性或独立性( lsolation)和持久性(Durabilily),简称就是ACID;
-
原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
-
一 致性:数据在事务的前后,业务整体一致。
- 转账。A:1000; B:1000; 转200 事务成功; A: 800 B: 1200
-
隔离性:事务之间互相隔离。
-
持久性:一旦事务成功,数据一定会落盘在数据库。
在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常,我们可以很容易的整体回滚;
Business:我们具体的业分代码
Storage:库存业务代码;扣库存
Order:订单业务代码;保存订单
Account:账号业务代码;减账户余额
比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败一个事务开始,代表以下的所有操作都在同一个连接里面;
事务的隔离级别
概念
- READ UNCOMMITTED (读未提交)
该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。 - READ COMMITTED (读提交)
一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和SQL Server的默认隔离级别。 - REPEATABLE READ (可重复读)
该隔离级别是MvSQL默认的隔离级别,在同一个事务里,select的结果是事务开始时时间点的转态,因此,同样的select 操作读到的结果会是一致的, 但是,会有幻读现象。MySQL的InnoDB 引擎可以通过next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。 - SERIALIZABLE (序列化)
在该隔离级别下事务都是串行顺序执行的,MySQL数据库的InnoDB引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
事务并发引起一些读的问题:
概念 | 解释 |
---|---|
脏读 | 一个事务可以读取另一个事务未提交的数据 |
不可重复读 | 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配 |
虚读(幻读) | 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点 |
并发写:使用mysql默认的锁机制(独占锁)
解决读问题:设置事务隔离级别,隔离级别越高,性能越低。
一般情况下:脏读是不可允许的,不可重复读和幻读是可以被适当允许的。
基本命令
命令 | 意思 |
---|---|
SELECT @@global.tx_isolation | 查看全局事务隔离级别 |
set global transaction isolation level read committed; | 设置全局事务隔离级别 |
SELECT @@tx_isolation | 查看当前会话事务隔离级别 |
set session transaction isolation level read committed | 设置当前会话事务隔离级别 |
select @@autocommit | 查看mysql默认自动提交状态 |
set autocommit = 0; | 设置mysql默认自动提交状态【0不自动提交】 |
start transaction; | 开启一个事务 |
commit | 提交事务 |
rollback | 回滚事务 |
savepoint tx1 | 在事务中创建一个保存点 |
rollback to tx1 | 回滚到保存点 |
事务的传播行为
- PROPAGATION REQUIRED: 如果当前没有事务,就创建一个新事务, 如果当前存在事务,就加入该事务,该设置是最常用的设置。
- PROPAGATION SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
- PROPAGATION _MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
- PROPAGATION REQUIRES NEW:创建新事务,无论当前存不存在事务,都创建新事务。
- PROPAGATION NOT SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- PROPAGATION NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION _NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION REQUIRED 类似的操作。
最常用的就是:PROPAGATION REQUIRED、PROPAGATION REQUIRES NEW
SpringBoot 事务关键点
事务的自动配置
TransactionAutoConfiguration
事务的坑
在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是绕过了动态代理对象,事务使用代理对象来控制的。
解决:使用代理对象来调用事务方法
1、引入spring-boot-starter-aop;这个场景启动器引入了aspectjweaver
2、@EnableAspectJAutoProxy(exposeProxy = true);开启aspectj 动态代理功能。以后所有的动态代理都是aspectj创建的(特点:即使没有接口也可以创建动态代理对象;exposeProxy = true 对外暴露代理对象)
3、本类方法互调,获取到本代理对象的方法(通过AopContext获取当前类的代理对象)
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
// 同一个对象内事务方法互调失效。原因:绕过了动态代理对象
// 事务使用代理对象来控制的
@Transactional(timeout = 30) // a 事务的所有设置会传播到和他公用一个事务的方法
public void a() {
// b,c做任何设置都没用。都是和a同一个事务
// this.b(); 没用
// this.c(); 没用
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
orderService.c();
// bService.b(); // a 事务
// cService.c(); // 新事物(不回滚)
int i = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRED, timeout = 2) // 设置的timeout不管用,因为你使用的事务是a的
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20) // 管用
public void c() {
}
分布式事务
为什么有分布式事务
- 分布式系统经常出现的异常
- 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失…
- 通过异常机制解决多个系统数据库一致性的问题有缺陷。假如其他系统是成功执行的,但是由于业务超时,我们这边认为对方出现异常,本系统的业务就回滚了,但是别的系统没有回滚。(假异常)
- 调用顺序的问题,本系统调用第三方系统成功,我们也感知到成功了,但是我们下面的代码出错了。我们这边能回滚,但是第三方数据已经修改了。(调用多个系统的数据库操作,第一个调用成功,第二个调用失败)
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
分布式事务的问题:节点的状态互相无法感知、网络问题。
CAP定理与BASE理论
CAP定理
CAP原则又称CAP定理,指的是在一个分布式系统中:
- 一 致性(Consistericy)
- 在分布式系统中的所有数据备份,在同一时刻是否同样的值。( 等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)
- 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容错性( Partition tolerance)
- 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
一般来说,分区容错无法避免,因此可以认为CAP的P总是成立。CAP定理告诉我们剩下的C和A无法同时做到。
可用性很容易解决,数据不一致的服务不对外提供即可。但是保持一致性很复杂需要通过制定的算法才能实现。实现一致性的算法:分布式系统中实现一致性的raft算法、 paxos。raft算法的核心是选举leader、复制日志
面临的问题
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到9.99999% (N个9),即保证P和A,舍弃C。
BASE理论
是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性。
BASE是指
- 基本可用 (Basically Available)
- 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
- 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
- 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 软状态(SoftState)
- 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。 mysql replication的异步复制也是一种体现。
- 最终一致性(Eventual Consistency)
- 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情祝。
强一致性、弱一致性、最终一致性
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性
分布式事务的几种方案
两阶段提交(2PC模式)
数据库支持的2PC【2 phase commit二阶提交】,又叫做 XA Transactions。
MysaL从5.5版本开始支持, SQL Server2005开始支持, Oracle7开始支持。其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:
- 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交
- 第二阶段:事务协调器要求每个数据库提交数据。其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
- XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
- XA性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景
- XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql 的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。
- 许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
- 也有3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)
柔性事务-TCC事务补偿型方案
是一种编程式分布式事务解决方案。
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
- Try:主要是对业务系统做检测及资源预留
- Confirm:真正执行业务,不作任何业务检查;只使用Try阶段预留的业务资源;Confirm操作满足幂等性。
- Cancel:释放Try阶段预留的业务资源;Cancel操作满足幂等性。
整个TCC业务分成两个阶段完成:
- 一阶段prepare行为:调用自定义的prepare 逻辑。
- 二阶段commit行为:调用自定义的commit逻辑。
- 二阶段rollback行为:调用自定义的rollback 逻辑。
- 所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。
流程:
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。
第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。
举个例子,假如 Bob 要向 Smith 转账100元,思路大概是:
我们有一个本地方法,里面依次调用
-
首先在 Try 阶段,要先检查Bob的钱是否充足,并把这100元锁住,Smith账户也冻结起来。
-
在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
-
如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
缺点:
- Canfirm和Cancel的幂等性很难保证。
- 这种方式缺点比较多,通常在复杂场景下是不推荐使用的,除非是非常简单的场景,非常容易提供回滚Cancel,而且依赖的服务也非常少的情况。
- 这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的,如果串行的服务很多,回滚的成本实在太高。
不少大公司里,其实都是自己研发 TCC 分布式事务框架的,专门在公司内部使用。国内开源出去的:ByteTCC,TCC-transaction,Himly。
柔性事务-最大努力通知型方案
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
柔性事务-可靠消息+最终致性方案 (异步确保型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
防止消息丢失:
- 做好消息确认机制(pulisher, consumer 【手动ack】 )
- 每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
DROP TABLE IF EXISTS `mq_message`;
CREATE TABLE `mq_message` (
`message_id` char(32) NOT NULL,
`content` text,
`to_exchane` varchar(255) DEFAULT NULL,
`routing_key` varchar(255) DEFAULT NULL,
`class_type` varchar(255) DEFAULT NULL,
`message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
使用Seata实现自动补偿
seata:Simple Extensible Autonomous Transaction Architecture
seata架构
Seata有3个基本组件:
- Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager™:事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager(RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
全局事务与分支事务:
a Distributed Transaction is a Global Transaction which is made up with a batch of Branch Transaction, and normally Branch Transaction is just Local Transaction.
Seata管理分布式事务的典型生命周期:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
至此,seata的协议机制总体上看与 XA 是一致的。但是是有差别的:
架构图
XA 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身(通过提供支持 XA 的驱动程序来供应用使用)。
而 Fescar 的 RM 是以二方包的形式作为中间件层部署在应用程序这一侧的,不依赖于数据库本身对协议的支持,当然也不需要数据库支持 XA 协议。这点对于微服务化的架构来说是非常重要的:应用层不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。
这个设计,剥离了分布式事务方案对数据库在 协议支持 上的要求。
seata的核心概念
- TC (Transaction Coordinator) 事务协调者 :维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
- Tm 开启全局事务、tc协调全局事务里面的各个分支事务。
使用流程
使用seata 控制分布式事务
-
每一个微服务先必须创建undo_log 表(需要连接到seata服务器就必须有这个表,需要实现全局事务的业务就加入这个表,seata server会往里面注入阶段的日志)
DROP TABLE IF EXISTS `mq_message`; CREATE TABLE `mq_message` ( `message_id` char(32) NOT NULL, `content` text, `to_exchane` varchar(255) DEFAULT NULL, `routing_key` varchar(255) DEFAULT NULL, `class_type` varchar(255) DEFAULT NULL, `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`message_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
安装事务协调器:seata-server https://github.com/seata/seata/releases
-
整合
-
导入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
-
启动seata-server
- registry.conf:注册中心配置(seata需要注册到注册中心中):修改registry type=nacos
- file.conf:配置文件
-
所有想要用到分布式事务的微服务使用 seata DataSourceProxy代理自己的数据源https://github.com/seata/seata-samples,查看jpa的配置数据源
public class WareConfig { @Bean public DataSource dataSource(DataSourceProperties dataSourceProperties) { HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); if (StringUtils.hasText(dataSourceProperties.getName())) { dataSource.setPoolName(dataSourceProperties.getName()); } // 最后在使用seata代理一下我们的数据源 return new DataSourceProxy(dataSource); } }
-
每个微服务都必须导入file.conf 和 registry.conf (file.conf 里面的分组名字需要修改一下,这个名字就是作为注册到TC里面的,alibaba seata自动配置已经写好的分组名字、必须要与file.conf的一致,alibaba seata自动配置的名字是
${spring.application.name}-fescar-service-group
) -
启动微服务
-
给分布式大事务的入口标注@GlobalTransactional
-
每一个远程的小事务用@Transactional
-
-
出现的问题
// 使用seata控制分布式事务 不能使用批处理,只能一个个操作 List<OrderItemEntity> orderItems = order.getOrderItems(); // for (OrderItemEntity orderItem : orderItems) { // orderItemService.save(orderItem); // } // exception Error updating database. Cause: io.seata.common.exception.NotSupportYetException // io.seata.common.exception.NotSupportYetException: null orderItemService.saveBatch(orderItems);
使用Rabbitmq实现 柔性事务-可靠消息+最终致性方案 (异步确保型)
场景
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
消息队列流程
设计建议规范: (基于事件模型的交换机设计)
- 交换机命名:业务+ exchange; 交换机为Topic
- 路由键:事件.需要感知的业务(可以不写)
- 队列命名:事件+想要监听服务名+ queue4、绑定关系:事件.感知的业务(#)
实操
下单可能会出现的情况:
- 订单下失败、直接回滚,无需担心库存锁定问题。
- 订单下成功、库存服务内部异常,订单回滚,无需担心库存锁定问题。
- 订单下成功、库存服务成功、接下来的业务失败,导致订单回滚,库存业务延时过后就会释放库存。
- 订单和库存服务都成功,只不过用户没有及时支付。由于库存服务延时比订单关闭时间长所以库存释放的时候查询没有这个订单的时候就会自动解锁。
- 订单和库存服务都成功,只不过用户没有及时支付。但是由于网络延时问题,库存服务解锁库存的时候发现有这个订单所以没有释放库存。对于这个问题我们可以在订单关闭的时候发送给库存服务让他释放库存。
谷粒商城下订单流程:
- 用户点击下单,请求来到服务端。服务端会生成一个token并存入redis中然后返回页面给用户。用户点击下单,请求来到服务器我们校验一下传送过来的token和redis中存放的是否一致。通过这个防止用于刷新订单导致库存耗尽问题(原子性校验,通过redis的lua脚本实现比值、删除的原子性,保证用户如果无聊多次刷新提交订单一个token只能被比较一次)
- 校验成功,开始保存订单。我们需要通过sku生成订单项和订单。
- 通过feign调用库存服务的锁库存方法锁定库存。
- feign调用成功,我们就将订单创建成功发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
消息发送到的队列,该队列设置了ttl、死信路由键、死信交换机 - 然后我们在订单服微服务设置了@RabbitListener 监听死信交换机发送到的队列。收到服务在发送消息给库存服务的队列告诉它我们已经关闭订单了,你再次确认一下有没有这个订单的库存信息 MQ
rabbitTemplate.convertAndSend("order-event-exchange", "stock.release.other", orderTo);
- 订单服务发送锁定库存请求,我们确认有库存后,就锁定库存然后发送消息MQ。
- 如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ,锁定失败。前面保存的工作单信息就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁。
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", stockLockedTo);
消息发送到的队列,该队列设置了ttl、死信路由键、死信交换机- 在库存服务设置@RabbitListener 监听死信交换机发送到的队列。收到服务在发送消息给库存服务的队列,库存服务监听到消息就会触发释放库存服务的业务。
解锁库存的流程
1、查询数据库关于这个订单的锁定库存信息。
有,证明库存锁定成功了。
解锁:订单情况。
1、没有这个订单,必须解锁。
2、有这个订单
订单状态:已取消:解锁库存
没取消:不能解锁库存。
没有,库存锁定失败了,库存回滚了。这种情况无需解锁
订单服务监听的消息
@Service
@RabbitListener(queues = {"order.release.order.queue"})
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler()
public void listener(Message message, OrderEntity entity, Channel channel) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + entity.toString());
try {
orderService.closeOrder(entity);
// 手动签收消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 消息消费失败,重新入队
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
库存服务监听的消息
@Service
@RabbitListener(queues = {"stock.release.stock.queue"})
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 1、库存自动解锁。
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
* 2、订单失败。锁库存失败
* <p>
* 只要解锁库存消息失败。一定要告诉服务器解锁是失败的(手动ack)
*
* @param to
* @param message
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息");
try {
// 当前消息是否被第二次及以后(重新)派发过来
// Boolean redelivered = message.getMessageProperties().getRedelivered();
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 出现异常就拒绝
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
@RabbitHandler
public void handleOrderClose(OrderTo to, Message message, Channel channel) throws IOException {
System.out.println("订单关闭准备解锁库存...");
try {
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 出现异常就拒绝
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
总结
CAP:
-
可用性:就是都可以访问,即使这些机器的数据不一致,容忍数据不一致问题。(全部都可用)
-
一致性:就是让数据不一致的机器禁止掉,不对外提供服务,这就保证了用户访问到的数据是一致的。(数据完整的可用)
-
可用性和一致性只能二选一,因为你是分布式系统,多个系统可能不在一台机器上,所以肯定会出现网络问题。没法保证网线不断、网络不中断,除非你是单台机器(既然是单台机器,干嘛整微服务,闲的蛋疼吗)所以一定要有分区容错性
柔性事务和刚性事务:
刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性;
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
分布式事务:
- 本地事务,在分布式系统,只能控制自己的回滚,控制不了其他服务的回滚
- 分布式事务:最大原因,网络问题 + 分布式(不同数据库)
- 2PC模式、柔性事务-TCC事务补偿型方案(商城项目用的很多) 性能很差适合用在后台管理系统并发度不高的业务(可以选用seata实现这一功能简单快速)
- 柔性事务-最大努力通知型方案、柔性事务-可靠消息+最终致性方案 (异步确保型): 都是基于消息服务的,适合高并发,比如订单服务。
AT 模式不适合高并发场景(下单),后台管理服务增删改查一些数据不要求高并发这一场景适合Seata AT 的分布式事务。
TA 模式自动模式,就是通过回滚日志表实现方向补偿。
2pc 是手动补偿,用户自己写代码实现自动补偿。
2PC模式,TCC 事务补偿型方案不适合于高并发场景。seata 的AT模式就是2PC模式的自动化写法。