Spring有5个隔离级别,7个传播行为。这是面试中经常被问到的,也是代码中经常遇到的知识点。知识枯燥乏味,有些还很绕口。要是落在这上面就太可惜了。xjjdog在一些交易的基础上,讨论了几个容易忘记的概念,从源码层面找原因,加深理解。问题大概包括:Spring的事务和数据库事务隔离是一个概念吗?Spring是如何实现事务的?事务隔离机制有哪些?什么是事务传播机制?查询语句需要开启事务吗?给私有方法加上事务注解有用吗?1、Spring的事务和数据库事务隔离是一个概念吗?先来一个问题,Spring的事务隔离级别和数据的事务隔离级别是一回事吗?实际上,数据库中一般只有4种隔离机制。Spring抽象出一个默认值,并根据数据设置改变它。未提交读(uncommittedread)已提交读(committedread,不可重复读)可重复读(repeatableread)可序列化(serializable)默认(PlatformTransactionManager默认隔离级别,使用默认数据库)这是因为,Spring只提供了统一的事务管理接口,具体实现由各个数据库(如MySQL)实现。Spring会在事务开始时根据当前环境设置的隔离级别调整数据库隔离级别,以保持一致性。在DataSourceUtils文件中,代码详细输出了这个过程。//Applyspecificisolationlevel,ifany.IntegerpreviousIsolationLevel=null;if(definition!=null&&definition.getIsolationLevel()!=TransactionDefinition.ISOLATION_DEFAULT){if(logger.isDebugEnabled()){logger.debug("ChangingisolationlevelofJDBCConnection["+con+"]to"+definition.getIsolationLevel());}intcurrentIsolation=con.getTransactionIsolation();if(currentIsolation!=definition.getIsolationLevel()){previousIsolationLevel=currentIsolation;con.setTransactionIsolation(definition.getIsolationLevel());}}结论:三个IfSpring没有指定事务隔离级别,会使用数据库默认的事务隔离级别;当Spring指定事务隔离级别时,会在代码中修改事务隔离级别为指定值;当数据库不支持该隔离级别时,效果以数据库为准(如使用MyISAM引擎)。我们将使用下面的方法来声明。如果没有性能和需求问题,不要盲目改动。如果事务处理没做好,就会锁表,锁表在大并发的情况下会杀人。@Transactional(isolation=Isolation.READ_UNCOMMITTED)2、只要针对Spring事务的七种传播机制编写代码,代码中总会存在嵌套或者循环,导致事务的嵌套或者循环。在这些情况下,事务将根据配置做出不同的反应。必需这是默认值。指示当前方法必须在具有事务的上下文中运行。如果客户端有事务正在进行,被调用端将在事务中运行,否则,重新启动事务。(如果被调用端发生异常,调用端和被调用端事务都会回滚)REQUIRE_NEW表示当前方法必须运行在自己的事务中。如果有当前事务,则当前事务在方法执行过程中会被挂起。NESTED如果当前方法有事务运行,则该方法应该在嵌套事务中运行。嵌套事务可以独立于嵌套事务。在封装的事务中提交或回滚。如果封装的事务存在,外层事务抛出异常回滚,则必须回滚内层事务,否则内层事务不影响外层事务。如果封装的事务不存在,则与要求相同。SUPPORTS表示当前方法不需要有事务上下文,但是如果有事务,也可以在这个事务中运行。NOT_SUPPORTED表示该方法不应在事务中运行。如果有事务在运行,运行期间会被挂起,直到事务提交或回滚恢复执行MANDATORY表示当前方法必须运行在一个事务中,如果没有事务,则抛出异常NEVER表示当前方法不应该在事务中运行。如果有事务,抛出异常的通常是REQUIRED和REQUIRES_NEW。如果使用其他的,在使用之前必须小心并了解它们。最怕如果用这两个词,事情会变得很复杂,尤其是代码量大的时候,你永远不知道谁会用你写的服务。真尴尬。我们将通过以下方式进行声明。鉴于Spring的事务传播非常复杂,如果功能满足要求,那就使用默认的,否则会造成不必要的麻烦。@Transactional(propagation=Propagation.REQUIRED)3.事务传播机制是如何实现的?事务传播机制看似神奇,其实是使用简单的ThreadLocal机制实现的。因此,如果在新线程中调用被调用方法,事务传播实际上会失败。这和我们之前讲的透传不同,Spring不做这种处理。因此,事务传播机制只能通过查看源代码才能有印象。只靠文字去传播,很多东西都会变得不可描述。如图所示,PlatformTransactionManager只有三个简单的抽象接口,定义了包括JDBC在内的Spring的所有事务操作。我们平时说的JDBC只占了一部分。实现方式仍然是使用AOP实现,具体实现类由TransactionAspectSupport实现。可以看出,代码中定义了一个ThreadLocal变量,叫做transactionInfoHolder。使用的时候可以保证在同一个线程下获取的变量是一致的。/***Holdertosupportthe{@codecurrentTransactionStatus()}方法,*并支持不同合作通知之间的通信*(例如beforeandafteradvice)如果方面涉及多个*单一方法(aswillbethecaseforaroundadvice)。*/privatestaticfinalThreadLocaltransactionInfoHolder=newNamedThreadLocal<>("Currentaspect-driventransaction");,在invokeWithinTransaction中实现。继续往下追查,会发现AbstractPlatformTransactionManager类中有getTransaction方法。@OverridepublicfinalTransactionStatusgetTransaction(@NullableTransactionDefinitiondefinition)throwsTransactionException{//使用defaultsifnotransactiondefinitiongiven.TransactionDefinitiondef=(definition!=null?definition:TransactionDefinition.withDefaults());Objecttransaction=doGetTransaction();booleandebugEnabled=logger.isDebugEnabled();if(isExistingTransaction(transaction)){//Existingtransactionfound->checkpropagationbehaviortofindouthowtobehave.returnhandleExistingTransaction(def,transaction,debugEnabled);}//Checkdefinitionsettingsfornewtransaction.if(def.getTimeout()checkpropagationbehaviortofindouthowtoproceed.if(def.getPropagationBehavior()==TransactionDefinition.PROPAGATION_MANDATORY){thrownewIllegalTransactionStateException("Noexistingtransactionfoundfortransactionmarkedwithpropagation'强制'");}elseif(def.getPropagationBehavior()==TransactionDefinition.PROPAGATION_REQUIRED||def.getPropagationBehavior()==TransactionDefinition.PROPAGATION_REQUIRES_NEW||def.getPropagationBehavior()==TransactionDefinition.PROPAGATION_NESTED){SuspendedResourcesHoldersuspendedResources=(null);if(debugEnabled){logger.debug("Creatingnewtransactionwithname["+def.getName()+"]:"+def);}try{returnstartTransaction(def,transaction,debugEnabled,suspendedResources);}catch(RuntimeException|Errorex){resume(null,suspendedResources);throwex;}}else{//创建“空”事务:noactualtransaction,butpotentiallysynchronization.if(def.getIsolationLevel()!=TransactionDefinition.ISOLATION_DEFAULT&&logger.isWarnEnabled()){logger.warn("自定义隔离级别已指定但未启动实际事务;"+"隔离级别将有效地被忽略:"+def);}booleannewSynchronization=(getTransaction同步()==SYNCHRONIZATION_ALWAYS);returnprepareTransactionStatus(def,null,true,newSynchronization,debugEnabled,null);}}我不需要解释太多,所有明显的逻辑都在代码中,事务在这里创建。4、查询方式可以不开启交易吗?事务有readonly,控制事务的只读属性,与事务是否开启无关。之前的一篇文章讲过通过设置readonly属性来控制statements的路由:《MySQL官方驱动》主从分离之谜(扫盲),其中使用了事务的一个属性,readonly,这最终体现在数据库连接层面。connection.setReadOnly(true);在Spring中使用如下:@Transactional(readOnly=true)值得注意的是,设置该属性后,并不是每个底层数据库都支持。中间层的ORM或者driver也可能会用这个属性来做一些文章,所以这个readonly与其说是功能,不如说是一个提示。以MySQL为例,有两种提交模式:SETAUTOCOMMIT=0禁用自动提交SETAUTOCOMMIT=1启用自动提交这些都是真正的SQL语句,所以如果启用了事务,AUTOCOMMIT必须为false。我们可以看到Spring做了如下操作。con.setAutoCommit(false);如果是只读事务,别忘了手动设置。if(isEnforceReadOnly()&&definition.isReadOnly()){try(Statementsstmt=con.createStatement()){stmt.executeUpdate("SETTRANSACTIONREADONLY");}}这种操作非常昂贵。如果不加Transaction注解,默认是Donotstartatransaction。不需要为单个查询语句启动事务,数据库默认配置即可满足要求。但是如果一次执行多条查询语句,比如统计查询,报表查询,这种场景下,多条查询SQL必须保证整体读的一致性,否则,在上一条SQL查询之后,下一条SQL查询之前,数据被其他用户改了,会造成数据不一致。只有在这种情况下,才应该启动读取事务。5、私有方法加事务注解有用吗?给私有方法加上@Transaction注解是没有用的。这不是编写具有事务处理功能的代码。由于事务的这些功能是通过AOP方式强加的,所以是由动态代理来控制的。不会代理私有和最终修改的方法。但是,您可以将私有方法放在具有事务功能的公共方法中。这样看来,它看起来具有交易的一些功能特征,但实际上并没有。在EndInternet中,用到的交易不多,很多都是很小很快速的接口。对于开发者来说,交易是一种负担。但是在一些具有金融属性的业务中,或者在一些企业级的开发应用中,交易确实是一个绕不开的坎。一旦深入其中,就会发现这个知识点,张着血盆大口,等着你入瓮。xjjdog从源码层面讲了几个面试常见问题。不要惊讶,有些人一直在用脏读、幻读等术语面试。而这些东西都属于当时看到后恍然大悟的内容,第二天继续迷糊。我们什么时候可以更务实?作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。我的个人微信xjjdog0,欢迎加好友进一步交流