Spring事务失效的12种场景

Spring业务失效的12种场景

一 业务不收效

1.拜访权限问题

众所周知,java的拜访权限首要有四种:private、default、protected、public,它们的权限从左到右,顺次变大。

但假如咱们在开发过程中,把有某些业务办法,界说了过错的拜访权限,就会导致业务功用出问题,例如:

@Service
public class UserService {
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

咱们可以看到add办法的拜访权限被界说成了private,这样会导致业务失效,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;
  }

也便是说,假如咱们自界说的业务办法(即目标办法),它的拜访权限不是public,而是private、default或protected的话,spring则不会供给业务功用。

2. 办法用final润饰

有时分,某个办法不想被子类从头,这时可以将该办法界说成final的。一般办法这样界说是没问题的,但假如将业务办法界说成final,例如:

@Service
public class UserService {
    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

咱们可以看到add办法被界说成了final的,这样会导致业务失效。

为什么?

假如你看过spring业务的源码,或许会知道spring业务底层运用了aop,也便是经过jdk动态署理或者cglib,帮咱们生成了署理类,在署理类中完结的业务功用。

但假如某个办法用final润饰了,那么在它的署理类中,就无法重写该办法,而增加业务功用。

3.办法内部调用

有时分咱们需求在某个Service类的某个办法中,调用别的一个业务办法,比方:

@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办法不会生成业务。

由此可见,在同一个类中的办法直接内部调用,会导致业务失效。

那么问题来了,假如有些场景,的确想在同一个类的某个办法中,调用它自己的别的一个办法,该怎么办呢?

3.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();
    }
 }

3.2 在该Service类中注入自己

假如不想再新加一个Service类,在该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.3 经过AopContent类

在该Service类中运用AopContext.currentProxy()获取署理目标

上面的办法2的确可以解决问题,可是代码看起来并不直观,还可以经过在该Service类中运用AOPProxy获取署理目标,完结相同的功用。详细代码如下:

@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实例化和依靠注入的功用。

假如有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比方:

//@Service
public class UserService {
    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

从上面的比方,咱们可以看到UserService类没有加@Service注解,那么该类不会交给spring办理,所以它的add办法也不会生成业务。

5.多线程调用

在实践项目开发中,多线程的运用场景仍是挺多的。假如spring业务用在多线程场景中,会有问题吗?

@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业务源码的朋友,或许会知道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还不支撑行锁和外键。

所以在实践业务场景中,myisam运用的并不多。在mysql5今后,myisam现已逐步退出了历史的舞台,取而代之的是innodb。

7.未敞开业务

有时分,业务没有收效的根本原因是没有敞开业务。

你看到这句话或许会觉得好笑。

敞开业务不是一个项目中,最最最基本的功用吗?

为什么还会没有敞开业务?

没错,假如项目现已建立好了,业务功用肯定是有的。

但假如你是在建立项目demo的时分,只要一张表,而这张表的业务没有收效。那么会是什么原因形成的呢?

当然原因有许多,但没有敞开业务,这个原因极其简单被疏忽。

假如你运用的是springboot项目,那么你很走运。由于springboot经过DataSourceTransactionManagerAutoConfiguration类,现已静静的帮你敞开了业务。

你所要做的事情很简略,只需求装备spring.datasource相关参数即可。

但假如你运用的仍是传统的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> 

静静的说一句,假如在pointcut标签中的切入点匹配规矩,配错了的话,有些类的业务也不会收效。

二 业务不回滚

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);
    }
}

咱们可以看到add办法的业务传达特性界说成了Propagation.NEVER,这种类型的传达特性不支撑业务,假如有业务则会抛反常。

目前只要这三种传达特性才会创立新业务: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业务可以正常回滚,有必要抛出它可以处理的反常。假如没有抛反常,则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,所以业务也不会回滚。

即便rollbackFor有默许值,但阿里巴巴开发者规范中,仍是要求开发者从头指定该参数。

这是为什么呢?

由于假如运用默许值,一旦程序抛出了Exception,业务不会回滚,这会呈现很大的bug。所以,主张一般状况下,将该参数设置成:Exception或Throwable。

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中,并且不继续往上抛反常。这样就能确保,假如内部嵌套业务中呈现反常,只回滚内部业务,而不影响外部业务。

三 其他

1 大业务问题

在运用spring业务时,有个让人十分头疼的问题,便是大业务问题。

通常状况下,咱们会在办法上@Transactional注解,填加业务功用,比方:

@Service
public class UserService {
    @Autowired 
    private RoleService roleService;
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}
@Service
public class RoleService {
    @Autowired 
    private RoleService roleService;
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

@Transactional注解,假如被加到办法上,有个缺陷便是整个办法都包含在业务傍边了。

上面的这个比方中,在UserService类中,其实只要这两行才需求业务:

roleService.save(userModel);
update(userModel);

在RoleService类中,只要这一行需求业务:

saveData(userModel);

现在的这种写法,会导致一切的query办法也被包含在同一个业务傍边。

假如query办法十分多,调用层级很深,而且有部分查询办法比较耗时的话,会形成整个业务十分耗时,而从形成大业务问题。

2.编程式业务

上面聊的这些内容都是根据@Transactional注解的,首要说的是它的业务问题,咱们把这种业务叫做:声明式业务

其实,spring还供给了别的一种创立业务的方法,即经过手动编写代码完结的业务,咱们把这种业务叫做:编程式业务。例如:

   @Autowired
   private TransactionTemplate transactionTemplate;
   ...
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

在spring中为了支撑编程式业务,专门供给了一个类:TransactionTemplate,在它的execute办法中,就完结了业务的功用。

相较于@Transactional注解声明式业务,我更主张我们运用,根据TransactionTemplate的编程式业务。首要原因如下:

  1. 避免由于spring aop问题,导致业务失效的问题。
  2. 可以更小粒度的控制业务的规模,更直观。