前言
这是之前开始学spring的时候的笔记,现在添加了一些理解,然后把他搬到博客上来。
事务概述:
这里仅对数据库事务进行一个概述,要详细的可以查询相关文章
- 在JavaEE企业级开发的应用领域,为确保数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
- 事务就是一组由于逻辑上紧密关联而合并成一个整体的多个数据库操作,这些操作要么都执行,要么都不执行。
事务四大特性(ACID)
- 原子性:事务的原子性表现为一个事务中设计多个操作在逻辑上缺一不可,事务的原子性要求事务中的所有操作要么全都执行,要么全都不执行。
- 一致性:指的是数据的一致,具体是指,所有数据都处于满足业务规则的一致性状态。比如转账业务,一方减少,一方才增加,这才符合业务的规则。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前的数据时正确的,执行之后的数据依然是正确的,如果一个事务执行过程中某个操作失败了,就必须将其他所有操作撤销,将数据恢复到事务执行之前,这就是回滚。
- 隔离性:在应用程序实际运行过程中,事务往往都是并发执行的,所以有可能多个事务处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
- 持久性:持久性原则要求事务执行完成后,对数据的修改永久保存下来。不会因各种系统错误或者其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写到持久化存储器中。
声明式事务:
- 编程事务都要把事务处理代码耦合到业务代码中,使得业务代码中加入了了非业务代码。
- 而声明式事务在大多数情况下都比编程式事务要好,他将事务处理代码从业务代码中分离出来,以声明的方式实现事务管理。
- spring将事务处理的固定模式代码作为一个横切关注点,借助spring AOP框架实现声明式事务管理。换句话说Spring声明式事务实际上是通过AOP实现的。
- Spring在不同事务管理API上定义了一个抽象层,通过配置的方式使其剩下,从而使得我们开发时不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
以下是比较常用的事务管理器,用于不同的场景:
DataSourceTransactionManager :使用JDBC进行数据库操作时使用的事务管理器。
HibernateTransactionManager :使用hibernate持久层框架进行数据库操作时使用的事务管理器。
JpaTransactionManager : 使用JPA进行数据库操作时使用的事务管理器。
JtaTransactionManager:使用JTA框架进行数据库操作时使用的事务管理器。
以下是没有使用到事务的操作:
实现对图书的购买,步骤是 检查库存、减库存、检查余额、减余额。
表结构:
public interface BuyBookService {
public void buyBook(Integer bookId, Integer userId);
}
//业务层
@Service
public class BuyBookServiceImpl implements BuyBookService{
@Autowired
private BuyBookDao buyBookDao;
private static final BigDecimal ZERO_BLANCE = new BigDecimal(0);
@Override
public void buyBook(Integer bookId, Integer userId) {
//获取库存
Integer stock = buyBookDao.getStock(bookId);
//获取余额
BigDecimal blance = buyBookDao.getBlance(userId);
//获取价格
BigDecimal price = buyBookDao.getPrice(bookId);
//获取余额-价格
BigDecimal finalBlance = blance.subtract(price);
if(stock<0) {
//库存小于0抛异常
throw new RuntimeException("库存不足");
}
//减库存
buyBookDao.reduceStock(bookId,--stock);
if(finalBlance.compareTo(ZERO_BLANCE)<0) {
//余额小于0抛异常
throw new RuntimeException("余额不足");
}
//减余额
buyBookDao.reduceBlance(userId,finalBlance);
}
}
//数据访问层
@Repository
public class BuyBookDao {
@Autowired
//对数据库操作的工具类,spring提供的。对jdbc的封装
private JdbcTemplate jdbcTemplate;
//获取库存sql
private static final String GET_STOCK_SQL = "select stock from stock where bid=?";
//获取价格sql
private static final String GET_PRICE_SQL = "select price from book where bid=?";
//获取余额sql
private static final String GET_BLANCE_SQL = "select blance from user where id=?";
//减库存sql
private static final String REDUCE_STOCK_SQL = "update stock set stock=? where bid=?";
//减余额sql
private static final String REDUCE_BLANCE_SQL = "update user set blance=? where id=?";
public Integer getStock(Integer bookId) {
Integer stock = jdbcTemplate.queryForInt(GET_STOCK_SQL, bookId);
return stock;
}
public BigDecimal getPrice(Integer bookId) {
BigDecimal price = jdbcTemplate.queryForObject(GET_PRICE_SQL, new Object[] {bookId}, BigDecimal.class);
return price;
}
public BigDecimal getBlance(Integer userId) {
BigDecimal blance = jdbcTemplate.queryForObject(GET_BLANCE_SQL, new Object[] {userId}, BigDecimal.class);
return blance;
}
public void reduceStock(Integer bookId,Integer stock) {
jdbcTemplate.update(REDUCE_STOCK_SQL,stock,bookId);
}
public void reduceBlance(Integer userId,BigDecimal finalBlance) {
jdbcTemplate.update(REDUCE_BLANCE_SQL, finalBlance,userId);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 扫描组件 -->
<context:component-scan base-package="com.cong.springdemo"/>
<!-- 引入外部属性配置文件 -->
<bean class = "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="db.properties"></property>
</bean>
<!-- 配置数据源,并使用Druid连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置jdbcTemplate 组件,对数据库进行操作 -->
<bean id = "jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
测试类:
public class TestTransaction {
private ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-transaction.xml");
private BuyBookService bookServiceImpl = (BuyBookService) context.getBean("buyBookServiceImpl");
@Test
public void TestWithoutTransaction() {
bookServiceImpl.buyBook(1, 1);
}
}
由于没有做事务处理,当用户余额不足抛出异常时,库存减少了,但是用户余额没减,这显然是不对的。
基于注解驱动的声明式事务:
所需jar包:
编写声明式事务步骤:
- xml配置文件要加上tx响应的命名空间。
- 配置事务管理器。
- 开启事务注解驱动。
- 使用@Transactional注解标注在要进行事务处理的方法上。
加上命名空间:
配置事务管理器:
<!-- 配置事务管理器,因为我使用的是 jdbcTemplate操作数据库,所以使用DataSourceTransactionManager-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 指定事务管理器要管理的数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
开启事务注解驱动:
<!-- 开启事务注解驱动 -->
<!-- transaction-manager 指定事务管理器,不指定时,默认使用id为 transactionManager的事务管理器组件
所以如果你配置的事务管理器id为transactionManager,可以不配置该属性。否则必须配置-->
<tx:annotation-driven transaction-manager="transactionManager"/>
新的xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- 扫描组件 -->
<context:component-scan base-package="com.cong.springdemo"/>
<!-- 引入外部属性配置文件 -->
<bean class = "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="db.properties"></property>
</bean>
<!-- 配置数据源,并使用Druid连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置jdbcTemplate 组件,对数据库进行操作 -->
<bean id = "jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器,因为我使用的是 jdbcTemplate操作数据库,所以使用DataSourceTransactionManager-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 指定事务管理器要管理的数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启事务注解驱动 -->
<!-- transaction-manager 指定事务管理器,不指定时,默认使用id为 transactionManager的事务管理器组件
所以如果你配置的事务管理器id为transactionManager,可以不配置该属性。否则必须配置-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
其他代码与上面的demo一样,只是 buyBook方法要加上@Transactional注解。
配置事务后执行方法,当余额不足抛出异常时,会全部数据库操作回滚,也就是库存也不减,余额也不减,符合业务逻辑了。
@Transactional注解:
- @Transactional注解可以标注在类或者方法上,标注在类上,表示类中的方法都有事务处理功能,标注在方法上,只表示该方法加上了事务处理功能。
- 如果@Transactional注解既标注在了类上,又标注在类中的方法里。则标注在方法里面的@Transactional注解的属性配置的优先级要高于标注在类上的,也就是相同的属性设置,以标注在方法上的为准,但是不同的属性配置实现互补。
@Target({ElementType.METHOD, ElementType.TYPE}) //作用在方法和类上
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
/**
* 指定该事务要用哪个事务处理器来处理。与transactionManager互为别名。
*/
@AliasFor("transactionManager")
String value() default "";
/**
* 指定该事务要用哪个事务处理器来处理。与value互为别名。
*/
@AliasFor("value")
String transactionManager() default "";
/**
* 设置事务的传播行为,默认REQUIRED,后面详细解释
*/
Propagation propagation() default Propagation.REQUIRED;
/**
* 设置事务的隔离级别,默认底层数据库的隔离级别。后面详细解释。
*/
Isolation isolation() default Isolation.DEFAULT;
/**
* 设置事务的超时时间,默认不超时。后面详细解释。
*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
/**
* 设置事务是否为只读事务,默认否。后面详细解释。
*/
boolean readOnly() default false;
/**
* 设置事务回滚的异常类型,只有抛出该类型的异常才会进行事务回滚操作。后面详细解释。
*/
Class<? extends Throwable>[] rollbackFor() default {};
/**
* 设置事务回滚的异常类型的全限定名,后面详细解释。
*/
String[] rollbackForClassName() default {};
/**
* 设置事务不回滚的异常类型,抛出该类型的异常不会进行事务回滚操作。后面详细解释
*/
Class<? extends Throwable>[] noRollbackFor() default {};
/**
* 设置事务不回滚的异常类型的全限定名,抛出该类型的异常不会进行事务回滚操作。后面详细解释
*/
String[] noRollbackForClassName() default {};
}
事务的传播行为:
先放一个例子:
其他代码跟上面事务代码一样,这里新增一个Cashier业务类,用于买多本书。
public interface Cashier {
public void checkOut(Integer userId,Integer[] bookId);
}
@Service
public class CashierImpl implements Cashier{
@Autowired
private BuyBookService BuyBookServiceImpl;
//结账
@Override
public void checkOut(Integer userId, Integer[] bookIds) {
if(bookIds == null || bookIds.length == 0)
return;
for (Integer bookId : bookIds) {
BuyBookServiceImpl.buyBook(bookId, userId);
}
}
}
public class TestTransaction {
private ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-transaction.xml");
private BuyBookService bookServiceImpl = (BuyBookService) context.getBean("buyBookServiceImpl");
private Cashier cashierImpl = (Cashier) context.getBean("cashierImpl");
@Test
public void TestWithoutTransaction() {
cashierImpl.checkOut(1, new Integer[] {1,2});
}
}
按照正确的业务逻辑,用户月为100元,不够买第二本书,买了第二本书后会抛出余额不足的异常,然后正确的业务逻辑是回滚全部事务,库存不会减,余额也不会减。
但是,上面例子的结果却是第一本书的库存减了,然后余额也减了第一本书的价格,第二本书的库存和余额都没减。显然是不对的,一次性买两本书,要么都要成功。要么一本都买不成功。
原因分析:
checkOut方法上没有加事务,实际上使用的事务仅仅是buyBook方法的事务,所以事务没有处理到checkOut方法。在checkOut方法上添加事务,解决问题。
事务的传播行为定义:
A方法和B方法都是事务处理,当A方法调用B方法时,A方法的事务会传播到B方法中,B方法对于传播传播过来的事务做出的处理方式就是事务的传播行为。
总的来说,事务的传播行为是对于被调用方而言的,被调用方对调用方传播过来的事务的处理方式就是事务的传播行为。
注意:理解传播行为要站在被调用方的角度,要站在被调用方的角度,要站在被调用方的角度,看待被调用方有事务的情况下和没有事务的情况下被调用方对事务的处理。
A方法比喻类比checkOut,B方法类比 buyBook方法。
事务的传播行为在Transactional注解的propagation中定义,值是枚举类Propagation。默认为Propagation.REQUIRED。
共有一下7种传播行为(A是调用方,B是被调用方):
如下是下面传播行为的演示代码,每种传播行为基本就是Transactional注解的propagation属性。
A方法是one方法,B方法是two方法。
Controller:
@RestController
public class DemoController {
@Autowired
private DemoService demoService;
@GetMapping("/test")
public String test(){
demoService.insertOne();
return "a";
}
}
service及其实现:
public interface DemoService {
void insertOne();
void insertTwo(int i);
}
@Service
public class DemoServiceImpl implements DemoService {
@Autowired
private DemoDao demoDao;
@Autowired
private DemoService demoService;
@Override
@Transactional
public void insertOne() {
demoDao.insertOne();
try {
//这里调用insertTwo方法必须要引入demoService方法来调用,
//因为demoService.insertTwo(1);才会使用动态代理来实现事务控制,
//如果直接调用insertTwo方法的话,就只是一个普通的方法调用,没有事务。
demoService.insertTwo(1);
//入参0,这个调用会抛出异常
demoService.insertTwo(0);
}catch (Exception e){
System.out.println("insertTwo 嵌套事务出现异常回滚");
}
//int a = 1/0;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insertTwo(int i) {
demoDao.insertTwo();
//如果该方法入参为0,就抛出异常
int a = 1/i;
}
}
dao及其mapper:
@Mapper
public interface DemoDao {
void insertOne();
void insertTwo();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.springbootstudy.transaction.DemoDao">
<insert id="insertTwo">
INSERT INTO two (`names`) VALUES ("yehaoxian")
</insert>
<insert id="insertOne">
INSERT INTO one (`names`) VALUES ("yehaocong")
</insert>
</mapper>
REQUIRED:
如果A有事务传播到了B,B会抛弃自身事务,使用A的事务。如果A中没有事务,则B就使用自己的事务。
A有事务的情况:
全程由A方法的事务控制着。出现异样就进行回滚所以数据库操作。所以第二次买书失败会使得第一次买书失败,因为两次买书都是出于同一事务中。
演示:
A方法有事务:
结果:
两个表都没有数据,因为调用第二个insertTwo方法时,就已经由于入参为0,抛出异常,方法B
的事务传播是REQUIRED,并且A有事务,所以B就抛弃自身事务使用A的事务,所以A、B处于同一个事务中,所以B方法抛出异常时,全部回滚,所以就两个表都没成功。
A中没有事务的情况:
B方法使用自己的事务,两次B方法的调用都是一个独立的事务。所以会出现买两本书,一本购买成功,一本购买失败的情况。因为两次买书是两个独立的事务。
结果:
one表:
two表:
因为B方法的事务传播方式是REQUIRED,并且A方法没有事务,所以B使用自己的事务,所以A方法执行insertOne时,插入数据,第一个insertTwo时,插入数据,第二个insertTwo时插入数据后抛出异常,但是因为A方法没有事务,所以不会回滚,两个insertTwo方法调用不是同一个事务,所以第二个的回滚不关第一个的事情,所以two表就只插入了一条数据。
REQUIRES_NEW
B方法必须启动自己的事务,不管A有没有事务传播过来,都不用,都用自己的,如果A有事务,就将A的事务挂起。挂起的意思是把该事务先放一边,不执行该事务,代表被挂起的事务在挂起期间失效。
A有事务的情况:
在调用B方法时,挂起A方法的事务,开始B方法的事务。如果上述代码使用的是该传播行为,也会导致一本书购买成功,一本书购买失败的结局。
演示:
结果:
A表没有插入数据,B表只插入了一条,因为A有事务,B的事务传播方式为REQUIRES_NEW,所以在执行B方法时,A方法的事务会被挂起,所以实际上这里就是三个事务了,A自己一个事务,两次B方法调用2个事务,所以第二次B方法抛出的异常不会影响其他两次事务的提交。所以第二次调用B方法的事务回滚,其他没回滚。
A没有事务的情况下:
可以看出与REQUIRED传播行为的A没有事务情况一样。
演示:
此种传播方式在A没有事务的情况下,使用自己的事务,也就是A方法没有事务,B方法的调用就使用各自独立的两个事务,所以就符合上面的结果。
SUPPORTS
如果A有事务,那么B就使用A的事务,如果A没有事务,那么B就不运行事务。
也就是说B必须要抛弃自身的事务,如果A有事务就运行在A的事务里,如果A没有事务,就也不启用自身事务,就当做没事务。
更直白就是说,B:反正我怎样都不用我的事务,A你有事务我就用你的,没有我就啥都不用。
A有事务的情况:
可以看出与REQUIRED传播行为的A有事务的情况是一致的。
结果:
因为此种情况下B方法运行在A方法的事务中,A、B方法同一事务,所以第二次调用B方法的异常回滚导致全部都回滚了,就没有数据了。
A没有事务的情况:
啥事务都没。
结果:
在此种情况下,因为B必须抛弃自己的事务,此时A又没有事务,所以总体对外表现出就是没有事务,所以不进行回滚,都插入成功了。
NOT_SUPPORTED
A方法有事务时,在调用B方法的时候,A方法事务会挂起,但是不会启动B方法的事务;A方法没有事务时,B方法也不会其他事务。
直白说:B:我无论怎样也不会用我的事务,但是我也不会用你A的事务。
A有事务情况:
结果:
A方法因为自身的异常回滚了,所以没有数据,B方法没有事务,所以不会滚,有数据。
A没有事务情况:
啥事务都没有,与SUPPORTS传播行为没有事务时情况一样。
结果:
A没有事务,B不使用自己的事务,也不使用A的事务,所以对外表现为没有事务,所以都插入成功了。
MANDATORY
B方法必须运行在A方法事务中,如果A方法没有事务,就抛出异常。
A有事务的情况:
与REQUIRED传播行为的A有事务情况一致。
A没有事务的情况:
抛出异常:
NEVER
B方法不应该运行在A的事务中,如果A有事务,就抛出异常。
与REQUIRED传播行为相对。
A有事务的情况:
抛出异常:
A没有事务的情况:
啥事务都没。
NESTED
如果A有事务,当调用B方法时,A事务不会被挂起,B方法会在A事务的嵌套事务中运行。如果A没有事务,则B启动一个新的事务运行。
嵌套事务的意思是外部事务的回滚会导致内部事务的回滚,内部事务的回滚不会导致外部事务的回滚。
A有事务的情况:
结果:
这个情况代码下,第二个调用B方法没有传参数0了,B方法的两次调用都不会抛出异常,但是A方法最后抛出了一次,然后外部事务回滚,导致嵌套事务也回滚了,所以没有数据。
看看内部事务回滚会不会影响外部事务:
结果:
此时第二个B方法的调用抛出异常,回滚,但是不会影响外部事务A方法和另外一个嵌套事务的回滚,所以one表一条数据,two表也一条数据。
A没有事务的情况:
结果:
因为A没有事务,所以这种情况下就跟只有两个B方法有事务,所以A方法的异常不会导致回滚,其中一个B方法调用正常也不会导致回滚,最后一个B方法异常回滚,所以one表一条数据,two表一条数据。
总结:
要根据不同的业务逻辑,业务情况使用不同的事务传播行为, 一般会使用REQUIRED和REQUIRES_NEW传播行为。
事务隔离级别:
数据库事务有四个隔离级别(更详细可以查看相关文章,数据库概论啥的)并发事务条件下隔离级别才有意义:
- 读未提交。
- 读已提交。
- 可重复读。
- 串行化。
@Transactional注解的isolation属性设置事务的隔离级别,使用枚举Isolation设置:
有以下五种情况:
DEFAULT: 使用数据库底层的隔离级别。也就是数据库设置了什么隔离级别就使用什么隔离级别。
READ_UNCOMMITTED:读未提交。
READ_COMMITTED:读已提交。
REPEATABLE_READ:可重复读。
SERIALIZABLE:串行化。
事务超时操作:
在事务进行强制回滚前最多可以执行(等待)的时间,也就是事务在执行超过该时间就会进行强制回滚。
使用Transactional注解的timeout属性指定,值类型是int值,单位秒。默认-1表示会一直等待下去,不会强制回滚。
下面demo只是在原来的demo上修改了一点:
测试代码:
@Test
public void TestTransactionTimeout() {
bookServiceImpl.buyBook(1, 1);
}
结果:
抛出异常,因为方法睡眠了5秒,而事务超时为3秒,所以会进行强制回滚。
事务只读
如果一个事务里面的操作都是读操作的话,可以把事务设置为只读。通过把Transactional注解的属性readOnly设置为true即可,默认是false。
如果把事务设置为只读,mysql就会在只读访问的时候不加锁,提高性能。
如果设置为只读的事务中有写操作,就会抛出异常。
事务的回滚条件
可以设置事务回滚的条件,通过@Transactional注解的rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName四个属性指定。
- rollbackFor:是一个class类数组,表示设置事务回滚的异常,只有在方法执行过程中抛出了设置的异常类型,事务才会回滚。
- rollbackForClassName:是一个String数组,作用于rollbackFor一样,只是rollbackFor传入的是异常Class对象,而rollbackForClassName传入的是异常类的全限定名。
- noRollbackFor:是一个class类数组,表示设置事务不回滚的异常,在方法执行中抛出了设置的异常,事务也不会回滚。
- noRollbackForClassName:是一个String数组,作用于noRollbackFor一样,只是noRollbackFor传入的是异常Class对象,而noRollbackForClassName传入的是异常类的全限定名。
rollbackFor和rollbackForClassName的优先级比noRollbackFor和noRollbackForClassName高,也就是rollbackFor或者rollbackForClassName设置的异常和noRollbackFor或者noRollbackForClassName设置的异常一样时,rollbackFor和rollbackForClassName设置的生效,还是会事务回滚。