事务管理是应用系统开发中必不可少的一部分,而Spring则为事务管理提供了丰富的功能支持.
在讲解其实现原理前,我们先来看看使用Spring的事务管理机制给我们带来的好处:它极大减少了样板式代码,提高了代码的清晰性和可维护性.我这样讲你可能没什么感觉,下面我通过原生的jdbc事务处理代码例子,以及Spring的事务处理代码例子来突出说明这个好处.
先建立三张表,表字段如下:
需求是要往这三张表保存数据,保存顺序是country,city,category.其中前两张表要求是原子操作,后一张表是另一个原子操作,即后一张表操作失败不需回滚前两张表的数据.
为了实现这个要求,原生的jdbc事务处理代码实现是这样的(可以看到大量的样板式代码):
@Slf4j
@Service
public class AddressServiceImpl implements AddressService {
@Autowired
private DataSource dataSource;
@SneakyThrows
@Override
public String transactionOriginalResearch() {
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
short countryId;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 其他业务逻辑处理
// ......
preparedStatement = conn.prepareStatement("insert into country(country) values (?)", Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, "中国");
preparedStatement.executeUpdate();
resultSet = preparedStatement.getGeneratedKeys();
resultSet.next();
countryId = resultSet.getShort(1);
} catch (Exception e) {
conn.rollback();
throw new RuntimeException(e);
} finally {
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
}
short cityId;
try {
// 其他业务逻辑处理
// ......
preparedStatement = conn.prepareStatement("insert into city(city, country_id) values (?,?)", Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, "北京");
preparedStatement.setShort(2, countryId);
preparedStatement.executeUpdate();
resultSet = preparedStatement.getGeneratedKeys();
resultSet.next();
cityId = resultSet.getShort(1);
} catch (Exception e) {
conn.rollback();
throw new RuntimeException(e);
} finally {
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
}
// country表和city表均保存成功,此处设置个Savepoint(保存点)
Savepoint savepointCity = conn.setSavepoint("city");
int affect;
try {
// 其他业务逻辑处理
// ......
preparedStatement = conn.prepareStatement("insert into category(name) values (?)");
preparedStatement.setString(1, "分类名称");
affect = preparedStatement.executeUpdate();
if (true) {
throw new RuntimeException("模拟抛出异常");
}
conn.commit();
} catch (Exception e) {
conn.rollback(savepointCity);
throw new RuntimeException(e);
} finally {
if (preparedStatement != null) {
preparedStatement.close();
}
}
return countryId + " " + cityId + " " + affect;
}
}
}
可以看到,事务包含了复杂的语句,我们通过设置Savepoint(保存点)回滚到了事务中某个特殊的点,而不是回滚整个事务.
再来看使用Spring的声明式事务的代码实现:
@Slf4j
@Service
public class AddressServiceImpl implements AddressService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private AddressHelper addressHelper;
@Override
@Transactional(timeout = 6000, rollbackFor = Exception.class)
public String transactionResearch() {
String res = addressHelper.save();
// 其他业务逻辑处理
// ......
int affect = jdbcTemplate.update("insert into category(name) values (?)", "分类名称");
if (true) {
throw new RuntimeException("模拟抛出异常");
}
return res + " " + affect;
}
}
@Component
public class AddressHelper {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public String save() {
// 其他业务逻辑处理
// ......
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement("insert into country(country) values (?)", Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, "中国");
return preparedStatement;
}, keyHolder);
short countryId = keyHolder.getKey().shortValue();
// 其他业务逻辑处理
// ......
GeneratedKeyHolder keyHolder1 = new GeneratedKeyHolder();
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement("insert into city(city,country_id) values (?,?)", Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, "北京");
preparedStatement.setInt(2, countryId);
return preparedStatement;
}, keyHolder1);
short cityId = keyHolder1.getKey().shortValue();
return countryId + " " + cityId;
}
}
注意AddressHelper上的事务属性设置Propagation.REQUIRES_NEW.可以看到仅仅简单加上@Transactional注解,Spring就替我们完成了所有的事务操作.这样是不是就深刻感受到Spring带来的好处了.
另外Spring也提供了编程式事务,通过使用TransactionTemplate,这里不再赘述.
使用@Transactional有一些需要注意的地方:
- Spring默认情况下会对(RuntimeException)及其子类来进行回滚,在遇见Exception及其子类的时候则不会进行回滚操作
- @Transactional注解应该只被应用到public方法上,这是由Spring AOP的本质决定的
好,下面开始进入主题,我们知道@Transactional注解要生效的话,需配置@EnableTransactionManagement,不过如果是使用SpringBoot的话,就可以不需要了:
@SpringBootApplication
@EnableTransactionManagement // 这行注解其实可以不需要,在TransactionAutoConfiguration自动配置类里已经带有此注解
public class SpringTransactionalApplication {
public static void main(String[] args) {
SpringApplication.run(SpringTransactionalApplication.class, args);
}
}
TransactionAutoConfiguration自动配置类定义了很多与事务处理相关的bean,其中与@Transactional注解息息相关的是这个类TransactionInterceptor.
每个带有@Transactional注解的方法都会创建一个切面,所有的事务处理逻辑就是由这个切面完成的,这个切面的具体实现就是TransactionInterceptor类.
注意,这个TransactionInterceptor是个单例对象,所有带有@Transactional注解的方法都会经由此对象代理(省略无关代码):
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
// ......
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
// ......
}
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// ......
// 这里TransactionAttributeSource对象保存了应用内所有方法上的@Transactional注解属性信息,利用Map来保存,其中
// key由目标方法method对象+目标类targetClass对象组成,所以通过method+targetClass就能唯一找到目标方法上的@Transactional注解属性信息
TransactionAttributeSource tas = getTransactionAttributeSource();
// 这个TransactionAttribute对象就保存了目标方法@Transactional注解所有的属性配置,如timeout,propagation,readOnly等等,
// 后续就利用这些属性完成对应的操作
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// ......
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// 这里最终会调用到目标方法
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// 目标方法抛出了异常,根据@Transactional注解属性配置决定是否需要回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
// ......
// 目标方法正常执行完成,提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
上面的completeTransactionAfterThrowing和commitTransactionAfterReturning方法最终也是使用的jdbc的Savepoint和Connection来完成回滚和提交,就如同我们使用原生的jdbc代码那样操作,具体的实现细节我就不展开了.关于原理解析就到这里.
@Transactional注解属性含义:
- value:可选的限定描述符,指定使用的事务管理器
- propagation:可选的事务传播行为设置
- isolation: 可选的事务隔离级别设置
- readOnly:读写或只读事务,默认读写
- timeout:事务超时时间设置
- rollbackFor:导致事务回滚的异常类数组
- rollbackForClassName:导致事务回滚的异常类名字数组
- oRollbackFor:不会导致事务回滚的异常类数组
- noRollbackForClassName:不会导致事务回滚的异常类名字数组
重点关注propagation,isolation,面试常常问到.
propagation各属性值含义:
- Propagation.REQUIRED:如果有事务,那么加入事务,没有的话新创建一个
- Propagation.NOT_SUPPORTED:这个方法不开启事务
- Propagation.REQUIREDS_NEW:不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
- Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常
- Propagation.NEVER:不能在一个事务中执行,就是当前必须没有事务,否则抛出异常
- Propagation.SUPPORTS:若当前存在事务,则在事务中运行.否则以非事务形式运行
- Propagation.NESTED:如果当前存在事务,则运行在一个嵌套的事务中,如果当前没有事务,则按照REQUIRED属性执行.它只对DataSourceTransactionManager事务管理器起效
关于Spring如何处理该属性参见AbstractPlatformTransactionManager类的getTransaction方法.
isolation各属性值含义:
- TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据.该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别.比如PostreSQL实际上并没有此级别.
- TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据.该级别可以防止脏读,这也是大多数情况下的推荐值.
- TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同.该级别可以防止脏读和不可重复读.
- TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读.但是这将严重影响程序的性能,通常情况下也不会用到该级别.
源码github地址和gitee地址(代码同步):
https://github.com/jufeng98/java-master?github.com/jufeng98/java-master
https://gitee.com/jufeng9878/java-master?gitee.com/jufeng9878/java-master