事务看完这篇你只能算入门

6 篇文章 0 订阅
2 篇文章 0 订阅

什么是事务?

事务(Transaction)是关系型数据库中,由一组 SQL 组成的一个执行单元,该单元要么整体执行成功,要么整体执行失败。 
如下所示:

什么是事务的 ACID 特性

事务的 4 大特性:原子性(A),一致性(C),隔离性(I),持久性(D)。这 4 大特性统称为事务 ACID 特性。

  • 原子性:指事务包含的所有操作SQL,要么都执行成功,要么都执行失败

  • 一致性:指事务前后数据的完整性必须保持一致。 
    以小张和小李转账为例:

  • 隔离性:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

  • 持久性:指一个事务一旦被提交,那么数据就永久的存储在系统磁盘中,即使系统发生故障,数据仍然不会丢失。

事务之间没有隔离会出现什么糟糕的情况

1. 第一个严重问题:脏读

  • 指一个事务处理过程中读取了另一个未提交(即回滚)的事务的数据。

  • 如何解决脏读呢? 
    1)如果AB这2个动作是操作的同一个数据库,那么可以把AB的动作放在同一个事务中,C就不会出现脏读。 
    2)如果AB操作的是不同的数据库,即所谓的微服务架构中多服务对应多个业务库,那只能引入分布式事务来解决。

2. 第二个严重问题:不可重复读

  • 指多次查询却返回了不同的数据值,这是由于查询隔离原因,被另一个事务修改并提交了。

  • 如何解决不可重复读呢? 
    在数据库读取数据时加锁,类似 “select * from t_xxx where … for update” 这样的排它锁,明确数据读取后是为了更新操作,所以加了一把行级锁,防止别人修改这些数据。

  • 脏读 和 不可重复读 区别在哪? 
    1)脏读:读到的数据是前一个事务未提交的数据。 
    2)不可重复读:读到的是前一个事务已提交的数据。

3. 第三个严重问题:幻读

  • 指当A事务在读取某个范围内的数据时,B事务又在该范围内新插入了数据记录,此时A事务再次读取该范围内的数据时,就会产生幻读。

  • 幻读 和 不可重复读 区别在哪? 
    1)相同点:幻读和不可重复读都是读取了另一个事务已提交的数据。 
    2)不同点:幻读是针对一批数据。不可重复读是针对的同一条数据

讲完了事务是什么、事务的 4 大特性以及事务隔离性带来了哪些问题,接下来我们继续从事务的实际应用入手。 

在传统的单体应用和当下流行微服务架构中,本地事务能解决的问题越来越少,大多互联网企业级系统都是微服务架构的,即多个服务对应多个业务库,所以引入分布式事务是必然的结果。

本地事务详解

本地事务是指只操作了一个数据库(单体应用较多)。 
在 Spring 应用中的数据库事务管理,只要给方法加一个 @Transactional的注解就可以搞定,是不是超赞????。

@Transactional注解常用参数:

  • name 当在配置文件中有多个 TransactionManager,可以用该属性指定选择哪个事务管理器。

  • propagation 事务的传播行为,默认值为 REQUIRED

  • isolation 事务的隔离级别,默认值为 DEFAULT,即采用数据库的默认隔离级别。

  • timeout 事务的超时时间,默认值为 -1。如果超过该时间限制但事务还未提交,则自动回滚事务。

  • readOnly 用于指定事务是否为只读事务,默认值为 false。为了忽略那些不需要事务的方法,比如select读取数据,可以设置 readOnly = true。

  • rollbackFor 指定能够触发事务回滚的异常类型。

  • noRollbackFor 指定不用回滚事务的异常类型。

rollbackFor 和 noRollbackFor 参数详解:

@Transactional(rollbackFor = xx.class, noRollbackFor = xx.class)
public void doSomething() {  
  //...数据库操作
}

Spring 怎么执行上面的方法呢?如下所示:

对以上伪代码中的 if (如果a属于该提交的状态)条件语句,什么时候事务该提交,什么时候该回滚。

  • noRollbackFor 或子类 ——> commit;

  • rollbackFor 或子类 ——> rollback;

  • throws 定义的异常或子类 ——> commit;

  • 其他异常 ——> rollback;

  • 无异常 ——> commit;

以小张在某商场下订单为例,伪代码如下:

@Transactional(rollbackFor = RuntimeException.class, noRollbackFor = WebServiceException.class)
public void addOrderAndNotify(OrderDTO orderDto) throws Exception {  
  // 查询库存  
  // 下订单(可能出现RuntimeException)  
  // 支付订单  
  // 通知仓库减库存并发货(调用外部服务,可能出现WebServiceException)
}

@Transactional(propagation = xx) 注解参数详解

  • Propagation.REQUIRED 如果有事务,那么加入该事务,没有的话新建一个(默认)

  • Propagation.REQUIRED_NEW 不管是否已经存在事务,都创建一个新的事务,原来的事务挂起,等新的事务执行完,继续执行老的事务

  • Propagation.NOT_SUPPORTED 指不为这个方法开启事务

  • Propagation.MANDATORY 必须在一个已有的事务中执行,否则抛出异常

  • Propagation.NEVER 必须在一个没有的事务中执行,否则抛出异常(与 Propagation.MANDATORY 相反)

  • Propagation.SUPPORTS 如果其他 bean 调用这个方法,在其他 bean 中声明事务,那就用事务。如果其他 bean 没有声明事务那就不用事务。

  • Propagation.NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与 Propagation.REQUIRED 类似的操作。 

伪代码示例 1:

在 methodA 方法中调用 methodB 方法时,已经存在一个事务(methodA方法开启的),methodB 会直接加入到 methodA 开启的事务中执行,所以两个方法中操作数据库的动作要么全部提交成功或全部回滚,不会存在一个提交一个回滚。 

伪代码示例 2:

在调用 methodB 时,methodB 会新开始一个事务,不加入到 methodA 方法的事务中。methodB 方法执行完提交事务成功,如果此时 methodA 在调用 methodB 方法结束后抛出异常,那么 methodA 对应的事务将会被回滚,即数据库中只有 id = 2 的记录被保存下来。 

伪代码示例 3:

在调用 methodB 时,methodB 会嵌套到 methodA 开启的事务中。methodA 方法内部执行插入一条 id = 1 的数据,并创建一个 savepoint 保存点,然后执行 methodB 方法,如果 methodB 方法执行插入一条 id = 2 的数据(未提交),methodA 方法执行正常,methodA 的事务提交时一并提交 methodB 的结果;如果 methodB 方法执行过程抛出异常,回滚到 methodA 设置的保存点,等 methodA 方法执行事务提交,此时数据库中只有 id = 1 的数据记录。 
注:嵌套的事务是不可以独立提交的,但是可以独立的回滚。

@Transactional(isolation = xx) 注解参数详解

  • Isolation.READ_UNCOMMITTED 读取未提交数据

  • Isolation.READ_COMMITTED 读取已提交数据

  • Isolation.REPEATABLE_READ 可重复读

  • Isolation.SERIALIZABLE 串行化

  • Isolation.DEFFAULT 使用数据库默认隔离级别

前 4 个数据库隔离级别是越来越高的。一般使用的隔离级别是 Isolation.READ_COMMITTED,读取已提交数据。Orcale 数据库默认是 READ_COMMITTED,MySQL 数据库默认是 REPEATABLE_READ。 
可以再回头看一下前文介绍的事务隔离级别出现的脏读不可重复读幻读现象。

  • 几种隔离级别的比较

隔离级别/现象

脏读

不可重复读

幻读

READ_UNCOMMITTED

yes

yes

yes

READ_COMMITTED

no

yes

yes

REPEATABLE_READ

no

no

yes

SERIALIZABLE

no

no

no

  • 谈到数据库事务隔离性一般会想到。 
    Isolation 和 Lock 的关系

    • 这是两个不同的概念,隔离不是通过锁实现的,而是通过数据库监控实现的。

    • 锁机制:表加完锁后,除非出现死锁等特殊的情况,事务是不会被数据库主动回滚。

    • 隔离机制:如果发现数据不符合相应的事务隔离级别,当前事务会出错并回滚。相比锁被回滚的可能性更大,需要借助程序编写出错重试机制。

@Transactional(timeout = xx) 注解参数详解 

  • timeout 事务的超时时间,默认值是 -1。如果超过该时间限制但事务还没提交,则自动回滚事务。 

  • 方法抛出异常,事务被回滚,可能是 SQL 执行时间过长的异常,也可能是 TransactionTimedOutException。

  • 从方法执行开始计算,每个 SQL 执行前检查一次是否超时,方法全部执行完毕后不检查是否超时。

分布式事务是指一个业务需要操作多个数据库的情况下,而且必须保持 ACID 的特性(一般存在于微服务架构中的多服务处理)。下次有时间再聊聊分布式事务。

小结

本文介绍了什么是事务,事务的 ACID 特性,事务的隔离级别会带来哪些问题,怎么解决的。另外详细讲述的 Spring 应用中是怎么处理事务的,以及 @Transactional 注解的使用方法,什么情况下回滚事务,数据库默认的隔离级别,各个隔离级别的比较,锁与隔离级别之间的关系,事务超时时间的介绍等等,分布式事务下次再细聊。希望这些内容对你能有所帮助,有所收获。

# 精彩推荐 #

微服务架构中你必须了解的 CAP 原理

记一次生产频繁出现 Full GC 的 GC日志图文详解

 "在看"吗,建议收藏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值