在Spring中,数据库事务是通过AOP技术来提供服务的。
@Transactional
对于事务,需要通过标注告诉Spring在什么地方启用数据库事务功能。对于声明式事务,是使用@Transactional进行标注的。这个注解可以标注在类或者方法上,当它标注在类上时,代表这个类所有公共(public)非静态的方法都将启用事务功能。
在@Transactional中,还允许配置许多的属性,如事务的隔离级别和传播行为。;又如异常类型,从而确定方法发生什么异常下回滚事务或者发生什么异常下不回滚事务等。这些配置内容,是在Spring IoC容器在加载时就会将这些配置信息解析出来,然后把这些信息存到事务定义器(TransactionDefinition接口的实现类)里,并且记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务。
@Transactional源码分析
package org.springframework.transaction.annotation;
/**** imports ****/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 通过bean name 指定事务管理器
@AliasFor("transactionManager")
String value() default "";
// 同value属性
@AliasFor("value")
String transactionManager() default "";
// 指定传播行为
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 {};
}
关于注解@Transactional值得注意的是它可以放在接口上,也可以放在实现类上。但是Spring团队推荐放在实现类上,因为放在接口上将使得你的类基于接口的代理时它才生效。我们知道在Spring可以使用JDK动态代理,也可以使用CGLIG动态代理。如果使用接口,那么你将不能切换为CGLIB动态代理,而只能允许你使用JDK动态代理,并且使用对应的接口去代理你的类,这样才能驱动这个注解,这将大大地限制你的使用,因此在实现类上使用@Transactional注解才是最佳的方式.
Spring事务管理器
事务的打开、回滚和提交是由事务管理器来完成的。在Spring中,事务管理器的顶层接口为PlatformTransactionManager,Spring还为此定义了一些列的接口和类,如图:
PlatformTransactionManager源码分析
package org.springframework.transaction;
public interface PlatformTransactionManager {
// 获取事务,它还会设置数据属性
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
配置MyBatis
mybatis.mapper-locations=classpath:com/springboot/chapter6/mapper/*.xml
mybatis.type-aliases-package=com.springboot.chapter6.pojo
依赖于mybatis-spring-boot-starter之后,Spring Boot会自动创建事务管理器、MyBatis的SqlSessionFactory和SqlSessionTemplate等内容。
Springbot 启动文件
package com.springboot.chapter6.main;
/**** imports ****/
@MapperScan(
basePackages = "com.springboot.chapter6",
annotationClass = Repository.class)
@SpringBootApplication(scanBasePackages = "com.springboot.chapter6")
public class Chapter6Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Chapter6Application.class, args);
}
// 注入事务管理器,它由Spring Boot自动生成
@Autowired
PlatformTransactionManager transactionManager = null;
// 使用后初始化方法,观察自动生成的事务管理器
@PostConstruct
public void viewTransactionManager() {
// 启动前加入断点观测
System.out.println(transactionManager.getClass().getName());
}
}
- 这里使用了@MapperScan扫描对应的包,并限定了只有被注解@Repository标注的接口,这样就可以把MyBatis对应的接口文件扫描到Spring IoC容器中了。
- 这里通过注解@Autowired直接注入了事务管理器,它是通过Spring Boot的机制自动生成的,并不需要我们去关心;
- 而在viewMyBatis方法中,加入了注解@PostConstruct,所以在这个类对象被初始化后,会调用这个方法,在这个方法中,因为先前已经将IoC容器注入进来,所以可以通过IoC容器获取对应的Bean以监控它们
*** 监控Spring Boot自动初始化的对象如下 ***:
可以看到Spring Boot已经生成了事务管理器,这便是Spring Boot的魅力,*允许我们以最小的配置代价运行Spring的项目。*那么按照之前的约定使用注解@Transactional标注类和方法后,Spring的事务拦截器就会同时使用事务管理器的方法开启事务,然后将代码织入Spring数据库事务的流程中,如果发生异常,就会回滚事务,如果不发生异常,那么就会提交事务,这样就我们从大量的冗余代码中解放出来了。
隔离级别
数据库事务
-
Atomic(原子性):事务中包含的操作被看作一个整体的业务单元,这个业务单元中的操作要么全部成功,要么全部失败,不会出现部分失败、部分成功的场景。
-
Consistency(一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性。
-
Isolation(隔离性):可能多个应用程序线程同时访问同一数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的发生。
-
Durability(持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘当中,即使断电重启后也可以提供给应用程序访问。
第一类隔离丢失
第二类隔离丢失
隔离级别
未提交读
未提交读(read uncommitted)是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。可能出现脏读:
读写提交
读写提交(read committed)隔离级别,是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。
克服脏读:
但是存在如下问题:
这样的现象我们称为不可重复读,这就是读写提交的一个不足。
可重复读
可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化,影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出了可重复读的隔离级别。这样就能够克服不可重复读的现象如表所示。
但是这样也会引发新的问题的出现,这就是幻读:
这里的笔数不是数据库中的值,而是一个统计值。商品库存,则是数据库中的值,这一点是要注意的。
也就是说,幻读不是针对一条记录,而是多条记录,例如,这51笔交易笔数就是多条数据库记录统计出来的。而可重复读是针对数据库的**单一条记录,**例如,商品的库存是以数据库里面的一条记录存储的,它可以产生可重复读,而不能产生幻读。
串行化
串行化(Serializable)是数据库最高的隔离级别,它会要求所有的SQL都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性。
使用合理的隔离级别
现实中一般而言,选择隔离级别会以读写提交为主,它能够防止脏读,而不能避免不可重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据库而使用其他的手段。例如,使用Redis作为数据载体。
对于隔离级别,不同的数据库的支持也是不一样的。例如,Oracle只能支持读写提交和串行化,而MySQL则能够支持4种,对于Oracle默认的隔离级别为读写提交,MySQL则是可重复读,这些需要根据具体数据库来决定。
pring Boot 可以通过配置文件指定默认的隔离级别。例如,当我们需要把隔离级别设置为读写提交时,可以在application.properties文件加入默认的配置,如下:
#隔离级别数字配置的含义:
#-1 数据库默认隔离级别
#1 未提交读
#2 读写提交
#4 可重复读
#8 串行化
#tomcat数据源默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=2
#dbcp2数据库连接池默认隔离级别
#spring.datasource.dbcp2.default-transaction-isolation=2
传播行为
传播行为是方法之间调用事务采取的策略问题
在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚那些出现异常的交易,而不是整个批量任务,这样就能够使得那些没有问题的交易可以顺利完成,而有问题的交易则不做任何事情,如图:
在Spring中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。
图中,批量任务我们称之为当前方法,那么批量事务就称为当前事务,当它调用单个交易时,称单个交易为子方法,当前方法调用子方法的时候,让每一个子方法不在当前事务中执行,而是创建一个新的事务去执行子方法,我们就说当前方法调用子方法的传播行为为新建事务。此外,还可能让子方法在无事务、独立事务中执行,这些完全取决于你的业务需求。
传播行为的定义
在Spring事务机制中对数据库存在7种传播行为,它是通过枚举类Propagation定义的。下面先来研究它的源码,如代码所示:
package org.springframework.transaction.annotation;
/**** imports ****/
public enum Propagation {
/**
* 需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务,
* 否则新建一个事务运行子方法
*/
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
/**
* 支持事务,如果当前存在事务,就沿用当前事务,
* 如果不存在,则继续采用无事务的方式运行子方法
*/
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
/**
* 必须使用事务,如果当前没有事务,则会抛出异常,
* 如果存在当前事务,就沿用当前事务
*/
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
/**
* 无论当前事务是否存在,都会创建新事务运行方法,
* 这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立
*/
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
/**
* 不支持事务,当前存在事务时,将挂起事务,运行方法
*/
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
/**
* 不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行
*/
NEVER(TransactionDefinition.PROPAGATION_NEVER),
/**
* 在当前方法调用子方法时,如果子方法发生异常,
* 只回滚子方法执行过的SQL,而不回滚当前方法的事务
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) { this.value = value; }
public int value() { return this.value; }
}
REQUIRED, REQUIRES_NEW,NESTED是最常用的。
在大部分的数据库中,一段SQL语句中可以设置一个标志位,然后后面的代码执行时如果有异常,只是回滚到这个标志位的数据状态,而不会让这个标志位之前的代码也回滚。这个标志位,在数据库的概念中被称为保存点(save point),Spring为我们生成了nested事务,而从其日志信息中可以看到保存点的释放,可见Spring也是使用保存点技术来完成让子事务回滚而不致使当前事务回滚的工作。注意,并不是所有的数据库都支持保存点技术,因此Spring内部有这样的规则:当数据库支持保存点技术时,就启用保存点技术;如果不能支持,就新建一个事务去运行你的代码,即等价于REQUIRES_NEW传播行为。
NESTED传播行为和REQUIRES_NEW还是有区别的。NESTED传播行为会沿用当前事务的隔离级别和锁等特性,而REQUIRES_NEW则可以拥有自己独立的隔离级别和锁等特性,这是在应用中需要注意的地方。
@Transactional自调用失效问题
@Autowired
private UserDao userDao = null;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED)
public int insertUsers(List<User> userList) {
int count = 0;
for (User user : userList) {
// 调用自己类自身的方法,产生自调用问题
count += insertUser(user);
}
return count;
}
// 传播行为为REQUIRES_NEW,每次调用产生新事务
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRES_NEW)
public int insertUser(User user) {
return userDao.insertUser(user);
}
Spring数据库事务的约定,其实现原理是AOP,而AOP的原理是动态代理,
在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,这样Spring就不能把你的代码织入到约定的流程中,于是就产生了失败场景。
为了克服这个问题用一个Service去调用另一个Service,这样就是代理对象的调用,Spring才会将你的代码织入事务流程。当然也可以从Spring IoC容器中获取代理对象去启用AOP,例如,我们再次对UserServiceImpl进行改造,如代码清单所示。
package com.springboot.chapter6.service.impl;
/**** imports ****/
@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {
@Autowired
private UserDao userDao = null;
private ApplicationContext applicationContext = null;
// 实现生命周期方法,设置IoC容器
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED)
public int insertUsers(List<User> userList) {
int count = 0;
// 从IoC容器中取出代理对象
UserService userService = applicationContext.getBean(UserService.class);
for (User user : userList) {
// 使用代理对象调用方法插入用户,此时会织入Spring数据库事务流程中
count += userService.insertUser(user);
}
return count;
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRES_NEW)
public int insertUser(User user) {
return userDao.insertUser(user);
}
......
}
从代码中我们实现了ApplicationContextAware接口的setApplicationContext方法,这样便能够把IoC容器设置到这个类中来。于是在insertUsers方法中,我们通过IoC容器获取了UserService的接口对象。但是请注意,这将是一个代理对象,并且使用它调用了传播行为为REQUIRES_NEW的insertUser方法,这样才可以运行成功。