本地事务
一、本地事务
一个或一组SQL语句组成一个执行单元,这个执行单元要么全部执行,要么全部不执行
二、ACID 特性
-
原子性:一个事务的整体操作不可拆分,要么都成功,要么都失败 -
一致性:一个事务执行会使数据从一个一致状态切换到另一个一致状态 以转账为例,转账之前 A 有 1000,B 有 1000, 如果 A 给 B 转 200,成功了,那么 A 就是 800,B 就是 1200,业务前后 它们的总量都是 2000,不可能出现转完账之后,A 扣了 200,B 没加 200 -
隔离性:事务之间互相隔离 假设有 100 个人都在下单,一个人下单失败了,他的数据回滚,不会影响其他人 -
持久性:一个事务一旦提交,则会永久的改变数据库的数据 一旦事务保证了前3项特性,数据库通知事务提交成功了,那就一定会成功,就算数据库刚提示完成功,然后断电了,等再 次启动数据库时,也能在数据库中发现数据的变化
二、隔离级别
@Transactional(isolation = Isolation.XXX)
整个事务期间,只要事务没结束,第一次去数据库查询数据,假设: 1 号记录数据是 100,我们在整个事务期间,无论读多少次,1 号记录的值都是 100,即使其他人把这个数据都删了,或者修改了,我们读到的都是 100 。
该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。
在该隔离级别下整个数据库的事务都是串行顺序执行的,这就意味着数据库没有任何并发的能力,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
| 效果 |
---|
脏读 | A事务未提交却被B事务读取到 | 不可重复读 | A事务做了修改提交了, 同时开启的B事务读取到了修改后的结果 | 幻读 | 同时开启的两个事务中,A事务进行了查询,发现表内有两条数据,此时B事务向表中插入了一条数据并提交,A事务打算修改这两条数据,结果修改了三条数据 |
| 脏读 | 不可重复读 | 幻读 |
---|
READ UNCOMMITED(读未提交数据) | √ | √ | √ | READ COMMITED(读已提交数据) | × | √ | √ | REPEATABLE READ(可重复读) | × | × | √ | SERIALIZABLE(串行化,性能最差) | × | × | × |
三、传播行为
-
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 类似的操作。
@Transactional(timeout = 30)
public void a(){
b();
c();
int i = 10 / 0;
}
@Transactional(propagation = Propagation.REQUIRED)
public void b(){
int i = 10 / 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){
}
1、同一个对象内,事务方法互调默认失效
上述的这个示例,如果 a、b、c 方法全都在同一个 service 下,那么 b、c 做的传播行为配置,都不会起作用,也就是说b、c都会跟 a 共用一个事务
除非b、c在其他不同的 service,那样才能使它们自己的配置生效
2、原因
事务是用代理对象来控制的,如果在 a 里面调用的是同一个 service 的 b、c方法,相当于把 b、c 的代码复制、粘贴过来了,也就是跳过了代理
3、解决
使用代理对象调用b、c方法,即可解决
-
导入 spring-boot-starter-aop 依赖,这个依赖引入了 aspectj -
启动类开启 aspectj 动态代理功能,以后所有的动态代理都是 aspectj 创建的(即使没有接口也可以创建动态代理),对外暴露代理对象 @EnableAspectJAutoProxy(exposeProxy=true) -
用代理对象对本类互调 AopContext.currentProxy() 调用方法
@Transactional(timeout = 30)
public void a(){
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
orderService.c();
}
@Transactional(propagation = Propagation.REQUIRED)
public void b(){
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){
}
本地事务在分布式下的问题
1、假失败
如果保存订单成功,远程锁库存假失败,那就会出现问题
假失败就是我们在订单服务调库存服务时, 库存锁定成功,然后由于服务器慢、卡顿、等故障原因,本地事务提交了之后,一直没返回到订单服务
此时再看订单服务,因为调用库存服务时间太长了,库存服务迟迟没有返回结果,可能就会触发 feign 的超时机制,在调用远程服务这里抛异常:read time out 读取超时,但是这个异常并不是我们手动抛的锁库存异常,而是 feign 的异常
并且订单服务,设计的回滚机制,是只要一出现异常就会全部回滚,
结果:库存锁定成功,订单服务因为 feign 的超时机制,出现异常,导致订单数据全部回滚,最终数据不一致
2、调用新服务出现异常之后,已经执行的服务不会回滚
假设库存锁定成功,将结果返回到了订单服务,我们根据结果又调用了积分服务,让它扣减积分,
结果积分服务内部出现异常,积分数据回滚
此时再看订单服务,订单服务感知到我们手动抛的积分异常,订单数据回滚,但是库存服务,却不会有任何感知,
结果:积分、订单数据全部回滚,库存给锁定了,也是数据不一致
只需要在订单服务的库存执行成功之后,添加一个 int i = 10 / 0; ,模拟积分服务出现异常,很容易就能复现这个问题
3、总结
本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚
产生分布式事务的最大原因,就是网络问题 + 分布式机器
分布式事务
一、为什么会有分布式事务
我们还是以我们之前的下单场景为例。比如我们来看这张图。这张图跟我们之前的下单场景稍微有些不一样,我们就以它为例,
我们现在有一个业务叫下单,就是我们的 business 这个业务,然后它首先要调用我们三个功能。第一个是我们扣库存,然后接下来是保存订单,订单保存完了我们可能还要扣减积分。
只有这三个功能同时调用成功了,我们这个单才算是下成功。
如果是我们以前的单体应用,我们将这三处代码全部写在一个系统里边。而且我们全部连想的是一个数据库。
那这样的话,我们使用本地事务就可以非常方便的控制住。只要有一个失败,大家全体回滚。
但是正是由于我们分布式系统的出现。
由于我们这个业务太大,我们不可能将所有业务全写进一个项目里边。所以我们拆分成了好多微服务。比如我们的库存服务,我们的订单服务。还有我们的用户账户服务,
我们现在拆分了三个服务,而且每个服务还是连自己的数据库操作自己的数据,还互相没有关系。
再加上分布式系统之间部署还可能不在一块。
比如我们这个库存服务在一号机器,订单在二号,账户在三号机器。
那这样我们想要完成整个下单逻辑,我们就要远程调用这三个机器的各个方法。
二、分布式系统带来的问题
1、问题一
比如机器宕机,我们二号执行完了,想要调用我们三号业务。但是三号业务这个机器宕机了,那一宕机以后,会出现什么问题呢?
其实是二号,它不能感知到我们这个三号到底是执行成了还是败了。
如果是我们在二号调三号之前,它给炸了,那还好,我们二号知道它连不上三号,那把自己也回滚一下。
2、问题二
但是如果是我们二号调了三号机器的代码,有请求给它发过去了,然后三号机器代码可能都执行完了,正好在执行完的那一刻就要给它返回的时候,它给炸了。
那这样的话,二号机器就永远等不到它的返回,会认为它已经宕机了。
但是此时的二号机器,它是没办法知道我们三号机器到底是执行成了还是执行败了。
就算我们需要查三号机器成了还是败了,它都宕机了,也没法查了。
所以现在可能会由于机器宕机的问题,我们想要同步它们之间的状态不好同步。
因为我们想要让一个失败,大家全体失败,来做一个事务。
3、问题三
或者由于网络异常 ,比如三号机器一切都执行成功了。然后把所有的成功消息都返回给订单服务了
结果我们消息刚发出去,老鼠把网线咬断了。那我们这个消息传不出去了,怎么办?
所以现在我们这个二号还是不知道我们这个三号机器它到底执行的状态怎么样。
那它们三个就无法同步我们的事务状态,
再加上我们分布式系统里边如果引入了更多中间件,一些消息中间件。造成的丢失乱序,包括一些数据的发送错误,我们经常类型转换转成了这个空指针异常的类型转不过来的。
但实际人家远程。执行成功了,只是你业务代码出了问题,
还有我们一些不可靠的这些网络 tCP 连接等等。
只要是我们这些网络的问题、机器服务节点故障的问题都会导致我们某一个机器的状态,它的这个成功失败,别的机器可能没办法感知。
那这样想要在分布式系统里边做事务,想协调一号、二号、三号机器,它们整体要么都回滚,要么都成功,这就非常难。
这就是我们的分布式事务。
只要我们有微服务,我们的业务太大,我们拆成微服务部署在了不同机器。
那我们在后来的业务开发中,我们一定百分百避免不了我们的分布式事务。
这个东西就是躲得过初一,躲不过十五。我们可以提前设计一些,比如我们前几个业务,我们通过自己的设计,我们把数据库揉在一起或者怎么着,我们终于躲过了分布式事务,但在后边一些复杂的业务又出现远程调用。我们总是躲不过。
我们不可能将所有的代码全部放在一个项目里边。因为现在的项目的规模都太大了,没有任何机器能负担住一个大型项目的运行。
所以只要是微服务架构,分布式事务就是无法避免的
三、如何解决分布式事务?
分布式事务出现的原因就是我们节点之间互相的状态不能同步,包括网络状况问题。
我们这些数据互相感知不到,现在我们就希望有人能协调这个事情。
当然我们做这个之前我们先来考虑一下我们分布式系统里边的一些定理。
它既然能叫定理,就是我们不能打破的一个理论。
那我们在分布式系统里边有一个叫 CAP 原则,也叫 CAP 定理 。
CAP 理论
一、CAP 指的是什么?
CAP 指的是一致性 、可用性 、分区容错性 。
二、一致性(Consistency )
CAP 想说一件什么事情,它想说咱们这个分布式系统里边,比如我们有三个节点,一号、二号、三号节点,
我们现在想做一个事务,想成功都成功,想失败都失败。
或者我保存了一个值,那大家都得跟我一样。所以我们说的第一个一致性指的就是这个。
在我们分布式系统里边,比如我们这一二三号机器,假设我们都是数据库,想要保存一个数据,比如这个数字是八,那我们既然要保存八,那一号保存了八。
我们下一个请求可能要访问二号数据库,二号就也得有八,三号也得有八。
这就相当于我们一个分布式系统里边所有这个数据的备份。
比如我们准备把八号备份在三个机器里边,所有数据的这个备份在同一个时刻是否都有同样的值。
比如我一号机器,我说八成功了,那你再来访问二号信息,结果你拿的刚才数据是七,那这个就不一致了。
所以我们说的一致性就是分布式系统里面所有的数据备份在同一时刻是一样的
三、可用性(Availability)
还有我们的可用性,可用性就指的是我们一号机器,二号机器和三号机器。
如果有一个机器宕机了,我们整个集群还能不能响应客户端的所有操作
假设能响应,那就说明是可用的。
如果不能响应,只要有任何一个机器炸了,我们就不响应客户端的操作,说我们这个后台集群有问题,有一个机器坏了,等我们修好了。你再来操作,那这就是不可用的。
四、分区容错性(Partition tolerance)
分区容错指的就是我们一号机器,二号机器,三号机器。由于它们部署在不同节点。
它们之间通信肯定要使用这个网络,网络肯定会出现的一些通信失败、错误问题。
只要通信失败,就说我们这个分区发生了这个错误,我们接下来该怎么办?
五、面临的问题
但是我们现在能发现一件事,就是我们这个一致性、可用性、分区容错性,这三个不能三者兼顾 ,为什么?
我们来举一个例子,假设一致性
一、二、三、三个机器都想要保存八号数据。
我们一个请求过来,让它三个备份节点,同时都要把八号数据保存起来。
所以如果我们想要满足一致性c ,那我们现在三个机器全部都保存好了。
那么接下来在可用性和分区容错二选一,怎么叫分区容错二选一?
假设我们不是由于节点故障,而是由于通信故障,我们一号跟三号机器的网线断了。
保存数据肯定只给某一个机器发了请求,我给一号机器发了八请求。
然后,一号机器让二号机器同步了一下,二号也保存了八,结果让三号机器同步的时候,由于网线断了,它死活连不到三号机器。
那此时就发生了分区错误,我们这个网络出错了,就是我们分区容错。
我们分区错误以后,接下来我们就考虑。
我们能不能满足可用性?假设我们还让它满足可用性。
行,我们让它三个机器都能用,那下一个客户端的请求。
比如我们负载均衡。去三号里边读数据了,但是由于三号通信故障,把一号的数据没同步过来。那它读到的数据不就不一样了吗?
所以你一满足可用性,你发现又不一致了。
所以你想要满足一致,那你就必须让三号机器不能访问,那它不能访问,相当于我们就不可用了。
所以我们说的这个一致性和可用性只能二选一 。
在分布式系统里边,我们永远都要满足分区容错。
因为我们这个网络肯定会出现问题,所以我们这个分区容错性分区出错了。我们一定要想办法解决,但怎么想办法解决?
那就二选一,那我们到底是要满足一致还是满足可用。
如果满足可用,让它们能去访问,那相当于下一个人能访问三号机器。那它访问到的就是不一致的数据,那相当于你就容忍了这个业务能访问到不一致的数据。
我们前边的人都是八,接下来这个人访问却变成了七。
但如果我们想要满足一致,我们一定让它访问任何一个节点。那都是八。那这样的话我们就不能让整个集群可用。如果我们让整个集群可用了,那发给一号二号的请求读出来是八,没问题。但是发给三号的请求读出来是七,会不一致的。
所以我们必须把所有的集群全部禁掉。
你说你给一号机器保存了八,然后。只要三号机器没同步,我就给你不响应,说我这个八保存成功了,我就告诉你保存失败了,要不然我告诉你保存成功了,你还跑三号机器一拿,结果还拿出来了。
这就是我们说的一致、可用必须二选一,而 p 我们是必须要选择的。所以我们这个 CA 二选一,
所以我们最终我们的分布式系统里边。要么是一个 CP 系统,要么是一个 AP 系统,CA 系统做不到,
因为你没办法保证你的网线不断,网络不中断,除非你是单台机器,单台机器自己连自己机器的数据库,自己连自己的 redis,所有的数据库、redis 都装在一台机器里边。那可以来考虑 CA。所以这个 c 其实就是本地。
所以我们不可能有 CA,只有 CAP 和 AP,这就是我们说的 CAP 定理。
同样的映射到我们的业务里边。我们分布式系统都得满足这个定理。
但是我们现在来考虑,如果我们来说 AP,我们分区错误了,我们让它可用读到的不一致就不一致。三个节点你突然去读三号节点数据跟一二不一样,就不一样的。我们满足 AP 没得说,
关键如果我们想要满足 CP,我们在分布式系统里边,想要保证它一致性。即使网络出错了,我们都能一致。那怎么一致?
我们牺牲可用,牺牲可用是一个方面。第二方面我们能不能通过什么算法能保证它们之间的一致?
我们在分布式系统里边一般会有一些一致性算法,典型的代表,比如说 raft 算法 ,还有我们的pencil 算法 等等等等。
|