专项攻克——事务

一、事务

事务,就是为了使得一些更新插入操作要么都成功,要么都失败。

二、事务的四大特性

严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。

2.1 原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。

2.2 一致性(Consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态。
也就是说事务前后数据的完整性必须保持一致。

2.3 隔离性(Isolation)

隔离性是指一个事务的执行不能有其他事务的干扰,事务的内部操作和使用数据对其他的并发事务是隔离的,互不干扰。

2.4 持久性(Durability)

持久性是指一个事务一旦提交,对数据库中数据的改变就是永久性的。此时即使数据库发生故障,修改的数据也不会丢失。接下来其他的操作不会对已经提交了的事务产生影响。

三、事务的几个问题

多个事务并发执行时,读取数据方面可能碰到三个问题:

  1. 脏读:读到了脏数据,即无效数据。
  2. 不可重复读:是指在数据库访问中,一个事务内的多次相同查询却返回了不同数据。(修改)
  3. 幻读:指同一个事务内多次查询返回的结果集不一样,比如增加了行记录。(新增)

备注:不可重复读对应的是修改update操作。幻读对应插入操作。幻读是不可重复读的特殊场景。

四、事务的隔离级别和MVCC

4.1 事务的隔离级别

提高事务隔离级别的目的:解决脏读、不可重复读、幻读等读现象。

矛盾:隔离级别越高,并发能力就越低。所以,需要根据业务来场景来衡量使用哪种隔离级别。

在这里插入图片描述

隔离级别由低到高如下:

  1. Read uncommitted(读未提交) :一个事务可以读取另一个未提交事务的数据。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。

    示例:小明去商店买衣服,付款时,小明发起事务,但还没有提交。而商店老板查看自己账户,发现钱已到账(读未提交),于是小明正常离开。【可能事故:小明在走出商店后,马上回滚差点提交的事务,撤销了本次交易,于是小明没花钱就买到了衣服】

  2. Read committed (读已提交):一个事务要等另一个事务提交后才能读取数据。在事务A处理期间,如果事务B修改了相应的表,则事务A的同一读sql在事务B执行前后的返回结果是不同的。(会造成幻读、不可重复读)读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行,会对该写锁一直保持直到到事务提交。

    示例:小明卡里有1000元,聚餐时,准备计算1000元(事务开启),收费系统检测到他卡里有1000元。收费系统检测完毕时,小明的老婆转成功走了卡里的钱。【可能事故:当收费系统准备扣款时,再检查小明卡里的金额,发现已经没钱了,付款不成功。】

  3. Repeatable read (重复读):在开始读取数据(事务开启)时,不再允许修改操作。可重复读会对读的行加锁,导致他事务修改不了这条数据,直到事务结束,但是这种方案只能锁住数据行,如果有新的数据进来,是阻止不了的,所以会有幻读问题。但是!!!Mysql已经是个成熟的数据库了,怎么会采用如此低效的方法呢? 其实这里的锁,是通过next-key锁实现的

    推荐博客:详解可重复度

    • 悲观锁:读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。缺点:数据库性能消耗大。
    • 乐观锁:大多是基于数据版本( Version )记录机制实现。通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

    示例:还是小明有1000元,准备跟朋友聚餐消费这个场景,当他买单(事务开启)时,收费系统检测到他卡里有1000元,这个时候,他的女朋友不能转出金额。接下来,收费系统就可以扣款成功了

    问题拓展:写和读在同一事务的操作
    第一步:更新A表id=1的记录
    第二步:查询A表id=1的记录
    第三步:使用第二步的查询结果作为依据继续业务逻辑
    第四步:提交事务
    问题:同一个事务中,事务未提交前,第二步的查询结果是第一步执行前的结果还是第一步执行后的结果?
    答案:是第一步执行后的记过。事务隔离级别是针对不通事务的,同一事务中的未提交的更新,在后续是可以查询到的。

  4. Serializable (序列化)
    数据库事务的最高隔离级别。在此级别下,事务串行执行。可以避免脏读、不可重复读、幻读等读现象。但是效率低下,耗费数据库性能,不推荐使用。它在选定对象上的读锁和写锁保持直到事务结束后才能释放,所以能防住上诉所有问题,但因为是串行化的,所以效率较低。

4.2 数据库的多版本并发控制MVCC

在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下:

  • SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时,保存当前事务版本号为行的删除版本号
  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

通过MVCC,虽然每行记录都要额外的存储空间来记录version,需要更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多读操作都不用加锁,读取数据操作简单,性能好。

细心的同学应该也看到了,通过MVCC读取出来的数据其实是历史数据,而不是最新数据,这在一些对于数据时效特别敏感的业务中,很可能出问题,这也是MVCC的短板之处,有办法解决吗? 当然有.
MCVV这种读取历史数据的方式称为快照读(snapshot read),而读取数据库当前版本数据的方式,叫当前读(current read).

在这里插入图片描述

五、MySQL和Spring的事务

5.1 MySQL和Spring的事务隔离级别

  • MySQL和Spring默认的事务隔离级别:重复读REPEATABLE-READ,可以避免脏读,不可重复读,不可避免幻读

  • mysql查看数据库实例默认的全局隔离级别sql
    (1) Mysql8以前:SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
    (2) Mysql8开始:SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;

  • 修改MySQL隔离级别命令:
    SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

  • 如果MySQL与代码的事务隔离级别不一致,会使用代码指定的事务隔离级别。

5.2 Spring事务原理

  • 声明式事务管理原理:使用了 AOP 实现的,本质就是对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,根据实际情况选择提交或是回滚事务。所以必须通过代理对象调用目标方法的时候,事务才会起效。

  • 优点:使用这种方式,对代码没有侵入性,方法内只需要写业务逻辑就可以了。

  • 缺点:
    (1) 声明式事务的最小粒度是方法。如果想要给一部分代码块增加事务的话,就需要把这个部分代码块单独独立出来作为一个方法。
    (2)由于声明式事务既可以通过注解使用,也可以通过配置实现,这就导致某些事务可能被开发者忽略。如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作,这些操作如果被包在事务中,有两个问题:1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能RPC调用成功了,但是本地事务回滚了,可是PRC调用无法回滚了。2、在事务中有远程调用,就会拉长整个事务。时间久会导致本事务的数据库连接一直被占用,类似操作过多就会导致数据库连接池耗尽。

5.2.1 编程式事务和声明式事务,如何选择?

推荐使用编程式事务,这样业务代码中就会清清楚楚看到什么地方开启事务,什么地方提交,什么时候回滚。有人想改这段代码时,就会强制考虑要加的代码是否应该方法事务内。

如以下几种场景导致的声明式事务失效,如果使用编程式事务的话,很多都是可以避免的:
1、@Transactional 应用在非 public 修饰的方法上
2、@Transactional 注解属性 propagation 设置错误
3、@Transactional 注解属性 rollbackFor 设置错误
4、同一个类中方法调用,导致@Transactional失效
5、异常被catch捕获导致@Transactional失效
6、数据库引擎不支持事务

有些人可能会说,已经有了声明式事务,但是写代码的人没注意,这能怪谁。话虽然是这么说,但是我们还是希望可以通过一些机制或者规范,降低这些问题发生的概率。因为,工作这么多年来,发生过不止一次开发者没注意到声明式事务而导致的故障。有些时候,声明式事务确实不够明显,另外,声明式事务用不对容易失效。


Spring的事务是基于AOP实现的,但是在代码中,有时候我们会有很多切面,不同的切面可能会来处理不同的事情,多个切面之间可能会有相互影响。在之前的一个项目中,我就发现我们的Service层的事务全都失效了,一个SQL执行失败后并没有回滚,排查下来才发现,是因为一位同事新增了一个切面,这个切面里面做个异常的统一捕获,导致事务的切面没有捕获到异常,导致事务无法回滚

5.3 Spring事务注解@Transactional什么时候失效?

推荐博客:spring事务失效场景

  • 事务失效的7种情况
    1. 未启用spring事务管理功能
    2. 方法不是public类型的
    3. 数据源未配置事务管理器
    4. 自身调用问题
    5. 异常类型错误
    6. 异常被吞了
    7. 业务和spring事务代码必须在一个线程中

5.3.1 未启用spring事务管理功能

@EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能,这个注解千万不要忘记写了。

5.3.2 方法不是public类型的

@Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效。

5.3.3 数据源未配置事务管理器

spring是通过事务管理器了来管理事务的,一定不要忘记配置事务管理器了,要注意为每个数据源配置一个事务管理器:

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

5.3.4 自身调用问题

spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。

看下面代码,思考一个问题:当外部直接调用m1的时候,m2方法的事务会生效么?

@Component
public class UserService {
    public void m1(){
        this.m2();
    }
    
    @Transactional
    public void m2(){
        //执行db操作
    }
}

显然不会生效,因为m1中通过this的方式调用了m2方法,而this并不是代理对象,this.m2()不会被事务拦截器,所以事务是无效的。如果外部直接调用通过UserService这个bean来调用m2方法,事务是有效的,上面代码可以做一下调整,如下,@1在UserService中注入了自己,此时m1中的m2事务是生效的。
重点:必须通过代理对象访问方法,事务才会生效。

@Component
public class UserService {
    @Autowired //@1
    private UserService userService;
 
    public void m1() {
        this.userService.m2();
    }
 
    @Transactional
    public void m2() {
        //执行db操作
    }
}

5.3.5 异常类型错误

spring事务回滚的机制:对业务方法进行try catch,当捕获到有指定的异常时,spring自动对事务进行回滚,那么问题来了,哪些异常spring会回滚事务呢?

并不是任何异常情况下,spring都会回滚事务,默认情况下,RuntimeException和Error的情况下,spring事务才会回滚。

也可以自定义回滚的异常类型:

@Transactional(rollbackFor = {异常类型列表})

5.3.6 异常被吞

当业务方法抛出异常,spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。

如下代码,事务发生了异常,但是被捕获了,此时事务并不会被回滚

5.3.7 业务和spring事务代码必须在一个线程中

spring事务实现中使用了ThreadLocal,可以实现同一个线程中数据共享,必须是同一个线程的时候,数据才可以共享,这就要求业务代码必须和spring事务的源码执行过程必须在一个线程中,才会受spring事务的控制,比如下面代码,方法内部的子线程内部执行的事务操作将不受m1方法上spring事务的控制,这个一定要注意

@Transactional
public void m1() {
    new Thread() {
        一系列事务操作
    }.start();
}

六、分布式事务

  • 分布式锁:解决分布式资源抢占的问题;

  • 分布式事务:解决流程化提交问题。在分布式系统中实现事务,它其实是由多个本地事务组合而成。

  • 分布式事务为什么复杂?
    (1)存储端的多样性。本地事务的情况下,所有数据都会落到同一个DB中,但是,在分布式的情况下,就会出现数据可能要落到多个DB,或者还会落到Redis,落到MQ等中。
    (2)请求链路被延展拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。

分布式事务要求:保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数要么一起成功,要么一起失败,必须是一个整体性的事务。

举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。

6.1 分布式中的CAP理论

参见博客:专项攻克——CAP理论

  1. 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
  2. 可用性(Availability) : 每个操作都必须以可预期的响应结束
  3. 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成

6.2 分布式事务解决方案——TCC(重要)

2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务。

  • TCC 分布式事务模型需要业务系统提供三段业务逻辑:

    1. 初步操作 Try:完成所有业务检查,预留必须的业务资源【数据处于冻结状态】。
    2. 确认操作 Confirm:真正执行的业务逻辑,不作任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务有且只能成功一次。
    3. 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
  • TCC的难点、注意事项、缺点、优点:

    1. 难点:难点在于业务上的定义,每一个操作都需要定义三个动作Try - Confirm - Cancel。
    2. 注意事项:撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
    3. 缺点:开发量大,有时候这三个方法还真不好写。对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。
    4. 优点:是在业务上实现的,可以跨数据库、跨不同的业务系统来实现事务。
      在这里插入图片描述

6.3 分布式事务解决方案——二阶段

参见博客:两阶段、三阶段
2PC(Two-phase commit protocol),中文叫二阶段提交。第一阶段是准备**【资源锁定】**,第二阶段是提交。

2PC 引入一个事务协调者,用于协调管理各参与者(也可称之为各本地资源)的提交和回滚。

总结:二阶段的核心是对每个事务先锁定后提交的处理方式,该提交方式是一个强一致性的算法。

优点:原理简单、实现方便
缺点:同步阻塞、单点问题、脑裂、太过保守

流程及分析:

  • 如果第一阶段所有参与者都返回准备成功,协调者就会向所有参与者发送提交事务命令。待所有事务都提交成功后,返回事务执行成功。
  • 如果第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败。
  • 如果第二阶段提交失败(回滚事务或提交事务),则不断重试,直到成功,否则阻塞。
    在这里插入图片描述

协调者故障分析:
2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。协调者是一个单点,存在单点故障问题。

  • 假设协调者在发送准备命令之前挂了,等于事务还没开始,还行
  • 假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其它操作
  • 假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功的参与者都阻塞着
  • 假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。
  • 假设协调者在发送提交事务命令之前挂了,这个不行,傻了!所有资源都阻塞着
  • 假设协调者在发送提交事务命令之后挂了,这个还行,也是至少命令发出去了,很大概率都会提交成功,然后释放资源,但是如果出现网络分区问题某些参与者将因为收不到命令而阻塞着。

6.3 分布式事务解决方案——三阶段

3PC 是为了单点故障问题和减少二阶段的资源阻塞问题,包含三阶段:询问阶段、预提交阶段、提交阶段(CanCommit、PreCommit 和 DoCommit)。

2PC和3PC的差异:

  • 3PC在第一阶段只是询问且不锁定资源,好处是可以让协调者尽早的发现发问题,同时降低资源的锁定范围。
  • 3PC 把 2PC 的提交阶段变成了预提交阶段和提交阶段。
  • 3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。

缺陷:(1)2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。

具体流程
在这里插入图片描述


在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城有术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值