事物的特性
- 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
- 事务隔离(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事物的隔离级别
- 未提交读(Read uncommitted),最低的隔离级别,允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据。
- 读已提交(read committed),一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。(一个事物读两次的结果不一致)还会出现幻读的情况。
- 可重复读(repeatable read),一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。(其他事务新插入了新的数据,本事务每次查询能把其他事务新插入的数据读出来)
- 串行化(Serializable),最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。
Spring对事物的支持
编程式事务
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。
通过TransactionTemplate
不管有没有异常,只有 setRollbackOnly() 才会回滚,不触发这个方法,即使有异常也会自动提交;
@Service
public class TestTransactionTemplateService {
@Autowired
private UserMapper mapper;
@Autowired
private TransactionTemplate transactionTemplate;
public void start() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
User u = new User();
u.setName("0959");
u.setAge(11);
mapper.insert(u);
System.out.println(6/0);
} catch (Exception e) {
// 只有指定这个才会回滚
status.setRollbackOnly();
System.out.println("error"+status.toString());
}
}
});
}
}
通过TransactionManager
只有commit()才会提交,如果出现异常,并设置了commit()会自动提交;
@Autowired
private PlatformTransactionManager transactionManager;
public void doStart() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
User u = new User();
u.setName("1007");
u.setAge(12);
mapper.insert(u);
System.out.println(6/0);
// 手动提交
transactionManager.commit(status);
} catch (Exception e) {
// 手动回滚
transactionManager.rollback(status);
}
}
就编程式事务管理而言,Spring 更推荐使用 TransactionTemplate。
在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。
声明式事务
@Transactional
采用AOP,对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
默认的事务是
public @interface Transactional {
// 事务行为:有事务则加入事物,没事物则创建一个事物
Propagation propagation() default Propagation.REQUIRED;
// 隔离行为:默认是和DB隔离级别一致
Isolation isolation() default Isolation.DEFAULT;
// 超时时间:使用DB的超时时间
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
}
事务7种的传播行为
-
PROPAGATION_REQUIRED
指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 只要其中一个方法回滚,整个事务均回滚。 更确切地意思是:
-
如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
-
如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚;
// 调用insert()方法后,执行完testSameMethod()出现异常后,testSameMethod()也会回滚 @Transactional(rollbackFor = Exception.class) public void insert(User user) { userRepository.insert(user); System.out.println("insert"); userService.testSameMethod(user); // 这里抛出异常 testSameMethod()也会回滚 System.out.println("error:"+(5/0)); } @Transactional(rollbackFor = Exception.class) public void testSameMethod(User user) { user.setName(user.getName() + " A"); userRepository.insert(user); System.out.println("insert"); }
-
-
PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。
// 调用insert()方法后,执行完testSameMethod()后出现异常,testSameMethod()【不会】回滚 @Transactional(rollbackFor = Exception.class) public void insert(User user) { userRepository.insert(user); System.out.println("insert"); userService.testSameMethod(user); // 这里抛出异常 testSameMethod()不会回滚 System.out.println("error:"+(5/0)); } @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) public void testSameMethod(User user) { user.setName(user.getName() + " A"); userRepository.insert(user); System.out.println("insert"); } -
PROPAGATION_NESTED
如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。
-
PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
-
PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
-
PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
-
PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
Spring事务5种隔离级别
- ISOLATION_DEFAULT: 使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。
- ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,可能会出现脏读、幻读或者不可重复读;
- ISOLATION_READ_COMMITTED: 允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。
- ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- ISOLATION_SERIALIZABLE:最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。
事务超时时间
事务超时,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。
假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源
@Transactional(rollbackFor = Exception.class, timeout = 2)
public void insert(User user) {
System.out.println("insert");
// 放在这里不生效,他是基于执行sql时的当前时间和timeout做对比判定超时的
// userRepository.insert(user);
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
userRepository.insert(user);
}
@Transactional 的作用范围
- 类上,表明类中所有 public 方法都启用事务
- 方法上,最常用的一种
- 接口上,不推荐使用
事务失效
-
在同一个类内调用方法会失效
@Service public class UserService { @Autowired private UserRepository userRepository; @Autowired private UserService userService; @Transactional(rollbackFor = Exception.class) public void insert(User user) { userRepository.insert(user); System.out.println("insert"); try { testSameMethod(user); }catch (Exception e) { System.out.println("error"); } } @Transactional(rollbackFor = Exception.class) public void testSameMethod(User user) { user.setName(user.getName() + " A"); userRepository.insert(user); System.out.println("insert"); int a = 5-5; System.out.println("error:"+(5/a)); } }此时,调用insert()方法,testSameMethod()发生异常但并没有回滚,数据依旧插入成功。原因是加了注解的方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法若直接调用了this对象的方法则不会有事务。
解决方案:设置循环引用,在该类中注入自己,即可解决。
注:spring boot 2.6以上关闭了循环引用,需要通过修改yaml文件开启循环引用。
spring: main: allow-bean-definition-overriding: true allow-circular-references: true -
访问权限必须是成员public方法;
-
未被spring管理;
-
多线程调用;
spring的事务是通过数据库连接来实现的。当前线程中保存了一个 ThreadLocal< Map >,key是数据源,value是数据库连接。
@Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); // 一个事务中又开启了一个线程 new Thread(() -> { roleService.doOtherThing(); }).start(); } -
引擎不支持事务;
-
异常在内部捕获;
源码剖析
当在启动类加上@EnableTransactionManagement注解时,会往Spring容器中注入一个Config类 ProxyTransactionManagementConfiguration,这个类会创建一个拦截器bean叫TransactionInterceptor。我们最终执行加了@Transactional 类的方法,都会被该拦截器拦下,然后基于AOP设置事务;
相关名词
TransactionManager:事务管理器,只是一个空接口,具体的实现交给其实现类去实现。比如JDBC对应的DataSourceTransactionManager。
这个类中包含了数据源
DataSource,通过管理器可以创建和获取事务状态TransactionStatus,提交事务,回滚事务等事务相关操作。
DataSourceTransactionObject
事务核心操作对象。该类是DataSourceTransactionManager的内部类;其对象中包含,当前数据库连接conn,设置是否为只读事务,是否允许事务嵌套等,基于当前事务的一些状态值。
TransactionStatus
事务的状态。可由管理器创建管理,创建了一个DefaultTransactionStatus的实现类,他主要包含了DataSourceTransactionObject,以及事务属性TransactionDefinition,挂起事务SuspendedResourcesHolder等信息。
TransactionInfo
事务整体信息。他是Spring事务切面类TransactionAspectSupport的内部类。包含了事务管理器,事务属性,安全点,事务状态等信息;它是Spring用来管理事务和线程之间的关系的。
切面类内部有许多TheadLocal,用于保存不同线程的事务信息。
TransactionDefinition
事务属性。定义了事务的超时时间,隔离属性,传播属性等;TransactionAttribute是其实现类。
SuspendedResourcesHolder
事务挂起封装。包含了挂起事务的DataSourceTransactionManager.DataSourceTransactionObject核心对象,以及TransactionSynchronization,主要解决,当本次事务结束后,恢复上一个挂起的事务;
TransactionSynchronizationManager对象
他是一个全局的事务同步管理器,内置了许多静态的ThreadLocal对象,用于保存不同线程间的事务属性。
// 保存当前线程当前事务内的(DataSource, ConnectionHolder)
ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// 设置事务活跃状态
ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
// 设置事务隔离状态,没有就是null; NULL表示使用数据库的默认隔离状态
ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
// 设置是否为只读事务,如果是则会让conn执行sql'SET TRANSACTION READ ONLY'
ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
// 事务名称-就是方法名全称
ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
// 【同步链】一个list,每次执行sql的时候
// 由mybatis会将会话连接放入其中
// 若没有事务,每个sql都会创建sqlsession
// 若有事务,事务中的sql都会共用一个sqlsession
ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
TransactionAspectSupport 他是Spring事务AOP中管理线程txInfo的对象
// 保存线程当前的txInfo
ThreadLocal<TransactionInfo> transactionInfoHolder =
new NamedThreadLocal<>("Current aspect-driven transaction");
Spring事务基于AOP的基础模型
本次分析嵌套事务失败的场景,同时会分析事务成功后的场景。
demo示例代码
// 入口方法
@Transactional(rollbackFor = Exception.class)
public void insert(User user) {
userRepository.insert(user);
//userService.testSameMethod(user);
//System.out.println("error:"+(5/0));
}
@Transactional(rollbackFor = Exception.class)
public void testSameMethod(User user) {
user.setName(user.getName() + " A");
userRepository.insert(user);
System.out.println("insert" + 9/0);
}
step1:执行insert方法
流程图
从流程图可以看出,我们的核心入口方法应该是事务切面方法TransactionAspectSupport.invokeWithinTransaction()
-
获取事务管理器TM。我们根据事务属性对象
TransactionAttribute获取我们的事务管理TM。TM实际上在本次案例中就是我们的DataSourceTransactionManager,它里面包含了数据源对象dataSource; -
创建事务信息txInfo。txInfo包含了事务管理器,事务属性,上一个事务txInfo等数据,他是SpringAop管理事务的对象,SpringAop会将当前txInfo绑定在线程上,记录当前线程执行的事务信息。
-
创建事务状态TS。在创建txInfo之前,我们需要先创建事务状态。而事务状态是由事务管理器创建的。事务状态对象中,主要包括挂起事务信息,事务核心操作对象等。
-
创建事务核心对象txObj。
DataSourceTransactionObject是DataSourceTransactionManager事务管理器的内部类对象,他其中会包含一个connectHolder,就是事务管理器分配的DB连接管理器,最终都是由这个holder持有的连接去操作事务。这个holder实际上是从TransactionSynchronizationManager.resources获取,这是一个ThreadLocal对象,为了该线程上所有conn都是同一个连接。所以第一次走到这里实际上holder是空的,需要后面第6步去设置。
-
执行startTransaction();
该方法会创建事务状态TS。事务状态中就包含了事务属性,事务核心对象txObj,newTransaction表示是否为第一次创建事务(顶层事务)以及挂起对象;
-
执行doBegin();
这里会从事务核心对象txObj中取出连接conn,设置连接为不自动提交,并且将dataSource和connHolder进行绑定。也就是前面说的绑定到
TransactionSynchronizationManager.resources中; -
执行prepareSynchronization();
在同一个线程里,通过将当前相关对象保存到TransactionSynchronizationManager静态类的ThreadLocal成员属性中即可;
由于这是第一个事务,所以这里TransactionSynchronizationManager会设置 当前事务活跃 当前隔离状态为默认,设置是否只读事务,设置事务名称(加了注解的方法全名),并且初始化synchronizations集合(new一个set<>);
-
上述执行完后,会返回创建好的TS;
-
执行prepareTransactionInfo();在我们创建好事务状态以后,我们就要开始创建由SpringAop管理的TxInfo对象了。TxInfo这个对象是AOP管理需要的,他保存了事务管理器tm,属性,以及事务状态ts。创建好以后会执行txInfo.bindToThread(),将该txInfo绑定到线程TransactionAspectSupport.transactionInfoHolder上;(名词解释有讲);
-
接下来就是开始执行原本的业务逻辑,try{}catch{}包裹。这里就涉及到事务嵌套,我们开始看接下来的流程。
step2: 执行testSameMethod()
-
本次进入了第二个事务中,这里仍进入的是切面的入口
TransactionAspectSupport.invokeWithinTransaction()。 -
获取事务管理器TM。和第一次获取的是同一个,TM是单例。
-
创建事务信息TxInfo;
-
创建事务状态TxStatus;
-
创建事务核心对象txObj;这次设置connHolder时,就可以从事务管理器中获得上一次创建的holder连接对象了。
-
执行
handleExistingTransaction()返回TS;由于嵌套事务的传播级别是同一个事务。因此这里isExistingTransaction()返回的是true,表示事务存在,走存在事务的逻辑。该方法内的逻辑是标记当前事务不是第一个事务,执行
prepareTransactionStatus()。 和startTransaction()方法逻辑比起来,少了dobegin();注意,如果嵌套事务是REQUIRES_NEW,则这里会挂起上个事务并走startTransaction();
-
执行prepareTransactionStatus();创建事务状态,由于当前事务不是第一个事务,因此
prepareSynchronization()方法中不会再触发一遍。(这个方法的作用就是上面第7点写的); -
执行prepareTransactionInfo()。创建完TS后,创建txInfo对象,逻辑和上面第一次的一样,创建好后,执行
txInfo.bindToThread(); -
执行try-catch逻辑;
-
此时由于原方法逻辑会抛出异常,因此会走到catch中;catch中先会回滚,再向上抛出异常。
-
catch域的completeTransactionAfterThrowing()方法;
-
TM的processRollback();当AOP切面抛出异常后,回滚逻辑交给TM去完成。
-
triggerBeforeCompletion();只有第一个事务状态TS才会执行同步链(事务管理器的synchronizations)triggerBeforeCompletion();因此本次不执行;
-
执行doSetRollbackOnly();这里就是说从TS中取出事务对象txObject,将connHolder 标记为回滚状态;注意,此处并不是真的回滚只是标记;
-
triggerAfterCompletion();只有第一个事务状态TS才会执行,同步链上的相应逻辑;
-
cleanupAfterCompletion();标记TS状态为完成;表示此时事务的状态完成了;
-
走finally逻辑。cleanupTransactionInfo(); 清除事务信息,让当前线程持有之前的TxInfo transactionInfoHolder.set(this.oldTransactionInfo);
至此嵌套事务执行完毕,回到第一个事务的try-catch中;
step3: 第一个事务继续执行catch逻辑
-
TM的processRollback();当AOP切面抛出异常后,回滚逻辑交给TM去完成。
-
triggerBeforeCompletion(); 由于是第一个事务会执行TransactionSynchronizationUtils.triggerBeforeCompletion();核心逻辑就是,如果会话Holder关闭了就关闭sqlSeesion;会话close()
-
核心回滚方法 doRollback();第一个事务(最外层事务)才能执行回滚,因此这里会执行回滚,实际上执行的是
conn.rollback(); -
triggerAfterCompletion(); 第一个事务才允许执行就是执行同步链上的afterCompletion(),如果当前会话holder活跃就关闭会话;
-
cleanupAfterCompletion(); 标记TS状态为完成;表示此时事务的状态完成了;恢复连接的自动提交;重置线程持有事务状态TransactionSynchronizationManager;
synchronizations.remove(); currentTransactionName.remove(); currentTransactionReadOnly.remove(); currentTransactionIsolationLevel.remove(); actualTransactionActive.remove();
若事务没有异常则走提交逻辑
-
commitTransactionAfterReturning(txInfo); 实际上就是从txInfo中取出TM,执行commit方法;
-
核心方法processCommit();
-
triggerBeforeCommit(); 核心逻辑是sqlsession执行commit();
-
triggerBeforeCompletion(); 核心逻辑是sqlsession执行close();
-
doCommit(); 核心逻辑是conn.commit();
-
triggerAfterCommit();是个空方法体;
-
cleanupAfterCompletion(); 标记TS为完成状态,恢复自动提交,重置TransactionSynchronizationManager
synchronizations.remove(); currentTransactionName.remove(); currentTransactionReadOnly.remove(); currentTransactionIsolationLevel.remove(); actualTransactionActive.remove();