这周一,公司新来了一个同事,面试的时候表现得非常不错,各种问题对答如流,老板和我都倍感欣慰。 这么优秀的人,绝不能让他浪费一分一秒,于是很快,我就发他了需求文档、源码,让他先在本地熟悉一下业务和开发流程。 结果没想到,周三大家一块review代码的时候就发现了问题,新来的同事直接把原来Transactional优化成了这个鬼样子:Transactional(propagationPropagation。REQUIRED,rollbackForException。class) 就因为这一行代码,老板(当年也是一线互联网大厂的好手)当场就发飙了,马上就要劝退这位新同事,我就赶紧打圆场,毕竟自己面试的人,不看僧面看佛面,是吧?于是老板答应我说再试用一个月看看。 会议结束后,我就赶紧让新同事复习了一遍事务,以下是他自己做的总结,还是非常详细的,分享出来给大家一点点参考和启发。相信大家看完后就明白为什么不能这样优化Transactional注解了,纯属画蛇添足和乱用。关于事务 事务在逻辑上是一组操作,要么执行,要不都不执行。主要是针对数据库而言的,比如说MySQL。 只要记住这一点,理解事务就很容易了。在Java中,我们通常要在业务里面处理多个事件,比如说编程喵有一个保存文章的方法,它除了要保存文章本身之外,还要保存文章对应的标签,标签和文章不在同一个表里,但会通过在文章表里(posts)保存标签主键(tagid)来关联标签表(tags):publicvoidsavePosts(PostsParampostsParam){保存文章save(posts);处理标签insertOrUpdateTag(postsParam,posts);} 那么此时就需要开启事务,保证文章表和标签表中的数据保持同步,要么都执行,要么都不执行。 否则就有可能造成,文章保存成功了,但标签保存失败了,或者文章保存失败了,标签保存成功了这些场景都不符合我们的预期。 为了保证事务是正确可靠的,在数据库进行写入或者更新操作时,就必须得表现出ACID的4个重要特性:原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。事务隔离(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 其中,事务隔离又分为4种不同的级别,包括:未提交读(Readuncommitted),最低的隔离级别,允许脏读(dirtyreads),事务可以看到其他事务尚未提交的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据。提交读(readcommitted),一个事务可能会遇到不可重复读(NonRepeatableRead)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。可重复读(repeatableread),一个事务可能会遇到幻读(PhantomRead)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。串行化(Serializable),最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。 需要格外注意的是:事务能否生效,取决于数据库引擎是否支持事务,MySQL的InnoDB引擎是支持事务的,但MyISAM就不支持。关于Spring对事务的支持 Spring支持两种事务方式,分别是编程式事务和声明式事务,后者最常见,通常情况下只需要一个Transactional就搞定了(代码侵入性降到了最低),就像这样:TransactionalpublicvoidsavePosts(PostsParampostsParam){保存文章save(posts);处理标签insertOrUpdateTag(postsParam,posts);} 1)编程式事务 编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。 你比如说,使用TransactionTemplate来管理事务:AutowiredprivateTransactionTemplatetransactionTpublicvoidtestTransaction(){transactionTemplate。execute(newTransactionCallbackWithoutResult(){OverrideprotectedvoiddoInTransactionWithoutResult(TransactionStatustransactionStatus){try{。。。。业务代码}catch(Exceptione){回滚transactionStatus。setRollbackOnly();}}});} 再比如说,使用TransactionManager来管理事务:AutowiredprivatePlatformTransactionManagertransactionMpublicvoidtestTransaction(){TransactionStatusstatustransactionManager。getTransaction(newDefaultTransactionDefinition());try{。。。。业务代码transactionManager。commit(status);}catch(Exceptione){transactionManager。rollback(status);}} 就编程式事务管理而言,Spring更推荐使用TransactionTemplate。 在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解Spring的事务管理模型非常有帮助。 2)声明式事务 声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。 当然了,要想实现事务管理和业务代码的抽离,就必须得用到Spring当中最关键最核心的技术之一,AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。 声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。事务管理模型 Spring将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:publicinterfaceTransactionManager{} 该接口有两个子接口,分别是编程式事务接口ReactiveTransactionManager和声明式事务接口PlatformTransactionManager。我们来重点说说PlatformTransactionManager,该接口定义了3个接口方法:interfacePlatformTransactionManagerextendsTransactionManager{根据事务定义获取事务状态TransactionStatusgetTransaction(TransactionDefinitiondefinition)throwsTransactionE提交事务voidcommit(TransactionStatusstatus)throwsTransactionE事务回滚voidrollback(TransactionStatusstatus)throwsTransactionE} 通过PlatformTransactionManager这个接口,Spring为各个平台如JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。 参数TransactionDefinition和Transactional注解是对应的,比如说Transactional注解中定义的事务传播行为、隔离级别、事务超时时间、事务是否只读等属性,在TransactionDefinition都可以找得到。 返回类型TransactionStatus主要用来存储当前事务的一些状态和数据,比如说事务资源(connection)、回滚状态等。 TransactionDefinition。java:publicinterfaceTransactionDefinition{事务的传播行为defaultintgetPropagationBehavior(){returnPROPAGATIONREQUIRED;}事务的隔离级别defaultintgetIsolationLevel(){returnISOLATIONDEFAULT;}事务超时时间defaultintgetTimeout(){returnTIMEOUTDEFAULT;}事务是否只读defaultbooleanisReadOnly(){}} Transactional。javaTarget({ElementType。TYPE,ElementType。METHOD})Retention(RetentionPolicy。RUNTIME)InheritedDocumentedpublicinterfaceTransactional{Propagationpropagation()defaultPropagation。REQUIRED;Isolationisolation()defaultIsolation。DEFAULT;inttimeout()defaultTransactionDefinition。TIMEOUTDEFAULT;booleanreadOnly()}Transactional注解中的propagation对应TransactionDefinition中的getPropagationBehavior,默认值为Propagation。REQUIRED(TransactionDefinition。PROPAGATIONREQUIRED)。Transactional注解中的isolation对应TransactionDefinition中的getIsolationLevel,默认值为DEFAULT(TransactionDefinition。ISOLATIONDEFAULT)。Transactional注解中的timeout对应TransactionDefinition中的getTimeout,默认值为TransactionDefinition。TIMEOUTDEFAULT。Transactional注解中的readOnly对应TransactionDefinition中的isReadOnly,默认值为false。 说到这,我们来详细地说明一下Spring事务的传播行为、事务的隔离级别、事务的超时时间、事务的只读属性,以及事务的回滚规则。事务传播行为 当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。 声明式事务的传播行为可以通过Transactional注解中的propagation属性来定义,比如说:Transactional(propagationPropagation。REQUIRED)publicvoidsavePosts(PostsParampostsParam){} TransactionDefinition一共定义了7种事务传播行为: 01、PROPAGATIONREQUIRED 这也是Transactional默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:如果外部方法没有开启事务的话,Propagation。REQUIRED修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。如果外部方法开启事务并且是Propagation。REQUIRED的话,所有Propagation。REQUIRED修饰的内部方法和外部方法均属于同一事务,只要一个方法回滚,整个事务都需要回滚。ClassA{Transactional(propagationPropagation。PROPAGATIONREQUIRED)publicvoidaMethod{dosomethingBbnewB();b。bMethod();}}ClassB{Transactional(propagationPropagation。PROPAGATIONREQUIRED)publicvoidbMethod{dosomething}} 这个传播行为也最好理解,aMethod调用了bMethod,只要其中一个方法回滚,整个事务均回滚。 02、PROPAGATIONREQUIRESNEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation。REQUIRESNEW修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。ClassA{Transactional(propagationPropagation。PROPAGATIONREQUIRED)publicvoidaMethod{dosomethingBbnewB();b。bMethod();}}ClassB{Transactional(propagationPropagation。REQUIRESNEW)publicvoidbMethod{dosomething}} 如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为bMethod()开启了独立的事务。但是,如果bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚。 03、PROPAGATIONNESTED 如果当前存在事务,就在当前事务内执行;否则,就执行与PROPAGATIONREQUIRED类似的操作。 04、PROPAGATIONMANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 05、PROPAGATIONSUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 06、PROPAGATIONNOTSUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 07、PROPAGATIONNEVER 以非事务方式运行,如果当前存在事务,则抛出异常。 3、4、5、6、7这5种事务传播方式不常用,了解即可。事务隔离级别 前面我们已经了解了数据库的事务隔离级别,再来理解Spring的事务隔离级别就容易多了。 TransactionDefinition中一共定义了5种事务隔离级别:ISOLATIONDEFAULT,使用数据库默认的隔离级别,MySql默认采用的是REPEATABLEREAD,也就是可重复读。ISOLATIONREADUNCOMMITTED,最低的隔离级别,可能会出现脏读、幻读或者不可重复读。ISOLATIONREADCOMMITTED,允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。ISOLATIONREPEATABLEREAD,对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATIONSERIALIZABLE,最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。 通常情况下,我们采用默认的隔离级别ISOLATIONDEFAULT就可以了,也就是交给数据库来决定,可以通过SELECT命令来查看MySql的默认隔离级别,结果为REPEATABLEREAD,也就是可重复读。 事务的超时时间 事务超时,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。 假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。事务的只读属性 如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。 为什么一个查询操作还要启用事务支持呢? 这是因为MySql(innodb)默认对每一个连接都启用了autocommit模式,在该模式下,每一个发送到MySql服务器的SQL语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。 那如果我们给方法加上了Transactional注解,那这个方法中所有的SQL都会放在一个事务里。否则,每条SQL都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。 有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条SQL查询后,被其他用户改变了数据,那么下一个SQL查询可能就会出现不一致的状态。事务的回滚策略 默认情况下,事务只在出现运行时异常(RuntimeException)时回滚,以及Error,出现检查异常(checkedexception,需要主动捕获处理或者向上抛出)时不回滚。 https:tobebetterjavaer。comexceptiongailan。html 如果你想要回滚特定的异常类型的话,可以这样设置:Transactional(rollbackForMyException。class)关于SpringBoot对事务的支持 以前,我们需要通过XML配置Spring来托管事务,有了SpringBoot之后,一切就变得更加简单了,只需要在业务层添加事务注解(Transactional)就可以快速开启事务。 也就是说,我们只需要把焦点放在Transactional注解上就可以了。Transactional的作用范围类上,表明类中所有public方法都启用事务方法上,最常用的一种接口上,不推荐使用Transactional的常用配置参数 虽然Transactional注解源码中定义了很多属性,但大多数时候,我都是采用默认配置,当然了,如果需要自定义的话,前面也都说明过了。Transactional的使用注意事项总结 1)要在public方法上使用,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。protectedTransactionAttributecomputeTransactionAttribute(Methodmethod,NullableC?targetClass){Dontallownopublicmethodsasrequired。if(allowPublicMethodsOnly()!Modifier。isPublic(method。getModifiers())){}Themethodmaybeonaninterface,butweneedattributesfromthetargetclass。Ifthetargetclassisnull,themethodwillbeunchanged。MethodspecificMethodAopUtils。getMostSpecificMethod(method,targetClass);Firsttryisthemethodinthetargetclass。TransactionAttributetxAttrfindTransactionAttribute(specificMethod);if(txAttr!null){returntxA}Secondtryisthetransactionattributeonthetargetclass。txAttrfindTransactionAttribute(specificMethod。getDeclaringClass());if(txAttr!nullClassUtils。isUserLevelMethod(method)){returntxA}if(specificMethod!method){Fallbackistolookattheoriginalmethod。txAttrfindTransactionAttribute(method);if(txAttr!null){returntxA}Lastfallbackistheclassoftheoriginalmethod。txAttrfindTransactionAttribute(method。getDeclaringClass());if(txAttr!nullClassUtils。isUserLevelMethod(method)){returntxA}}} 2)避免同一个类中调用Transactional注解的方法,这样会导致事务失效。 更多事务失效的场景测试事务是否起效 在测试之前,我们先把SpringBoot默认的日志级别info调整为debug,在application。yml文件中修改:logging:level:org:hibernate:debugspringframework:web:debug 然后,来看修改之前查到的数据: 开搞。在控制器中添加一个update接口,准备修改数据,打算把沉默王二的狗腿子修改为沉默王二的狗腿:RequestMapping(update)publicStringupdate(Modelmodel){UseruseruserService。findById(2);user。setName(沉默王二的狗腿);userService。update(user);} 在Service中为方法加上Transactional注解并抛出运行时异常:OverrideTransactionalpublicvoidupdate(Useruser){userRepository。save(user);thrownewRuntimeException(啊,出现妖怪了!);} 按照我们的预期,当执行save保存数据后,因为出现了异常,所以事务要回滚。所以数据不会被修改。 在浏览器中输入http:localhost:8080userupdate进行测试,注意查看日志,可以确认事务起效了。 当我们把事务去掉,同样抛出异常:Overridepublicvoidupdate(Useruser){userRepository。save(user);thrownewRuntimeException(啊,出现妖怪了!);} 再次执行,发现虽然程序报错了,但数据却被更新了。 这也间接地证明,我们的Transactional事务起效了。 看到这,是不是就明白为什么新同事的优化纯属画蛇添足卵用了吧? 原文链接:https:mp。weixin。qq。comsiGwrb8T7L78eHOJij6n8w