大家好,又见面了。

在大部分波及到数据库操作的我的项目外面,事务管制、事务处理都是一个无奈回避的问题。比方,须要对SQL执行过程进行事务的管制与解决的时候,其整体的解决流程会是如下的示意:

首先是要开启事务、而后执行具体SQL,如果执行异样则回滚事务,否则提交事务,最初敞开事务,实现整个处理过程。依照这个流程的逻辑,写一下对应的实现代码:

public void testJdbcTransactional(DataSource dataSource) {    Connection conn = null;    int result = 0;    try {        // 获取链接        conn = dataSource.getConnection();        // 禁用主动事务提交,改为手动管制        conn.setAutoCommit(false);        // 设置事务隔离级别        conn.setTransactionIsolation(            TransactionIoslationLevel.READ_COMMITTED.getLevel()        );        // 执行SQL        PreparedStatement ps =             conn.prepareStatement("insert into user (id, name) values (?, ?)");        ps.setString(1, "123456");        ps.setString(2, "Tom");        result = ps.executeUpdate();        // 执行胜利,手动提交事务        conn.commit();    } catch (Exception e) {        // 出现异常,手动回滚事务        if (conn != null) {            try {                conn.rollback();            } catch (Exception e) {                // write log...            }        }    } finally {        // 执行完结,最终不论胜利还是失败,都要开释资源,断开连接        try {            if (conn != null && !conn.isClosed()) {                conn.close();            }        } catch (Exception e) {             // write log...        }    }}

不难发现,下面大段的代码逻辑并不简单,对于业务而言其实仅仅只是执行了一个insert操作而已。然而杂糅的事务控制代码,显然烦扰了业务本身的代码解决逻辑的浏览与了解

惯例我的项目的代码中,波及到DB解决的场景很多,如果每个中央都有这么一段事务管制的逻辑,那么整体代码的可维护性将会比拟差,想想都令人窒息。

好在,JAVA中很多我的项目当初都是基于Spring框架进行构建的。得益于 Spring框架的封装,业务代码中进行事务管制操作起来也很简略,间接加个 @Transactional注解即可,大大简化了对业务代码的侵入性。那么对 @Transactional事务注解理解的够全面吗?晓得有哪些场景可能会导致 @Transactional注解并不会如你预期的形式失效吗?晓得应该怎么应用 @Transactional能力保障对性能的影响最小化吗?

上面咱们一起探讨下这些问题。

Spring申明式事务处理机制

为了简化业务开发场景对事务的解决复杂度,让开发人员能够更关注于业务本身的解决逻辑,Spring提供了申明式事务的能力反对。

Spring数据库事务约定解决逻辑流程如下图所示,比照后面示例中基于JDBC的事务处理,Spring的事务的解决操作交给了Spring框架解决,开发人员仅须要实现本人的业务逻辑即可,大大简化了事务方面的解决投入。

基于Spring事务机制,实现上述DB操作事务管制的代码,咱们的代码会变得十分的简洁:

@Transactionalpublic void insertUser() {    userDao.insertUser();}

与JDBC事务实现代码相比,基于Spring的形式只须要增加一个 @Transactional注解即可,代码中只须要实现业务逻辑即可,实现了事务管制机制对业务代码的低侵入性

Spring反对的基于 Spring AOP实现的申明式事务性能,所谓申明式事务,即应用@Transactional注解进行申明标注,通知Spring框架在什么中央启用数据库事务控制能力。@Transactional注解,能够增加在类或者办法上。如果其增加在类上时,表明此类中所有的public非静态方法都将启用事务控制能力。

既然@Transactional注解承载了Spring框架对于事务处理的相干能力,那么接下来咱们就一起看下该注解的一些可选配置以及具体应用场景。

@Transactional次要可选配置

只读事务配置

通过readonly参数指定以后事务是否为一个只读事务。设置为true标识此事务是个只读事务,默认状况为false。

@Transactional(readOnly = true)public DomResponse<CiCdItemDetail> queryCicdItemDetail(String appCode) {    return null;}

这里波及一个概念,叫做只读事务,其含意形容如下:

在多条查问语句一起执行的场景外面会波及到的概念。示意在事务设置的那一刻开始,到整个事务执行完结的过程中,其余事务所提交的写操作数据,对该事务都不可见。

举个例子:

当初有一个复合查问操作,蕴含2条SQL查问操作:先获取用户表count数,再获取用户表中所有数据。
(1) 先执行完获取用户表count数,失去后果10
(2) 在还没开始执行后一条语句的时候,另一个过程操作了DB并往用户表中插入一条新数据
(3) 复合操作的第二条SQL语句,获取用户列表的操作被执行,返回了11条记录

很显著,复合操作中的两条SQL语句获取的数据后果无奈匹配上。起因就是非原子性操作导致,即2条查问操作执行的距离内,有另一个写操作批改了指标读取的数据,导致了此问题的呈现。

为了防止此状况的产生,能够给复合查问操作增加上只读事务,这样事务管制范畴内,事务外的写操作就不可见,这样就保障了事务内多条查问语句执行后果的一致性。

那为什么要设置为只读事务、而不是惯例的事务呢?次要是从执行效率角度的思考。因为这个里的操作都是一些只读操作,所以设置为只读事务,数据库会为只读事务提供一些优化伎俩,比方不启动回滚段、不记录回滚log之类的。

回滚条件设定

@Transactional有提供4个不同属性,能够反对传入不同的参数,设定须要回滚的条件:

参数含意阐明
rollbackFor用于指定须要回滚的特定异样类型,能够指定一个或者多个。当指定rollbackFor或者rollbackForClassName之后,办法执行逻辑中只有抛出指定的异样类型,才会触发事务回滚
rollbackForClassNamerollbackFor雷同,设置字符串格局的类名
noRollbackFor用于指定不须要进行回滚的异样类型,当办法中抛出指定类型的异样时,不进行事务回滚。而其余的类型的异样将会触发事务回滚。
noRollbackForClassNamenoRollbackFor雷同,设置字符串格局的类名

其中,rollbackFor反对指定单个或者多个异样类型,只有抛出指定类型的异样,事务都将被回滚掉:

// 指定单个异样@Transactional(rollbackFor = DemoException.class)public void insertUser() {    // do something here}// 指定多个异样@Transactional(rollbackFor = {DemoException.class, DemoException2.class})public void insertUser2() {    // do something here}

rollbackForrollbackForClassName作用雷同,只是提供了2个不同的指定办法,容许执行Class类型或者ClassName字符串。

// 指定异样名称@Transactional(rollbackForClassName = {"DemoException"})public void insertUser() {    // do something here}

同理,noRollbackFornoRollbackForClassName的应用与下面示意的类似,只是其含意性能点是相同的。

事务流传行为

propagation用于指定此事务对应的流传类型。所谓的事务流传类型,即以后曾经在一个事务的上下文中时,又须要开始一个事务,这个时候来解决这个将要开启的新事务的解决策略。

次要有7种类型的事务流传类型:

流传类型含意形容
REQUIRED如果以后存在事务,则退出该事务;如果以后没有事务,则创立一个新的事务
SUPPORTS如果以后存在事务,则退出该事务;如果以后没有事务,则以非事务的形式持续运行
MANDATORY如果以后存在事务,则退出该事务;如果以后没有事务,则抛出异样
REQUIRES_NEW创立一个新的事务,如果以后存在事务,则把以后事务挂起
NOT_SUPPORTED以非事务形式运行,如果以后存在事务,则把以后事务挂起
NEVER以非事务形式运行,如果以后存在事务,则抛出异样
NESTED如果以后存在事务,则创立一个事务作为以后事务的嵌套事务来运行;如果以后没有事务,则该取值等价于REQUIRED

事务的流传行为,将会影响到事务管制的后果,比方最终是在同一事务中,一旦遇到异样,所有操作都会被回滚掉,而如果是在多个事务中,则某一个事务的回滚,不影响已提交的其余事务的回滚。

理论编码的时候,能够通过@Transactional注解中的 propagation参数来指定具体的流传类型,取值由 org.springframework.transaction.annotation.Propagation枚举类提供。如果不指定,则默认取值为 Propagation.REQUIRED,也即如果以后存在事务,则退出该事务,如果以后没有事务,则创立一个新的事务

/** * The transaction propagation type. * <p>Defaults to {@link Propagation#REQUIRED}. * @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior() */Propagation propagation() default Propagation.REQUIRED;  

事务超时设定

能够应用timeout属性来设置事务的超时秒数,默认值为-1,示意永不超时。

@Transactional生效场景避坑

同一个类中办法间调用

Spring的事务实现原理是AOP,而AOP的原理是动静代理。

在类外部办法之间互相调用的时候,实质上是类对象本身的调用,而不是应用代理对象去调用,也就不会触发AOP,这样其实Spring也就无奈将事务管制的代码逻辑织入到调用代码流程中,所以这里的事务管制就无奈失效。

public void insertUser() {    writeDataIntoDb();}@Transactionalpublic void writeDataIntoDb() {  // ...}

所以遇到同一个类中多个办法之间互相调用,且调用的办法须要做事务管制的时候须要特地留神下这个问题。解决形式,能够建2个不同的类,而后将办法放到两个类中,这样跨类调用,Spring事务机制就能够失效。

增加在非public办法上

如果将@Transactional注解增加在protected、private润饰的办法上,尽管代码不会有任何的报错,然而实际上注解是不会失效的。

@Transactionalprivate void writeDataIntoDb() {  // ...}

办法外部Try Catch吞掉相干异样

这个其实很容易了解,业务代码中将所有的异样给catch侵吞掉了,等同于业务代码认为被捕捉的异样不须要去触发回滚。对框架而言,因为异样被捕捉了,业务逻辑执行都在失常往下运行,所以也不会触发异样回滚机制。

// catch了可能的异样,导致DB操作失败的时候事务不会触发回滚@Transactionalpublic void insertUser() {    try {        UserEntity user = new UserEntity();        user.setWorkId("123456");        user.setUserName("王小二");        userRepository.save(user);    } catch (Exception e) {        log.error("failed to create user");        // 间接吞掉了异样,这样不会触发事务回滚机制    }}

在业务解决逻辑中,如果的确须要通晓并捕捉相干解决的异样进行一些额定的业务逻辑解决,如果要保障事务回滚机制失效,最初须要往外抛出 RuntimeException异样,或者是继承RuntimeException实现的业务自定义异样。如下:

// catch了可能的异样,对外抛出RuntimeException或者其子类,可触发事务回滚@Transactionalpublic void insertUser() {    try {        UserEntity user = new UserEntity();        user.setWorkId("123456");        user.setUserName("王小二");        userRepository.save(user);    } catch (Exception e) {        log.error("failed to create user");        // @Transactional没有指定rollbackFor,所以抛出RuntimeException或者其子类,可触发事务回滚机制        throw new RuntimeException(e);    }}

当然,如果@Transactional注解指定了 rollbackFor为某个具体的异样类型,则最终须要保障异样时对外抛出相匹配的异样类型,才能够触发事务处理逻辑。如下:

// catch了指定异样,对外抛出对应类型的异样,可触发事务回滚@Transactional(rollbackFor = DemoException.class)public void insertUser() {    try {        UserEntity user = new UserEntity();        user.setWorkId("123456");        user.setUserName("王小二");        userRepository.save(user);    } catch (Exception e) {        log.error("failed to create user");        // @Transactional有指定rollbackFor,抛出异样要与rollbackFor指定异样类型统一        throw new DemoException();    }}

对应数据库引擎类型不反对事务

MySQL数据库而言,常见的数据库引擎有 InnoDBMyisam等类型,然而MYISAM引擎类型是不反对事务的。所以如果建表时设置的引擎类型设置为 MYISAM的话,即便代码外面增加了@Transactional最终事务也不会失效的。

@Transactional应用策略

因为事务处理对性能会有肯定的影响,所以事务也不是说任何中央都能够轻易增加的。对于一些性能敏感场景,须要留神几点:

  1. 仅在必要的场合增加事务管制
(1)不含有DB操作相干,无需增加事务管制
(2)单条查问语句,没必要增加事务管制
(3)仅有查问操作的多条SQL执行场景,能够增加只读事务管制
(4)单条 insert/update/delete语句,其实也不须要增加 @Transactional事务处理,因为单条语句执行其实数据库有隐性事务管制机制,如果执行失败,是属于 SQL报错,数据不会更新胜利,天然也无需回滚。
  1. 尽可能放大事务管制的代码段解决范畴
次要从性能层面思考,事务机制,相似于并发场景的加锁解决,范畴越大对性能影响越显著
  1. 事务管制范畴内的业务逻辑尽可能简略、防止非事务相干耗时解决逻辑
也是从性能层面思考,尽量将耗时的逻辑放到事务管制之外执行,事务内仅保留与DB操作切实相干的逻辑

总结

好啦,对于Spring中事务管制的相干用法,以及@Transactional应用过程中可能的一些生效场景,就探讨到这里了。那么你对事务这块有哪些本人的了解呢?或者是否有遇到相干的问题呢?欢送一起交换下咯。

我是悟道,聊技术、又不仅仅聊技术~

如果感觉有用,请点个关注,也能够关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的本人。