天天看点

spring事务失效的12种场景

                                                                  一 事务不生效

1.访问权限问题

java的访问权限主要有四种:private<default<protected<public。

把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题:

 @Service

public class UserService {

    

    @Transactional

    private void add(UserModel userModel) {

         saveData(userModel);

         updateData(userModel);

    }

}

spring要求被代理方法必须是

public

的

在

AbstractFallbackTransactionAttributeSource

类的

computeTransactionAttribute

方法中有个判断,如果目标方法不是public,则

TransactionAttribute

返回null,即不支持事务。

=====>protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {

    // Don't allow no-public methods as required.

    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {

      return null;

    }

    // The method may be on an interface, but we need attributes from the target class.

    // If the target class is null, the method will be unchanged.

    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.

    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);

    if (txAttr != null) {

      return txAttr;

    }

    // Second try is the transaction attribute on the target class.

    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());

    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {

      return txAttr;

    }

    if (specificMethod != method) {

      // Fallback is to look at the original method.

      txAttr = findTransactionAttribute(method);

      if (txAttr != null) {

        return txAttr;

      }

      // Last fallback is the class of the original method.

      txAttr = findTransactionAttribute(method.getDeclaringClass());

      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {

        return txAttr;

      }

    }

    return null;

  }

2.方法用final修饰:

某个方法不想被子类重新,这时可以将该方法定义成final的,但如果将事务方法定义成final的事物会失效。

@Service

public class UserService {

    @Transactional

    public final void add(UserModel userModel){

        saveData(userModel);

        updateData(userModel);

    }

}

spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

如果某个方法是static的,同样无法通过动态代理,变成事务方法。

 

3.方法内部调用:

@Service

public class UserService {

    @Autowired

    private UserMapper userMapper;

    @Transactional

    public void add(UserModel userModel) {

        userMapper.insertUser(userModel);

        updateStatus(userModel);

    }

    @Transactional

    public void updateStatus(UserModel userModel) {

        doSameThing();

    }

}

在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。

解决办法:

1)新加一个Service方法

这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中

@Servcie

public class ServiceA {

   @Autowired

   prvate ServiceB serviceB;

   public void save(User user) {

         queryData1();

         queryData2();

         serviceB.doSave(user);

   }

 }

 @Servcie

 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)

    public void doSave(User user) {

       addData1();

       updateData2();

    }

 }

2)在该Service类中注入自己

Servcie

public class ServiceA {

   @Autowired

   prvate ServiceA serviceA;

   public void save(User user) {

         queryData1();

         queryData2();

         serviceA.doSave(user);

   }

   @Transactional(rollbackFor=Exception.class)

   public void doSave(User user) {

       addData1();

       updateData2();

    }

 }

3)通过AopContent类

在该Service类中使用AopContext.currentProxy()获取代理对象

@Servcie

public class ServiceA {

   public void save(User user) {

         queryData1();

         queryData2();

         ((ServiceA)AopContext.currentProxy()).doSave(user);

   }

   @Transactional(rollbackFor=Exception.class)

   public void doSave(User user) {

       addData1();

       updateData2();

    }

 }

4.未被spring管理

使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

public class UserService {

    @Transactional

    public void add(UserModel userModel) {

         saveData(userModel);

         updateData(userModel);

    }    

}

5.多线程调用

@Slf4j

@Service

public class UserService {

    @Autowired

    private UserMapper userMapper;

    @Autowired

    private RoleService roleService;

    @Transactional

    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);

        new Thread(() -> {

            roleService.doOtherThing();

        }).start();

    }

}

@Service

public class RoleService {

    @Transactional

    public void doOtherThing() {

        System.out.println("保存role表数据");

    }

}

事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

====》private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>("Transactional resources");

说明:同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

 

6.表不支持事务

(mysql5之前,默认的数据库引擎是MYISAM。

它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。

创建表的时候,只需要把

ENGINE

参数设置成

MyISAM

即可:

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin


myisam好用,但有个很致命的问题是:         不支持事务; myisam还不支持行锁和外键                

)

7.未开启事务

(1)使用的是springboot项目,那么你很幸运。因为springboot通过

DataSourceTransactionManagerAutoConfiguration

类,已经默默的帮你开启了事务。

(2)使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。<!-- 配置事务管理器 --> 

<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 

    <property name="dataSource" ref="dataSource"></property> 

</bean> 

<tx:advice id="advice" transaction-manager="transactionManager"> 

    <tx:attributes> 

        <tx:method name="*" propagation="REQUIRED"/>

    </tx:attributes> 

</tx:advice> 

<!-- 用切点把事务切进去 --> 

<aop:config> 

    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 

    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 

</aop:config> 

                                                              二 事务不回滚

1.错误的传播特性

在使用

@Transactional

注解时,是可以指定

propagation

参数的。

该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:

  • REQUIRED

     如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
  • SUPPORTS

     如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY

     如果当前上下文中存在事务,否则抛出异常。
  • REQUIRES_NEW

     每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED

     如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER

     如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED

     如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

手动设置propagation参数的时候,把传播特性设置错了

@Service

public class UserService {

    @Transactional(propagation = Propagation.NEVER)

    public void add(UserModel userModel) {

        saveData(userModel);

        updateData(userModel);

    }

}

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

2.自己吞了异常

开发者在代码中手动try...catch了异常

@Slf4j

@Service

public class UserService {

    

    @Transactional

    public void add(UserModel userModel) {

        try {

            saveData(userModel);

            updateData(userModel);

        } catch (Exception e) {

            log.error(e.getMessage(), e);

        }

    }

}

开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

 

3.手动抛了别的异常

开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

@Slf4j

@Service

public class UserService {

    

    @Transactional

    public void add(UserModel userModel) throws Exception {

        try {

             saveData(userModel);

             updateData(userModel);

        } catch (Exception e) {

            log.error(e.getMessage(), e);

            throw new Exception(e);

        }

    }

}

开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

因为spring事务,默认情况下只会回滚

RuntimeException

(运行时异常)和

Error

(错误),对于普通的Exception(非运行时异常),它不会回滚。

 

4.自定义了回滚异常

在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置

rollbackFor

参数,来完成这个功能

@Slf4j

@Service

public class UserService {

    

    @Transactional(rollbackFor = BusinessException.class)

    public void add(UserModel userModel) throws Exception {

       saveData(userModel);

       updateData(userModel);

    }

}

在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

5.嵌套事务回滚多了

public class UserService {

    @Autowired

    private UserMapper userMapper;

    @Autowired

    private RoleService roleService;

    @Transactional

    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);

        roleService.doOtherThing();

    }

}

@Service

public class RoleService {

    @Transactional(propagation = Propagation.NESTED)

    public void doOtherThing() {

        System.out.println("保存role表数据");

    }

}

使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

解决办法:

@Slf4j

@Service

public class UserService {

    @Autowired

    private UserMapper userMapper;

    @Autowired

    private RoleService roleService;

    @Transactional

    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);

        try {

            roleService.doOtherThing();

        } catch (Exception e) {

            log.error(e.getMessage(), e);

        }

    }

}

==将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

 

Â