目录
前言
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。了解spring如何管理以及使用事务,我们首先了解一下数据库事务的相关特性。
数据库事务ACID特性
原子性(Atomicity):事务的原子性确保动作要么全部完成,要么完全不起作用,一旦发生错误,就会回滚到事务开始前的状态。
一致性(Consistency):指当一个事务可以改变封装状态(除非他是只读的),事务必须始终保持系统处于一致的状态,不管在给定的时间内并发事务有多少。
隔离性(Isolation):当有多个事务进行数据处理时,表示各个事务之间的隔离程度
持久性(Durability):一旦事务完成,该事务对数据库所做的更改变持久性的保存在数据之中,并不会被回滚。
事务的传播行为
属性 | 含义 |
---|---|
Require | 如果 A 方法存在事务,则 B 方法将直接使用A的事务。否则将创建新事务 |
Supports | 如果 A 方法存在事务,则 B 方法将直接使用A的事务。否则不使用事务 |
Mandatory | 方法必须在是事务内运行,如果 A 方法不存在事务,则抛出异常 |
RequiresNew | 无论 A 方法是否存在事务, B 方法都将创建新事务 |
NotSupported | 如果 A 方法不存在事务,则 B 方法将不使用A的事务。如果A方法存在事务,则将挂起他,等到方法调用结束时才回复当前事务 |
Never | 不支持事务,只有在没有事务的方法才能运行,如果A存在事务则将抛出异常 |
Nested | 嵌套事务,调用方法时如果抛出异常只回滚自己内部执行的SQL,不会回滚主方法的SQL,他的实现有两种方式,如果当前数据库支持保存点,那莫他就会使用保存点技术,一旦发生异常就会回滚到设置保存点的位置,而不是全部回滚,否则就等同于 RequiresNew创建新事务 |
事务的隔离级别
事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。
并发事务引起的问题:在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致以下的问题。
类别 | 含义 |
---|---|
脏读 | 一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。 |
不可重复读 | 一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新(同一条数据,更新)。 |
幻读 | 幻读与不可重复读类似,它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,因为事务1并不知道谁做了什么,他就会以为是产生的幻读 (多条数据,增加,删除) |
解决以上的问题
隔离级别 | 脏读 | 可重复读 | 幻读 |
---|---|---|---|
Read-Uncommitted | x | x | x |
Read-Committed | √ | x | x |
Repeatable-Read | √ | √ | x |
Serializable | √ | √ | √ |
Read-Committed是可以解决脏读的,因为脏读会读到未提交的数据,而Read-Committed隔离级别就是只能读到读已提交的数据,但同时Read-Committed也产生了新的问题,就是会读到不可重复读的问题例如:
妈妈 | 爸爸 |
---|---|
妈妈读取总金额 1000 | — |
爸爸读取到金额1000,消费100 剩余900,提交 | |
妈妈再次读取,发现剩余900 |
针对同一条数据而言,两次读取的数字不一致,就会产生不可重复读的问题,解决这个问题就是Repeatable-Read(可重复读),对同一条数据进行序列化操作(串行化),这里一定要理解是针对数据库中同一条数据而言的,那么就可以解决不可重复读的问题。但是大多数情况下,我们都会出现操作多条数据的情况,那就会出现幻读。
例:
妈妈 | 爸爸 |
---|---|
妈妈读取消费记录2次 | — |
爸爸消费100增加消费1次记录 | |
妈妈再次读取发现消费记录3次 |
但妈妈根本不知道谁操作了他的钱增加了消费记录,他就会以为是幻读出来的,解决幻读,就是使用Serializable(序列化)
1 序列化虽然能保证数据的完整性和一致性,但是对并发性能的影响也越大,会大大降低并发的性能。
2 大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
3 少数数据库默认隔离级别为:Repeatable Read 比如:MySQL InnoDB
不可重复读和幻读到底有什么区别呢?
(1) 不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2) 幻读是读取了其他事务新增的数据,针对insert和delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
Spring的事务机制
Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个
PlatformTransactionManager接口,不同的数据访问技术的事务使用不同的接口实现,如表所示。
数据访问技术 | 实现 |
---|---|
JDBC | DataSourceTransactionManager |
JPA | JpaTransactionManager |
Hibernate | HibernateTransactionManager |
JDO | JdoTransactionManager |
分布式 | JtaTransactionManager |
PlatformTransactionManager接口
PlatformTransactionManager 接口是 Spring 提供的平台事务管理器,用于管理事务。该接口中提供了三个事务操作方法,具体如下。
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager extends TransactionManager {
//用于获取事务状态信息
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//用于提交事务
void commit(TransactionStatus var1) throws TransactionException;
//用于回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
DataSourceTransactionManager(事务管理器继承上面接口的一个实现类 AbstractPlatformTransactionManager)的源码太过长,可以去IDEA中是直接查看。
在项目中,Spring 将 xml 中配置的事务详细信息(或者使用注解)封装到对象 TransactionDefinition(接口)的实现类中,然后通过事务管理器的 getTransaction() 方法获得事务的状态(TransactionStatus 接口),并对事务进行下一步的操作。
TransactionDefinition接口
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
//下面是事务的传播行为
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
//下面是事务的隔离级别
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
//超时时间
int TIMEOUT_DEFAULT = -1;
//获取传播行为
default int getPropagationBehavior() {
return 0;
}
//获取隔离级别
default int getIsolationLevel() {
return -1;
}
//获取超时时间
default int getTimeout() {
return -1;
}
//是否只读
default boolean isReadOnly() {
return false;
}
//获取事务对象名称
@Nullable
default String getName() {
return null;
}
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}
}
TransactionStatus接口
package org.springframework.transaction;
import java.io.Flushable;
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
//获取是否存在保存点(之后解释)
boolean hasSavepoint();
//刷新事务
void flush();
}
package org.springframework.transaction;
public interface TransactionExecution {
//获取是否是新事务
boolean isNewTransaction();
//设置事务回滚
void setRollbackOnly();
//获取是否回滚
boolean isRollbackOnly();
//获取事务是否完成
boolean isCompleted();
}
声明式事务
编程式事务就是之前我们写的Java代码,事务的定义获取、SQL执行、事务的提交回滚都由开发者自己实现;唯一的优点就是代码流程清晰,但不推荐使用,代码就不详述了。因为声名式事务的出现更方便,更简洁让开发人员只针对SQL的具体业务逻辑实现。大大提高了开发效率,所以声名式事务就大受欢迎,本篇博客也就讲讲声名式事务的应用以及原理。
声明式事务又可分为XML配置和注解事务,XML也已不常用,目前主流事务处理方法是@Transactional;
声名式事务采用的是AOP技术,而AOP技术底层的实现原理是动态代理
动态代理有两种实现方式:
Jdk:要求必须要有接口,生成的代理对象是挂在接口下的。
Cglib:没有接口,相当于生成子类对象。
实现动态代理的步骤:
1 建立真实对象和代理对象的关系。
2 编辑业务逻辑,会利用反射调用真实对象的方法。
不管是Jdk和还是Cglib都是这两步。
首先我们需要在xml文件里面加入事务管理器的配置(或者使用java配置都可以),Spring就知道已将数据库事务委托给事务管理器了;
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
然后使用@EnableTransactionManagement开启事务管理器,然后使用@Transactional注解放在需要的方法或类上。
我们可以设置该注解的一些属性如下:
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
/**
* propagation:事务的传播行为(多事务间进行操作,事务该怎莫进行管理)Spring框架事务的传播行为有7种和上面的传播行为一致
* ioslation:事务的隔离级别5个(并发操作产生的,多事务产生的3个问题)Isolation.DEFAULT这个代表默认的应用数据的隔离级别,其他四种和上面一致
* timeout:超时时间:事务必须在一定的时间内提交,否则就会回滚,默认为-1,就是没有超时
* readOnly:是否只读,默认值可以增删改查,为false,true就只能查询
* rollbackFor:回滚,设置出现哪些异常进行回滚
* noRollbackFor:不回滚,设置出现哪些异常不进行回滚
*/
然后就可以进行操作了
AOP在事务的使用
AOP是面向切面编程,通俗的来讲就是当你需要增加某些方法的时候,你不需要改变当前方法的源代码,而是使用AOP技术来实现。
利用AOP自定义简单的事务管理器.这个可以很好的理解AOP在事务的应用。
我主要说一下使用spring自带的事务管理器是如何应用的,原理是一样的。
在SpringIoc容器初始化的时候,会读取xml或者加注解@Transactional的设置的一些属性,然后将这些属性配置到这个TransactionDefinition接口的子类中.之后在运行的过程中会拦截那些使用事务注解的方法或类,spring会通过事务管理器创建事务,与此同时把事务定义中设置的属性往事务上设置,根据传播行为采取措施,然后通过反射调用开发者提供的业务逻辑代码,一旦发生异常且符合回滚条件的,就会回滚,否则就会提交事务。
@Transactional的自调用失效问题
当一个类中有多个方法都添加了事务注解,不能直接调用类中某个方法,因为事务底层使用的是动态代理,你如果直接调用相当于没用代理对象调用该方法那么事务就会失效。
解决办法:你可以重新获取当前类的代理对象,然后再次调用方法,就可以调用成功了。