在上一篇使用JdbcTemplate配置Dao时遇到了事务问题,那么我们今天就详细的说说事务,包括概念、分类、传播行为、隔离级别等等。
一、首先说说事务
在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
看看事务的四个关键属性(ACID):
① 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
② 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
③ 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
④ 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
二、接下来说说Spring事务管理
1、编程式事务管理
- 使用原生的JDBC API进行事务管理步骤:
①获取数据库连接Connection对象
②取消事务的自动提交
③执行操作
④正常完成操作时手动提交事务
⑤执行失败时回滚事务
⑥关闭相关资源 - 优缺点:
要将事务管理代码嵌入到业务方法中来控制事务 的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务 管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块 都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
2、声明式事务管理
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
事务最重要的两个特性,是事务的传播级别和数据隔离级别。传播级别定义的是事务的控制范围,事务隔离级别定义的是事务在数据库读写方面的控制范围。接下来就具体说一下。
三、事务的传播行为
事务的传播行为就是说当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由传播属性指定。Spring定义了以下7种类传播行为:
传播属性 | 描述 |
---|---|
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行 |
REQUIRED_NEW | 当前的方法必须启动新事务,并在它自己的事务运行,如果有事务在运行,应该将它挂起 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中 |
NOT_SUPPORTED | 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起 |
MADATORY | 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常 |
NEVER | 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常 |
NESTED | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行 |
具体说明:
① REQUIRED 传播行为*
当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书也买不了。
② REQUIRED_NEW 传播行为*
表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。
③ SUPPORTS 传播行为
从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
④ NOT_SUPPORTED 传播行为
这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
⑤ MANDATORY 传播行为
该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
⑥ NEVER 传播行为
该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
⑦ NESTED 传播行为
字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。嵌套级别事务子事务是依托于父事务存在的。
注意:
事务传播属性可以在@Transactional注解的propagation属性中定义。
在Spring2.x 事务通知中,可以在<tx:method>元素汇总设定传播事务属性。
以上是事务的7个传播级别,在日常应用中,通常可以满足各种业务需求,但是除了传播级别,在读取数据库的过程中,如果两个事务并发执行,那么彼此之间的数据是如何影响的呢?
这就需要了解一下事务的另一个特性:数据隔离级别
四、事务的隔离级别
先说说数据库事务并发问题:
假设现在有两个事务:Transaction01和Transaction02并发执行。
1) 脏读
所谓的脏读,其实就是读到了别的事务回滚前的脏数据。
例如:
① Transaction01将某条记录的AGE值从20修改为30。
② Transaction02读取了Transaction01更新后的值:30。
③ Transaction01回滚,AGE值恢复到了20。
④ Transaction02读取到的30就是一个无效的值。
2)不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。
例如:
① Transaction01读取了AGE值为20。
② Transaction02将AGE值修改为30。
③ Transaction01再次读取AGE值为30,和第一次读取不一致。
3)幻读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。
例如:
① Transaction01读取了STUDENT表中的一部分数据。
② Transaction02向STUDENT表中插入了新的行。
③ Transaction01读取了STUDENT表时,多出了一些行。
再来看看事务的隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
1)读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
保证了读取过程中不会读取到非法数据。
2)读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。
大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
3)可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
4)串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能是非低下。
5)各个隔离级别解决并发问题的能力见下表:
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
READ UNCOMMITTED | √ | √ | √ |
READ COMMITTED | × | √ | √ |
REPEATABLE READ | × | × | √ |
SERIALIZABLE | × | × | × |
6) 各个数据库产品对事务隔离级别的支持程度见下表:
Oracle | MySQL | |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
注意:
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别。
在Spring 2.x事务通知中,可以在tx:method元素中指定隔离级别。
下面说说两种方式配置一下声明书事务:
事务常用到的两个属性:
readonly(超时事务属性):事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
timeout(只读事务属性): 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
注意:
在Spring 2.x事务通知中,超时和只读属性可以在<tx:method>元素中进行指定。
触发事务回滚的异常:
① rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个。
② noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个。
是基于上一篇博客遇到事务问题的方法:
① 基于注解声明式事务配置:
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
rollbackFor={IOException.class,SQLException.class},
noRollbackFor=ArithmeticException.class,
readOnly=true,
timeout=30)
@Override
public void purchase(String userName, String isbn) {
//查询单价
int priceByIsbn = bsd.findBookPriceByIsbn(isbn);
//减库存
bsd.updateBookStock(isbn);
//减余额
bsd.updateAccount(userName, priceByIsbn);
System.out.println("购买成功!");
}
②基于XML文档的声明式事务配置:
<!-- 配置事务切面 -->
<aop:config>
<aop:pointcut
expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))"
id="txPointCut"/>
<!-- 将切入点表达式和事务属性配置关联到一起 -->
<aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/>
</aop:config>
<!-- 配置基于XML的声明式事务 -->
<tx:advice id="myTx" transaction-manager="transactionManager">
<tx:attributes>
<!-- 设置具体方法的事务属性 -->
<tx:method name="find*" read-only="true"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="purchase"
isolation="READ_COMMITTED"
no-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException"
propagation="REQUIRES_NEW"
read-only="false"
timeout="10"/>
</tx:attributes>
</tx:advice>