3. 隔离级别
不同于”传播规则“是Spring提到的概念。隔离级别这个概念是数据库事务自带的,其目的是当存在多个事务时,如果这些事务访问到了共同的资源,该怎么处理事务间的数据隔离。一般来说数据库存在四种隔离级别,Spring则是定义了五种。它们分别如下
- DEFAULT 用数据库的设置隔离级别,数据库设置的是什么我就用什么
- UNCOMMITTED:读未提交,最低隔离级别、事务未提交前,就可被其他事务读取
- COMMITTED:读已提交,一个事务提交后才能被其他事务读取到
- REPEATABLE_READ:可重复读,本事务会建立快照,在快照建立前其他事务提交的内容可见,快照后提交的内容不可见
- SERIALIZABLE:序列化,事务必须一个执行完才允许建下个事务,隔离级别最高
这里面有两点需说明:
- 数据库事务的隔离级别有两种,一种是全局的,一种是会话的,与数据库建立连接后,可以为本会话设置隔离级别。如果不设置,则会使用数据库的隔离级别。这也就是Spring能指定隔离级别的原因,其实际上指定的是当前会话的隔离级别
- 数据库的隔离级别是数据库事务的重要内容,我们会在后续详解Mysql的时候,仔细说明各隔离级别的实现和其特点
4. 超时时间
我们前面说到,在@Transactional注解上可以填写超时时间,而它的单位是秒。但关于超时时间的设置有很多问题,或者说陷阱,希望大家通过学习后能够避开。
首先 timeout 的设置只适用于以下两种传播规则: REQUIRED 和 REQUIRES_NEW,其他的传播规则只允许超时默认值 -1 (即永不超时),否则会抛出异常
另外此处的超时时间,其实是在判断最后一个SQL执行前是否超时,我们以一个经典案例来说明
@Transactional(timeout = 2) public void methodA(UserInfo info) { userDao.updateInfo(info); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } @Transactional(timeout = 2) public void methodB(UserInfo info) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } userDao.updateInfo(info); }
我们不难看出,事务限时2秒,但方法执行至少3秒。那结果会如何呢?这里methodA不会报错,methodB会报错,但是是在第三秒(即线程恢复,开始执行Sql前)才报的错。
其原因是事务开启时,会读取当前时间 和 timeout 的值,并将其相加得到事务“过期时间”,而在每次执行sql前则会获取到statement,此时才会判断当前时间是否已超期,超期才会抛出超时异常。也就是说,通俗的理解,只有在每次执行SQL前,才会判断下是否已超过事务限时。
5. 是否只读
这里的只读是对开发者的提示,并不意味着你标注只读就真的不能写更新语句。实际上你更新,出错后回滚等功能都还是正常的。甚至该标注也支持 SUPPORTS 传播规则,说明即使没有开启事务也无妨。
我们可以看原文注释
This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts.A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.return
这只是作为实际事务子系统的提示,它不一定会导致写访问尝试失败。不能理解只读提示的事务管理器在请求只读事务时也不会抛出异常
6. 回滚 / 不回滚异常类型
这里就是说你可以显式的指定什么异常需要回滚,什么异常不需要回滚。比方说我们有一种邮件通知异常,代表通知邮件发送失败,但这种异常无伤大雅,只是非核心功能,失败了不需要导致整个业务失败。此时我们可以指定这种异常不需要回滚事务。
我们可以看看提到的默认需要回滚的异常
By default, a transaction will be rolling back on RuntimeException and Error but not on checked exceptions business exceptions
默认情况下,事务将在RuntimeException和Error上回滚,但不会在受检异常(业务异常)上回滚
五、@Transactional 的功能实现
1. 使用代理的好处
@Transactional 注解之所以好用,是因为对开发者来说,不需要写额外的控制代码。然而,我们知道对事务的控制必不可少,而这部分代码其实全都被放进了“代理”中,我们如果把没有代理的Bean比作小鸡,那么普通小鸡,和被代理的小鸡的工作模式如下图
被代理过的小鸡,能够执行代理对象里给定的方法,同时也能继续调用小鸡本身的方法,这就是Spring的重要特性 —— AOP,此处就是利用代理对原有的功能进行了增强,在增强的方法里加入了关于事务管理的内容
2. 启用事务与创建代理
我们可以利用@EnableTransactional注解来启用事务,其原因就是使用该注解后,Spring会创建一个针对事务增强的Advisor ,该Advisor可以扫描标注有@Transctional的地方。同时,Spring还启用了一个可以利用Advisor来创建代理的 后置处理器,这样在Bean被创建时,后置处理器发挥作用,开始搜寻所有Advisor(包括我们这个事务的advisor),最终为这个Bean创建出代理对象,这个代理对象,就能进行事务的配置
其具体流程。可以见下图
3. 代理功能及要点
我们上面看了,一个带有事务功能的代理对象被创建,那么这个代理到底干了些什么呢?是怎么管理事务的呢?如果按上面的流程图,此处的拦截器为TransactionInterceptor,其增强部分的方法为TransactionAspectSupport.invokeWithinTransaction
其源码如下:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // 如果事务的属性为空,代表这个方法不需要事务 final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); // 根据注解里的属性选择事务管理器 final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // 使用getTransaction和commit/rerollback调用进行标准事务划分。 // 如果有需要,则创建事务 TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // 方法执行完,返回后提交事务 commitTransactionAfterReturning(txInfo); return retVal; } else { // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in. try { Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, new TransactionCallback<Object>() { @Override public Object doInTransaction(TransactionStatus status) { TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); try { return invocation.proceedWithInvocation(); } catch (Throwable ex) { if (txAttr.rollbackOn(ex)) { // A RuntimeException: will lead to a rollback. if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else { throw new ThrowableHolderException(ex); } } else { // A normal return value: will lead to a commit. return new ThrowableHolder(ex); } } finally { cleanupTransactionInfo(txInfo); } } }); // Check result: It might indicate a Throwable to rethrow. if (result instanceof ThrowableHolder) { throw ((ThrowableHolder) result).getThrowable(); } else { return result; } } catch (ThrowableHolderException ex) { throw ex.getCause(); } } }
本文并不会针对源码层层展开,想看源码的上面已经给了具体的类和方法,可以自行阅览。这里只对关键部分进行表述。
3.1 基础功能
我们使用代理来实现事务,那么代理的基本功能或者说基本目的是什么呢?相信大家都明白,是在方法开始前新建一个事务 ,设置自动提交为否,方法结束后手动提交(上面代码已展示)
3.2 ThreadLocal
我们看到前述方法 invokeWithinTransaction 中,有一步“如果需要则创建事务”,而这里面第一步就是要获取事务,更准确的说是获取对数据库的连接。
这里面就用到了threadLocal,每一个线程都会去“TransactionSynchronizationManager”(事务同步管理器)中获取连接(内部包含一个对数据库的连接)。因为threadLocal的特性,我们知道,连接对象其实是存储在线程内的。而连接是事务的根基,把连接存于线程内,随着线程去经历不同方法,这是Spring事务能在不同方法间传播的基础保证
关于 Threadlocal 的解析,可以看我的另一篇博文: 图解,深入浅出带你理解ThreadLocal
3.3 传播特性与回滚
从里我们知道,线程会先取出自己存着的事务对象(连接)。此处就有三种情况了:
- 没有取到连接,即本线程还未连接数据库
- 有连接但连接并没有开启事务
- 有连接且已经开启事务
从事务角度看,就是两种情况,当前在事务中,或当前不在事务中。因此对于同一个传播属性就有两套逻辑了,举例来说,如果是@Transactional 里设置了传播属性是 REQUIRE,
那么如果当前有事务,则直接在这个事务内运行方法,如果当前没事务,就得新建事务,再在新建的事务内运行了
这里面比较特殊的是嵌套 NESTED 和 REQUIRED_NEW,为什么这么说?因为使用 这两种传播形式,即使当前已经有事务(连接),也必须新建一个事务(连接)。但我们说了,连接是以threadLocal的形式存在线程内的,而且只存了一个。那么这种情况,就会出现个问题: 新建事务(连接)后,原连接怎么处理?
对于 QUIRED_NEW 的处理,就是把当前信息的事务信息复制一份存起来并挂起,然后根据@Transacional注解里的属性重新填充当前事务信息,并重新取得一个连接。利用新的事务属性和新的连接去执行接下来的方法。因为是两个独立的连接与事务,所以外层方法和内部方法互不干涉,内部方法回滚,外部会毫无反应(除非你把异常往外抛,导致每层方法都吃到异常,那么每层都会回滚)
对于 NESTED 的处理,则利用了JDBC连接可以创建保存点的特性,会在此setSavePoint创建个保存点并保存下来。因为事实上用的同一个连接,中间有个保存点,所以一旦本方法内需要回滚,Spring会使用 rollback(savepoint) 恢复到本保存点,而不会全部回滚。但是如果是外部方法要回滚,就是全部回滚掉了