由锁定库存引发的分布式事务问题【事务自治,抛出异常,可能造成不是最终一致】可能出现订单回滚,库存锁定成功

1、远程调用超时异常,订单回滚,但是库存远程锁定成功【库存事务自治】

2、库存锁定成功,但是订单后面的服务发生故障回滚,库存未回滚【库存事务自治】

一、锁定库存

image-20220201175122834

二、事务的基本性质

数据库事务的几个特性:(ACID

  • 原子性(Atomicity):一系列的操作整体不可拆分,要么同时成功,要么同时失败
  • 一致性 (Consistency):数据在事务的前后,业务整体一致。例如:转账。A:1000;B:1000;转 200 事务成功;A:800B: 1200
  • 隔离性或独立性 (Isolation): 事务之间互相隔离。
  • 持久性(Durabilily) :一旦事务成功,数据一定会落盘在数据库。

重点:使用同一个数据库连接

事务的隔离级别:【就是事务 之间 互相隔离的级别,例如事务级别是可重复读,第一个事务进来读到 1,后面在读取该条数据还是 1,但是读取 count()时数据会改变(insert delete 无法隔离)】

  • READ UNCOMMITTED(读未提交)【导致脏读】

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

  • READ COMMITTED (读提交)【避免脏读】

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

  • REPEATABLE READ(可重复读)【避免脏读、可重复读】【行锁】

    该隔离级别是 MysQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间
    点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。Mysql 的 InnoDB 引擎可以通过 next-key locks 机制来避免幻读。

  • SERIALIZABLE(序列化)【避免脏读、可重复读、幻读】【表锁】【所有事务按照顺序一个个执行】

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

image-20220201180415335

1、SpringBoot 修改事务隔离级别

1
@Transactional(isolation=Isolation.REPEATABLE READ)

2、传播行为

1、PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,
就加入该事务,该设置是最常用的设置。
2、PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当
前不存在事务,就以非事务执行。
3、PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果
当前不存在事务,就抛出异常。
4、PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
5、PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6、PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
7、PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATON_REQUIRED 类似的操作。

重点

REQUIRED: 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务

REQUIRES_NEW: 创建新事务,无论当前存不存在事务,都创建新事务

案例:就是 b、c 与 a 方法是否共用一个事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional(timeout=30)
public void a() {
b();// a事务传播给了b事务,并且b事务的设置失效
c();// c单独创建一个新事务
}

@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {

}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {

}

image-20220201200854287

3、本地事务失效解决:Spring aop + SpringBoot 配置

1、如果方法 a、b、c 都在同一个 service 里面,事务传播行为不生效,共享一个事务

​ 原理:事务是代理对象类控制的,内部调用 b(),c() ,就相当于直接调用没有经过事务【绕过了代理对象】

​ 解决: 不能使用 this.b(); 也不能注入自己【要使用代理对象来调用事务方法】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional(timeout=30)
public void a() {
b();// a事务传播给了b事务,并且b事务的设置失效
c();// c单独创建一个新事务
}

@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {

}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {

}

具体步骤:

1、引入 aop 依赖

1
2
3
4
5
<!-- 引入aop,解决本地事务失效问题 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2、开启动态代理【默认使用 jdk 动态代理,需要有接口】

1
@EnableAspectJAutoProxy(exposeProxy = true)     //开启了aspect动态代理模式,对外暴露代理对象

好处:cglib 继承的方式完成动态代理

3、获取动态代理对象

1
2
3
OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
orderService.b();
orderService.c();

三、分布式事务

image-20220201205326429

1、CAP 定理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CAP原理又称 CAP定理,指的是一个分布式系统中

一致性(Consistency)【3个数据库,同一份数据值一致!】
在分布式系统的所有数据备份,在同一个时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(Availability)【集群中一台机器网络通信故障,是否还能继续工作】
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备可用性)

分区容错性(Partition tolerance)【分布式系统一定出现网络不可用(所有分布式系统一定要保证分区容错性)】
大多数分布式系统都分布在多个子网络。每个子网络就叫一个区(partition)
分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼蹊。

如果满足P,此时要满足A(所有机器都可用包括通信故障那台【数据未同步】),就不能保证一致性【同步数据的通信线故障,无法同步】
如果满足P,此时要满足C,那网络通信故障的节点就不应该继续提供服务(因为他的数据不一致)【宕机的那台机器数据 无法同步】
AP:容易,就算未同步的数据也可用
CP:牺牲可用性
1、算法:raft和paxos算法:http://thesecretlivesofdata.com/raft/【raft算法演示】

无法CA

2、实现 CA【raft 算法】

1
2
3
4
5
6
raft算法
http://thesecretlivesofdata.com/raft/【raft算法演示】
raft.github.io【演示】

不能实现可用性,例如6个节点,组成 3 | 3
两边都无法选出领导,没有大多数节点投票,都只有一半票【导致服务不可用】

领导选举

  1. 集群所有节点启动默认都是随从状态
  2. 如果没有监听到到领导者命令自己,变成候选者
  3. 投票,最终成为领导

具体步骤

  1. 选举超时 election timeout
    随从变成候选者的时间【150ms and 300ms 随机的】【自旋时间,如果没有收到领导的命令变成候选者】例如:启动集群,3 个节点获得随机自旋时间,自旋时间到了就成为候选节点
  2. 成为候选节点,并给自己投票 1,然后给其他随从节点发送选举请求【随从节点的票可能投给更快的候选者】随从节点的票一旦投出便重新自旋
  3. 心跳时间:每隔一段时间发送一个心跳,然后随从节点刷新自旋时间【小于 300ms,否则大家都成为候选者了】此时领导网络延时,自旋结束产生候选者,产生新领导
  4. 有多个候选者,并且票数一样,就自旋重新投

image-20220201224024357

领导复制【可保证数据一致性】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
所有节点修改数据,都要通过领导来修改
1、客户端通知领导修改一个数据,领导先创建一个 节点日志
2、领导将这条日志 发送给所有所有随从节点【随从节点收到并返回确认消息给领导】
3、领导等待大多数随从节点的确认消息,领导提交数据,然后通知随从节点可以提交了
4、随从节点也提交数据。最后领导节点给请求返回提交成功


具体步骤:
1、领导收到后并不会马上给随从节点发送 日志,等待下一次心跳时发送日志
2、然后领导提交并马上返回请求提交成功。然后跟随下一个心跳发送随从 告诉其提交
3、可保证数据一致性【例如选出来两个领导,不同机房。2个和3个组成两个群】
demo:此时2个的那个客户端发请求,一直保存失败,因为不是大多数人成功【所以数据未提交】,但是另外一边3个节点组成的集群可以保存成功【大多数节点】
如果此时两个集群恢复了数据通信,旧领导退位,并且跟着旧领导未提交的数据需要回滚【低轮领导退位,新领导上位】
然后匹配上新领导的日志

image-20220201224507471

数据一致性:只有上面一个领导数据保存成功【大多数节点】

image-20220201224547910

数据通信恢复,旧领导退位

image-20220201224638662

3、面临的问题

1
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到99.99999%(N个9),即保证p和A,舍弃C【强一致性,改为弱一致性】。

4、BASE 理论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可
以采用适当的采取弱一致性,即最终一致性。【保证AP时,无法保证C,但是可以最终一致性】

BASE是指
1、基本可用(Basically Available)
1)基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、
功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系
统不可用。
1、响应时间上的损失;正常情况下搜索引擎需要在0.5秒之内返回给用户相应的
查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询
结果的响应时间增加到了1~2秒。
2、功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,
部分消费者可能会被引导到一个降级页面。

2、软状态(Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布
式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体
现。mysal replication的异步复制也是-种体现。

3、最终一致性(Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状
态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

5、强一致性、弱一致性、最终一致性

1
2
3
4
强一致性:更新后的数据后续访问能看到【本地事务,立即回滚】
弱一致性:容忍部分或全部访问不到(数据不一致)【软状态,存在不一致的数据】
最终一致性:一段时间后能更新到最新数据【经过一段时间后回滚】

四、分布式事务几种方案【本系统采用最终一致性 AP】

1、2PC 模式【少】AT 模式【根据回滚日志表自动回滚】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
数据库支持的2PC【2 phase commit二阶提交】,又叫做XA Transactionso
MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle7开始支持。
其中,xA 是一个两阶段提交协议,该协议分为以下两个阶段:

第一阶段,事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是
否可以提交.
第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务
中的那部分信息。

重点:只要一个服务没有就绪,事务管理器就会控制回滚

AT模式:Auto Transiaction:自动事务模式,根据回滚日志表自动回滚
TCC模式:就是根据自己手写的事务补偿方法 来回滚

1598773991455

1598774078828

2、柔性事务-TCC 模式【手写事务补偿方案】

1
2
3
4
5
6
7
8
9
10
11
刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性;
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

实现:
将业务代码拆成三部分。
1、try锁库存
2、confirm提交数据
3、事务补偿逻辑:一旦出现异常执行cancel来回滚【取消锁定库存】

其实就是2PC的手动实现

1598774153000

1598774160281

3、柔性事务-最大努力通知型方案【支付宝支付】【多,高并发场景】【基于消息服务 mq】

1
2
3
4
5
6
7
8
9
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种
方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种
方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通
知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对
账文件),支付宝的支付成功异步回调

例如支付宝支付成功,往MQ发送消息【隔几秒发一个】
订单订阅topic,一旦订单确认消息,给支付宝发送确认,支付宝就不再通知了

4、柔性事务-可靠消息+最终一致性方案(异步确保性)【多,高并发场景】【基于消息服务 mq】

1
2
3
4
5
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只
记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确
认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

往mq里面发送回滚消息