五、Spring事务原理
1、事务四大特性(ACID)
-
原子性(Atomicity):一个事务中的所有操作,要么都完成,要么都不执行。对于一个事务来说,不可能只执行其中的一部分。
-
一致性(Consistency):数据库总是从一个一致性的状态转换到另外一个一致性状态,事务前后数据的完整性必须保持一致。。
-
隔离性(Isolation):一个事务所做的修改在最终提交以前,对其它事务是不可见的,多个事务之间的操作相互不影响。
-
持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
2、事务隔离级别
(1)、Read Uncommitted(读取未提交内容):一个事务可以看到其他事务已执行但是未提交的结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少,并且存在脏读问题。
(2)、Read Committed(读取已提交内容):一个事务只能看到其他事务已执行并已提交的结果(Oracle、SQL Server默认隔离级别)。这种隔离级别支持不可重复读,因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
(3)、Repeatable Read(可重读):同一事务的多个实例在并发读取数据时,会看到同样的数据行(MySQL的默认事务隔离级别)。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了不可重复读问题,存在幻读问题。
(4)、Serializable(可串行化):最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
-
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据就是脏数据
-
不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
-
幻读:A事务在执行过程中,B事务插入了符合A事务查询条件的数据,导致A事务两次读取的数据不一致。
不可重复读和幻读的区别:
不可重复读是由update引起的,需要用锁行来避免;幻读是由insert或delete引起的,需要用锁表来避免。
√:可能出现 ×:不会出现
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read Uncommitted | √ | √ | √ |
Read Committed | × | √ | √ |
Repeatable Read | × | × | √ |
Serializable | × | × | × |
(5)、注:每降低一个事务隔离级别都能提高数据库的并发
①、读未提交:其它事务未提交就可以读
②、读已提交:其它事务只有提交了才能读
③、可重复读:只管自己启动事务时候的状态,不受其它事务的影响(mysql默认)
④、事务串行:按照顺序提交事务保证了数据的安全性,但无法实现并发
3、事务的关键对象
(1)、PlatformTransactionManager:事务管理器,是用来管理事务的操作,它保存着当前的数据源连接,对外提供对该数据源的事务提交回滚操作接口,同时实现了事务相关操作的方法。一个数据源DataSource需要一个事务管理器。它只包含三个方法:获取事务;回滚事务;提交事务。
根据底层所使用的不同的持久化API或框架,使用如下:
①、DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况,在定义时需要提供底层的数据源作为其属性,也就是DataSource。
②、HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况,与HibernateTransactionManager对应的是SessionFactory。
③、JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况,与JpaTransactionManager对应的是EntityManagerFactory。
(2)、TransactionDefinition:定义事务的类型,事务包含很多属性,比如是否可读,事务隔离级别,事务传播属性,超时时间等。通过事务的定义,我们根据定义获取特定的事务。DefaultTransactionDefinition:TransactionDefinition的一个实现类。就是对上述属性设置一些默认值,默认的传播特性为PROPAGATION_REQUIRED,隔离级别为ISOLATION_DEFAULT。
public interface TransactionDefinition {
/**
* 事务隔离级别
*/
//数据库默认的事务隔离级别
int ISOLATION_DEFAULT = -1;
//读未提交
int ISOLATION_READ_UNCOMMITTED = 1;
//读已提交
int ISOLATION_READ_COMMITTED = 2;
//可重复读
int ISOLATION_REPEATABLE_READ = 4;
//串行执行
int ISOLATION_SERIALIZABLE = 8;
/**
* 事务传播属性
*/
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int TIMEOUT_DEFAULT = -1;
...
}
①、事务隔离级别:
a、isolation_default:是PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。Mysql在InnoDB引擎下默认ISOLATION_REPEATABLE_READ;Oracle默认ISOLATION_READ_COMMITTED。
b、isolation_read_uncommitted:事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
c、isolation_read_committed:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。
d、isolation_repeatable_read:该事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。
e、isolation_serializable:是代价最高但最可靠的事务隔离级别。事务被处理为顺序执行。
②、事务传播属性:
示例前提:外层事务ServiceA的MethodA()调用内层ServiceB的MethodB()
a、propagation_required:如果存在一个事务,则支持当前事务,如果当前没有事务,就新建一个事务。这通常是一个事务定义的默认设置,并且通常限定了事务同步范围,是Spring默认的事务的传播。
示例:如果ServiceB.methodB()的事务级别定义为PROPAGATION_REQUIRED,那么执行ServiceA.methodA()的时候spring已经起了事务,这时调用ServiceB.methodB(),ServiceB.methodB()看到自己已经运行在ServiceA.methodA()的事务内部,就不再起新的事务。假如ServiceB.methodB()运行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在ServiceA.methodA()或者在ServiceB.methodB()内的任何地方出现异常,事务都会被回滚。
b、propagation_requires_new:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作。
示例:如果ServiceA.methodA()的事务级别定义为PROPAGATION_REQUIRED,ServiceB.methodB()的事务级别为PROPAGATION_REQUIRES_NEW。那么当执行到ServiceB.methodB()的时候,ServiceA.methodA()所在的事务就会挂起,ServiceB.methodB()会起一个新的事务,等待ServiceB.methodB()的事务完成以后,它才继续执行。
PROPAGATION_REQUIRES_NEW与PROPAGATION_REQUIRED的事务区别:
两者区别在于事务的回滚程度,因为ServiceB.methodB()是新起一个事务,那么就是存在两个不同的事务。如果ServiceB.methodB()已经提交,那么ServiceA.methodA()失败回滚,ServiceB.methodB()不会回滚。如果ServiceB.methodB()失败回滚,如果他抛出的异常被ServiceA.methodA()捕获,ServiceA.methodA()事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。
c、propagation_supports:如果存在一个事务,则支持当前事务,如果当前没有事务,就以非事务方式执行。注意对于具有事务同步的事务管理器,PROPAGATION_SUPPORTS与完全没有事务稍有不同,因为它定义了同步可能适用的事务范围。
示例:如果ServiceB.methodB()的事务级别为PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。
d、propagation_not_supported:总是以非事务地执行,如果当前存在事务,则挂起任何存在的事务
示例:如果ServiceB.methodB()的事务级别为PROPAGATION_NOT_SUPPORTED,执行ServiceA.methodA()方法,当执行到ServiceB.methodB()的时候,如果ServiceA.methodA()有事务,则会挂起ServiceA.methodA()的事务,当执行完ServiceB.methodB()方法的时候,ServiceA.methodA()方法继续以事务的方式执行。
示例:如果ServiceB.methodB()的事务级别为PROPAGATION_MANDATORY,那么当执行ServiceA.methodA()执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则ServiceB.methodB()沿用该事务,如果没有,则会抛出异常。
e、propagation_mandatory:如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。MANDATORY是强制的,命令的意思,放在这里就是强制需要一个事务。
f、propagation_never:总是非事务地执行,如果存在一个活动事务,则抛出异常
示例:如果ServiceB.methodB()的事务级别为PROPAGATION_NEVER,执行ServiceA.methodA()方法,当执行到ServiceB.methodB()的时候,如果有事务,则抛出异常,如果没有则以非事务方式执行。
d、propagation_nested:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。
示例:如果ServiceB.methodB()的事务级别为PROPAGATION_REQUIRED,执行ServiceA.methodA()方法,当执行到ServiceB.methodB()的时候,如果ServiceA.methodA()方法有事务,则会创建一个依赖于当前事务的嵌套事务,如果ServiceB.methodB()执行失败,只会回滚ServiceB.methodB(),不会回滚ServiceA.methodA(),只有当ServiceA.methodA()执行完成后才会提交ServiceB.methodB()的事务,因为嵌套事务不能单独提交;如果ServiceA.methodA()回滚了,则会导致内部嵌套事务的回滚。如果ServiceA.methodA()方法没有事务,就会新建一个事务。
(3)、TransactionStatus:一个事务运行的状态,事务管理器通过状态可以知道事务的状态信息,然后进行事务的控制。事务是否完成,是否是新的事务,是不是只能回滚等。
4、多线程事务
问题:多线程底层连接数据库的时候,使用的线程变量(TheadLocal)。所以,开多少线程理论上就会建立多少个连接,每个线程有自己的连接,事务肯定不是同一个了。
(1)、解决办法一:强制手动把每个线程的事务状态放到一个同步集合里面。然后如果有单个异常,循环回滚每个线程。
@Resource
private PlatformTransactionManager transactionManager;
private void mainFun(List<DoctorAdviceDto> doctorAdviceDtoList){
List<Future<String>> futureList = new ArrayList<>();
AtomicBoolean rollbackFlag = new AtomicBoolean(false);
CountDownLatch subThreadLatch = new CountDownLatch(1);
CountDownLatch mainThreadLatch = new CountDownLatch(doctorAdviceDtoList.size());
doctorAdviceDtoList.stream().forEach(doctorAdviceDto -> {
Future<String> future = threadPoolUtils.submit(() -> {
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = transactionManager.getTransaction(defaultTransactionDefinition);
try {
//子线程开始处理自有业务
//...
//业务处理结束
}catch (Exception e){
//子线程异常,rollbackFlag修改值为true
rollbackFlag.set(true);
//subThreadLatch为0,解除各个子线程阻塞
subThreadLatch.countDown();
return e.getMessage();
}finally {
//子线程运行,mainThreadLatch执行countDown减1,将控制权交给主线程
mainThreadLatch.countDown();
//子线程阻塞,等待其他子线程执行完
subThreadLatch.await(10, TimeUnit.SECONDS);
if (rollbackFlag.get()) {
transactionManager.rollback(status);
} else {
transactionManager.commit(status);
}
}
return null;
});
futureList.add(future);
});
if (!rollbackFlag.get()) {
try {
//主线程阻塞,如果mainThreadLatch为0或await超过10秒,继续往下运行
mainThreadLatch.await(10, TimeUnit.SECONDS);
subThreadLatch.countDown();
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
//异常处理开始
//...
//异常处理结束
}
}
(2)、解决办法二:(测试好像不可行)
场景:A类使用多线程来完成一个添加业务,所以这个类里面会有run()方法,然后再run()方法里面执行添加add()方法,完成业务实现。
①、不进行事务管理的时候,可以将添加add()方法放在A类里面,然后直接进行调用就可以。
②、当进行事务管理时候,如果将add()方法放在A类里面,事务Spring是不帮忙管理的,所以需要将add()方法放到B类里面,然后将B类注入到A类里面,这样我们就相当于将B类交给Spring管理类,这样Spring就会帮处理事务了,就可以使用@Transaction注解控制事务了,B类的add()方法也需要添加@Transaction注解。
5、事务实现原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。Spring在启动的时候会去解析生成相关的Bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理类中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
6、@Transactional注解的相关问题
ServiceA中有method1()方法、method2()方法、method3()方法;
ServiceB中有method4()方法、method5()方法;
(1)、Spring默认情况下会对unchecked异常进行事务回滚,对checked异常则不回滚。Java里面将派生于Error或者RuntimeException(如NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException)的异常称为unchecked异常(编译器不要求强制处置的异常)。其他继承自java.lang.Exception的异常统称为Checked Exception,如IOException、TimeoutException等(编译器要求必须处置的异常)。
如果需要遇见Exception异常也回滚,需要进行标注。
@Transactional(rollbackFor=Exception.class)
(2)、@Transactional注解只能应用到public的方法上,如果应用在protected、private或者package方法上,也不会报错,不过事务设置不会起作用。
(3)、如果ServiceA中有method1()方法中调用了method2()方法和method3()方法,@Transactional注解只在method2()方法和method3()方法上标注,则事务不生效,需要标注在method1()方法上。
因为Spring的事务是基于动态代理实现的,同一个类内这样调用的话,只有ServiceA的第一次调用会根据动态代理生成ProxyClass,之后类内调用是不带任何切面信息的方法本身,因为第一次调用没有标注@Transactional注解,所以不会调用Spring生成的代理对象。