Spring事务的介绍,以及基于注解@Transactional的声明式事务

前言

事务是一个非常重要的知识点,前面的文章已经有介绍了关于SpringAOP代理的实现过程;事务管理也是AOP的一个重要的功能。

事务的基本介绍
数据库事务特性:
  • 原子性
  • 一致性
  • 隔离性
  • 持久性
事务的隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

  • 读未提交(READ UNCOMMITTED)
  • 读已提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 串行化(SERIALIZABLE)

至于为什么会设定数据库的隔离级别,原因是由于在并发操作数据库的时候可能会引起脏读、不可重复读、幻读、第一类丢失更新、第二类更新丢失等现象。

脏读:

事物A读取事物B尚未提交的更改数据,并做了修改;此时如果事物B回滚,那么事物A读取到的数据是无效的,此时就发生了脏读。

不可重复读:

一个事务执行相同的查询两次或两次以上,每次都得到不同的数据。如:A事物下查询账户余额,此时恰巧B事物给账户里转账100元,A事物再次查询账户余额,那么A事物的两次查询结果是不一致的。

幻读:

A事物读取B事物提交的新增数据,此时A事物将出现幻读现象。幻读与不可重复读容易混淆,如何区分呢?幻读是读取到了其他事物提交的新数据,不可重复读是读取到了已经提交事物的更改数据(修改或删除)

第一类丢失更新现象:

撤销一个事务的时候,把其它事务已提交的更新数据覆盖了。这是完全没有事务隔离级别造成的。如果事务1被提交,另一个事务被撤销,那么会连同事务1所做的更新也被撤销。

第二类丢失更新现象:

它和不可重复读本质上是同一类并发问题,通常将它看成不可重复读的特例。当两个或多个事务查询相同的记录,然后各自基于查询的结果更新记录时会造成第二类丢失更新问题。每个事务不知道其它事务的存在,最后一个事务对记录所做的更改将覆盖其它事务之前对该记录所做的更改。

针对以上问题,其实可以有其它的解决方法,设置数据库隔离级别就是其中的一种,简单说一下数据库四个隔离级别的作用,见下表

image

简单总结:

  • Read Uncommitted存在:脏读、不可重复读、第二类丢失更新和幻读问题。
  • Read committed存在:不可重复读、第二类丢失更新和幻读问题。
  • Repeatable Read存在:幻读问题。
  • Serializable 不存在问题。
接下来我们看一下Spring支持事务的核心接口:

概要图:

image

TransactionDefinition
  • 看源码(TransactionDefinition.java)
public interface TransactionDefinition {

	/**
	 * 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中。
	 */
	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;
	/**
	 * 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同
	 */
	int PROPAGATION_NESTED = 6;

	/**
	 * 使用后端数据库默认的隔离级别。
	 */
	int ISOLATION_DEFAULT = -1;

	/**
	 * READ_UNCOMMITTED 隔离级别
	 */
	int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

	/**
	 *  READ_COMMITTED 隔离级别
	 */
	int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;

	/**
	 * REPEATABLE_READ 隔离级别
	 */
	int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

	/**
	 * SERIALIZABLE 隔离级别
	 */
	int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


	/**
	 * 默认超时时间
	 */
	int TIMEOUT_DEFAULT = -1;

	/**
	 *  获取事物传播特性
	 * @return
	 */
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}
	/**
	 * 获取事物隔离级别
	 * @return
	 */
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}
	/**
	 * 获取事物超时时间
	 * @return
	 */
	default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

	/**
	 * 判断事物是否可读
	 * @return
	 */
	default boolean isReadOnly() {
		return false;
	}

	/**
	 *  获取事物名称
	 * @return
	 */
	@Nullable
	default String getName() {
		return null;
	}

	static TransactionDefinition withDefaults() {
		return StaticTransactionDefinition.INSTANCE;
	}

}
Spring事务传播行为
事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
Spring支持的隔离级别
隔离级别 描述
DEFAULT 使用数据库本身使用的隔离级别 ORACLE(读已提交) MySQL(可重复读)
READ_UNCOMMITTED 读未提交(脏读)最低的隔离级别,一切皆有可能。
READ_COMMITTEN 读已提交,ORACLE默认隔离级别,有幻读以及不可重复读风险。
REPEATABLE_READ 可重复读,解决不可重复读的隔离级别,但还是有幻读风险。MySQL默认隔离级别
SERLALIZABLE 串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了
Spring事务基础结构中的中心接口(PlatformTransactionManager.JAVA)
  • 看源码
/**
 * Spring事务基础结构中的中心接口
 */
public interface PlatformTransactionManager extends TransactionManager {

	/**
	 * 根据指定的传播行为,返回当前活动的事务或创建新事务。
	 * @param definition
	 * @return
	 * @throws TransactionException
	 */
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
	throws TransactionException;

	/**
	 * 就给定事务的状态提交给定事务
	 * @param status
	 * @throws TransactionException
	 */
	void commit(TransactionStatus status) throws TransactionException;
	/**
	 * 执行给定事务的回滚
	 * @param status
	 * @throws TransactionException
	 */
	void rollback(TransactionStatus status) throws TransactionException;

}
  • 源码分析

    Spring将事务管理委托给底层的持久化框架来完成,因此,Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现类,我们看一下具体有哪些事务管理器:

image

简单说一下图中标注的几个事务管理器:

事务管理器 描述
DataSourceTransactionManager 提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理
JpaTransactionManager 提供对单个javax.persistence.EntityManagerFactory事务支持,用于集成JPA实现框架时的事务管理
JtaTransactionManager 提供对分布式事务管理的支持,并将事务管理委托给Java EE应用服务器事务管理器

看完事务管理器,我们看一下概览图中的第三部分TransactionStatus事务状态描述接口类

Spring事务状态描述
  • 看源码(TransactionStatus.java)
// 事务状态描述
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

	/**
	 * 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。
	 * @return
	 */
	boolean hasSavepoint();
	/**
	 *  将会话刷新到数据存储区
	 */
	@Override
	void flush();

}

继续看一下它的父类TransactionExecution.javaSavepointManager.java

public interface TransactionExecution {
	/**
	 * 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行)
	 * @return
	 */
	boolean isNewTransaction();
	/**
	 * 设置事务仅回滚。
	 */
	void setRollbackOnly();
	/**
	 * 返回事务是否已标记为仅回滚
	 * @return
	 */
	boolean isRollbackOnly();
	/**
	 * 返回事物是否已经完成,无论提交或者回滚。
	 * @return
	 */
	boolean isCompleted();

}
public interface SavepointManager {
	/**
	 * 创建一个新的保存点。
	 * @return
	 * @throws TransactionException
	 */
	Object createSavepoint() throws TransactionException;
	/**
	 * 回滚到给定的保存点。
	 *   注意:调用此方法回滚到给定的保存点之后,不会自动释放保存点,
	 *        可以通过调用releaseSavepoint方法释放保存点。
	 * @param savepoint
	 * @throws TransactionException
	 */
	void rollbackToSavepoint(Object savepoint) throws TransactionException;
	/**
	 * 显式释放给定的保存点。(大多数事务管理器将在事务完成时自动释放保存点)
	 * @param savepoint
	 * @throws TransactionException
	 */
	void releaseSavepoint(Object savepoint) throws TransactionException;

}

到这里Spring的事务相关概念已经大概介绍完了,我们先来熟悉一下Spring的编程式事务的应用实例:

Spring编程式事务
package com.vipbbo.spring.transaction;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

public class MyTransaction {

	private JdbcTemplate jdbcTemplate;
	private DataSourceTransactionManager transactionManager;
	private DefaultTransactionDefinition transactionDefinition;
	private String insertSql = "insert into account (balance) values ('100')";

	public void save(){
		// 初始化jdbcTemplate
		DataSource dataSource = getDataSource();
		jdbcTemplate = new JdbcTemplate(dataSource);

		// 创建事务管理器
		transactionManager = new DataSourceTransactionManager();
		transactionManager.setDataSource(dataSource);

		// 定义事务属性
		transactionDefinition = new DefaultTransactionDefinition();
		transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

		// 开启事务
		TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

		// 执行业务逻辑
		try {
			jdbcTemplate.execute(insertSql);
			//int i = 1/0;
			jdbcTemplate.execute(insertSql);
			transactionManager.commit(transactionStatus);
		} catch (DataAccessException e) {
			e.printStackTrace();
		} catch (TransactionException e) {
			transactionManager.rollback(transactionStatus);
			e.printStackTrace();
		}

	}

	public DataSource getDataSource(){
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUrl("jdbc:mysql://192.168.1.100:3306/spring_aop?" +
				"useSSL=false&useUnicode=true&characterEncoding=UTF-8");
		dataSource.setUsername("root");
		dataSource.setPassword("root");
		return dataSource;
	}
}

测试类:

package com.vipbbo.spring.transaction;

import org.junit.jupiter.api.Test;

public class MyTransactionTest {

	@Test
	public void test1() {
		MyTransaction myTransaction = new MyTransaction();
		myTransaction.save();
	}
}

分析

运行测试类,一旦放开int i = 1/0;这段代码,再抛出异常之后手动回滚事务,所以数据库表不会增加记录。

基于@Transactional注解的声明式事务
其底层建立在`AOP`的基础之上,对方法前后进行拦截,然后在目标方法开始之前创建一个或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。通过声明式事务,无需在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相应的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。

非XMl方式配置声明式事务

package com.vipbbo.spring.transaction;


import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(propagation = Propagation.REQUIRED)
public interface AccountByAnnotationService {
	void save() throws RuntimeException;
}

实现类:

package com.vipbbo.spring.transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * 账户接口实现
 *
 * @author paidaxing
 */
@Service("accountByAnnotationService")
public class AccountByAnnotationServiceImpl implements AccountByAnnotationService {


	@Autowired
	private JdbcTemplate jdbcTemplate;
	private static String insertSql = "insert into account(balance) values (100)";

	@Override
	public void save() throws RuntimeException {
		System.out.println("======开始执行sql======");
		jdbcTemplate.execute(insertSql);
		System.out.println("======sql执行结束======");
		System.out.println("======准备抛出异常======");
		throw new RuntimeException("手动抛出异常");
	}

}

注解开启声明式事务

配置类

package com.vipbbo.spring.config;


import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.net.ProtocolException;

@ComponentScan(basePackages = {"com.vipbbo"})
@Configuration
//开启基于注解的声明式事务
@EnableTransactionManagement
public class SpringConfig {


	/**
	 * 注解数据源
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public DataSource dataSource() throws ProtocolException {
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUsername("root");
		dataSource.setPassword("root");
		dataSource.setUrl("jdbc:mysql://192.168.1.100:3306/spring_aop?" +
				"useSSL=false&useUnicode=true&characterEncoding=UTF-8");
		return dataSource;
	}

	/**
	 * 注册JdbcTemplate
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public JdbcTemplate jdbcTemplate() throws ProtocolException{
		// 两种方式获取DataSOurce
		//1. 直接在方法上放置参数 public JdbcTemplate jdbcTemplate(DataSource dataSource)
		// 默认会去容器去取
		// 2. 如下: 调用上面的方法
		//spring对@Configuration类有特殊处理,注册组件的方法多次调用只是在IOC容器中找组件
		return new JdbcTemplate(dataSource());
	}

	/**
	 * 注册事务管理器
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public PlatformTransactionManager transactionManager() throws ProtocolException{
		//需要传入dataSource
		return new DataSourceTransactionManager(dataSource());
	}
}

测试代码类

package com.vipbbo.spring.transaction;

import com.vipbbo.spring.config.SpringConfig;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MyTransactionByAnnotationTest {

	@Test
	public void test(){
		ApplicationContext tx = new AnnotationConfigApplicationContext(SpringConfig.class);
		AccountByAnnotationService annotationService =
				tx.getBean("accountByAnnotationService",AccountByAnnotationService.class);
		annotationService.save();
	}
}

测试类运行截图:

image
我们在上述实现类中手动抛出了一个异常,Spring会自动回滚事务,我们查看数据库可以知道并没有新增数据。。

注意重中之重

默认情况下Spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果

参考文章://cloud.tencent.com/developer/article/1589894

微信搜索【码上遇见你】第一时间获取更多精彩内容!