事务

一、引言

事务(Transaction)是数据库区别文件系统的重要特征之一。

二、知识点

2.1 事务基本概念

数据库事务拥有以下四个特性,习惯上被称之为ACID特性。换句话说,满足下面4个特性的操作,可以认为是事务操作。

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态,一致状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

2.2 数据库事务

虽然事务有着严格的定义,也就是必须同时满足ACID4个特性,但是并不是所有的数据库都满足事务ACID的要求。

数据库事务主要依赖锁、redo日志、undo日志实现。

锁可以实现隔离性redo日志实现原子性和持久性,undo日志实现一致性。

数据库事务的隔离级别有4种,由低到高分别为Read uncommitted (读未提交,存在脏读问题)、Read committed(读已提交,存在不可重复读问题) 、Repeatable read (重复读,存在幻读问题)、Serializable (序列化解决了脏读、不可重复读、幻读的问题)。这个是标准定义,但是不同数据实现的时候,会有差异,例如MySQL数据库的InnoDB默认隔离级别是可重复读,但是却解决了幻读的问题。

  • 脏读问题:一个事务可以读取另一个未提交事务的数据,如果这个未提交的事务回滚,就会出现读取的数据和实际不一致的问题。若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。(两个事务操作的相同的数据,是UPDATE和DELETE操作导致的问题)
  • 不可重复读问题:一个事务范围内有两个相同的查询,但是这两个查询中间执行了另外一个事务,导致同一个事务的两次相同查询却返回了不同数据,这就是不可重复读。(两个事务操作的相同的数据,是UPDATE和DELETE操作导致的问题)
  • 幻读问题:例如在事务A内执行了两次范围查询操作(例如id>0),在两次查询中间,事务B向该表插入了一条新的数据,并提交。事务A两次查询的结果将出现不一致。另一个例子是事务A首先判断id = 2的记录不存在,然后再向数据库插入id = 2的记录。在查询之后,插入之前,如果事务B插入了id = 2的记录也会存在问题。因此幻读本质原因是,一个事务的插入操作,对另一个事务在执行期间是可见的。

备注:不可重复读和幻读的区别。不可重复读重点在于update和delete,而幻读的重点在于insert。如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

2.3 Spring 事务注解 @Transaction

Spring 事务注解是依赖数据库事务实现的,本质是使用的就是数据库事务。@Transaction 原理是使用AOP动态代理,在目标方法前和后,增加相应的事务操作。

@Transaction 可以加在public方法上,也可以加到类上。如果加在类上,类下所有的public方法均会开启事务。

错误使用@Transaction的3种常见场景:

1、@Transaction 没有配置 rollbackFor。@Transactional的rollbackFor用于指定能够触发事务回滚的异常类型,可以指定多个,用逗号分隔,如果事务内部抛出了配置异常、或是子类,就会触发回滚操作。如果没有配置rollbackFor,默认值是UncheckedException,包括RuntimeException和Error。RuntimeException是Exception的子类,因此默认情况下Exception异常不会触发回滚。

2、Spring 的 AOP 的自调用问题。类A里面有两个方法:a和b,如果a声明了@Transaction开启事务,在b的内部调用了a,那么事务是无效的。只有外部类方法调用才可以正确开启事务。

3、@Transactional 只能应用到 public 方法。

备注:2和3都是Spring AOP机制导致的。(为解决这两个问题,可以使用 AspectJ 取代 Spring AOP 代理。)

 

Spring事务

事务的传播特性

以下是事务的7种传播级别:

1、PROPAGATION_REQUIRED(支持当前事务)

默认的Spring事务传播级别,使用该级别的特点是:如果上下文中已经存在事务,那么就加入到事务中执行;如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。

2、PROPAGATION_SUPPORTS(支持当前事务)

从字面意思就知道,supports,支持,该传播级别的特点是:如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包含在TransactionTemplate.execute方法中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。

3、PROPAGATION_MANDATORY(支持当前事务)

该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。

4、PROPAGATION_REQUIRES_NEW(不支持当前事务)

从字面即可知道,new,每次都要一个新事务,该传播级别的特点是:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。

这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。

怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。

5、PROPAGATION_NOT_SUPPORTED(不支持当前事务)

这个也可以从字面得知,not supported,不支持,当前级别的特点是:若上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。

这个级别有什么好处?可以帮助你将事务尽可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况,所以事务的这个传播级别就派上用场了。用当前级别的事务模板包含起来就可以了。

6、PROPAGATION_NEVER

该事务更严格,PROPAGATION_NOT_SUPPORTED只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。

7、PROPAGATION_NESTED

从字面也可知道,nested,嵌套级别事务。该传播级别的特征是:如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

嵌套是子事务嵌套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:

(1)如果子事务回滚,会发生什么?
父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。

(2)如果父事务回滚,会发生什么?
父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。

(3)事务的提交,是什么情况?
是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。

以上是事务的7个传播级别,在日常应用中,通常可以满足各种业务需求,但是除了传播级别,在读取数据库的过程中,如果两个事务并发执行,那么彼此之间的数据是如何影响的呢?这就需要了解一下事务的另一个特性:数据隔离级别。

备注 事务挂起的含义:函数B在事务A里面执行,函数B挂起了事务A,那么函数B的任何操作都不被包含在事务A里面。(可以测试下)

事务的隔离级别

数据隔离级别分为不同的4种:
1、SERIALIZABLE
最严格的级别,事务串行执行,资源消耗最大。

2、REPEATABLE_READ
保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。

3、READ_COMMITTED
大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。

4、READ_UNCOMMITTED
保证了读取过程中不会读取到非法数据。

/**
 * 1.添加事务注解
 * 使用propagation 指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时如何使用事务。
 * 默认取值为REQUIRED,即使用调用方法的事务
 * REQUIRES_NEW:使用自己的事务,调用的事务方法的事务被挂起。
 * 2.使用isolation 指定事务的隔离级别,最常用的取值为READ_COMMITTED
 * 3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置。通常情况下,默认值即可。
 * 4.使用readOnly 指定事务是否为只读。 表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。若真的是一个只读取数据库值得方法,应设置readOnly=true
 * 5.使用timeOut 指定强制回滚之前事务可以占用的时间。
 *
@Transactional(propagation=Propagation.REQUIRES_NEW,
        isolation=Isolation.READ_COMMITTED,
        noRollbackFor={UserAccountException.class},
        readOnly=true, timeout=3)

备注 readOnly=true 为只读事务,两方面作用:数据库可以针对只读事务做优化,如果执行了写操作,会抛异常。

 

2.4 分布式事务

对于一些强一致性的业务场景,分布式事务是保证系统正确运营的关键。

https://blog.csdn.net/ljheee/article/details/99696571

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值