今天来说一说Spring中的事务管理机制把,在Spring中可以通过xml配置和注解配置的方式,实现对事务的管理和控制。xml配置,emmmmm,我已经忘得差不多了。主要是注解用的太爽了,哈哈哈哈。本篇文章就只说一下注解配置吧。之后待我复习一下xml配置,再整理一篇xml配置和注解配置的文章。
首先,我们先来了解一下什么事务。
事务(Transaction)是一个业务,是一个不可分割的逻辑工作单元,具备ACID特性,实际工作中可借助Spring进行事务管理。
事务四大特性:ACID:
1)原子性:一个事务中的多个操作要么都成功要么都失败。
2)一致性:一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。例如存钱操作,存之前和存之后的总钱数应该是一致的
3)隔离性:不同的事务有可能会处理相同的数据,因此事务与事务应该是相互隔离的,避免出现脏数据
4)持久性:事务一旦提交,数据要持久化存储。
在Spring中,要想为一个方法添加事务管理的只需要为方法添加一个@Transitional注解即可。它是基于AOP面向切面编程来实现的,对原有代码没有侵入性,只需加上注解即可。@Transitional注解既可以用在方法上,也可以用来类上。如果在同一个类中的类和方法上都添加注解,方法上的注解会覆盖类上的注解。
下面介绍以下@Transitional注解中的几个属性。
属性名 | 作用 | 类型 |
---|---|---|
transactionManager | 用于指定被修饰类或者方法被那个事务管理器来管理 | String类型 |
propagation | 用于设置事务的传播行为 | Propagation类型 |
isolation | 设置事务的隔离级别: | Isolation类型 |
timeout | 设置超时时间 | int类型 |
readOnly | 设置事务是否为只读事务 | 布尔类型 |
rollbackFor | 指定异常类型,遇到指定异常时是回滚事务 | 字节码类型 |
rollbackForClassName | 设置异常类名,遇到指定异常类时回滚事务 | String类型 |
norollbackFor | 指定异常类型,遇到指定异常时不回滚事务 | 字节码类型 |
norollbackForClassName | 设置异常类名,遇到指定异常类时不回滚事务 | String类型 |
以上是@Transitional注解的属性,我们可以为这些属性配置属性值来达到管理事务的目的。
事务的传播特性主要是指不同业务对象中的事务方法之间相互调用时,事务的传播方式。这个主要由@Transitional注解的propagation属性来控制。propagation属性有七个属性值,这里只说两个常用的两个属性值,分别是Propagation.REQUIRED和Propagation.REQUIRES_NEW。
Propagation.REQUIRED规定当前如果没有事务创建新事务, 如果当前有事务参与当前事务
Propagation.REQUIRES_NEW规定被修饰方法必须是新事务, 如果有当前事务, 挂起当前事务并且开启新事务。
只读事务主要用于当一个方法中会执行多次查询操作时,可以将事务设置为只读事务,来保证数据的一致性,避免多次查询的数据不一致。只读事务是由readOnly属性来控制的,当属性值为true是,则事务为只读事务,为false是事务不为只读事务。
Spring中可以通过@Transitional注解的isolation属性来设置事务的隔离级别。 isolation有四个属性值:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。
DEFAULT(默认值):设置该属性值,事务会使用数据库的默认事务隔离级别。
READ_UNCOMMITTED(读未提交):最低事务隔离级别,设置该属性值, 会产生脏读,不可重复读和幻像读。
READ_COMMITTED(读已提交):保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。
REPEATABLE_READ(可重复读):这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。
SERIALIZABLE(串行化):这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 除了防止脏读,不可重复读外,还避免了幻读。
这里说一下事务的异步处理,我们知道,当一个事务被设置为只读事务时候,在这个事务执行完之前,当前数据库是不允许执行写操作的,这就造成了一个只读事务在执行读操作时候,后边来了一个写操作的方法,但是,现在事务是只读事务,不允许执行写操作,依据事务的传播特性,默认是如果当前没有事务,会创建新事务,如果当前有事务,会参与到当前事务中来。这样依赖就会报异常。因为当前事务是只读事务,不允许执行写操作。于是我们可以为方法设置睡眠时间等待只读事务执行完毕才能继续执行,但是这样显然是不可行的,十分影响性能。那么我们就可以修改事务的传播特性,创建新事务来规避这一异常。当然这是一种处理方法。还有就是把只读事务给取消。
这里就说一种异步处理的方法,我们开启一个子线程来异步处理这个事务,这样两个事务就不在一个线程中,也就互不影响了。但是这里就又有新的问题了,我们每次都要开启一个子线程,并发不高时, 还可以搞一搞,但是如果出现高并发,我们就会出现多个线程对象在内存中,这样就及其浪费资源。我们继续优化,可以使用线程池来获取线程从而代替我们自己创建线程。
在Spring中,可以通过使用@Async注解的方式来,使用Spring内置的线程池,使用这个注解需要实现在配置文件中配置开启线程池,Springboot项目只需在入口类上添加@EnableAsync即可开启线程池。
如果我们不想使用Spring内置的线程池,我们可以整合第三方的线程池或者自定义线程池,可以使用@Async的属性来设置我们的自定义线程池。如下:我自定义了一个线程池来交给Spring管理,并起名:asyncPoolExecutor。
@Configuration
public class SpringAsyncConfig {
/**核心线程数*/
private int corePoolSize=3;
/**最大线程数*/
private int maximumPoolSize=5;
/**线程空闲时间*/
private int keepAliveTime=30;
/**构建线程工厂*/
private ThreadFactory threadFactory=new ThreadFactory() {
//CAS算法
private AtomicInteger at=new AtomicInteger(1000);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "db-async-thread-"+at.getAndIncrement());
}
};
/**
* 自定义池对象(基于java中的ThreadPoolExecutor类)
* 池对象应用:
* 1)当池中线程数没有达到corePoolSize指定的值,每次请求都会创建新的线程并存储到池中
* 2)当池中线程数已经达到corePoolSize指定的值,并且线程都在忙,新的请求会进入阻塞队列
* 3)当池中线程数已经达到corePoolSize指定的值,并且线程都在忙,并且队列已满,此时会创建新的线程,直到达到maximumPoolSize.
* @return
*/
@Bean("asyncPoolExecutor")
public ThreadPoolExecutor newPoolExecutor() {
//创建阻塞式对象:基于数组存储结构,FIFO算法实现的一个阻塞式队列
BlockingQueue workQueue=
new ArrayBlockingQueue<>(10);
//创建池对象
ThreadPoolExecutor threadPoolExecutor=
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
threadFactory);
return threadPoolExecutor;
}
}
@Async(value = "asyncPoolExecutor")
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public Integer saveObject(SysLog log) {
int rows=sysLogMapper.insertObject(log);
try{Thread.sleep(5000);}catch(Exception e) {}
return rows;
}