前言
终于学习完了AOP,当方法存在事务时执行真相是怎样的?在某一个Service类中,f1调用f2,f1、f2使用不同的事务传播会有什么现象?f1、f2存在或不存在事务注解时会有什么现象?带着这些问题我们一起学习Spring事务
代码示例:
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/nba?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
@Service
public class TxService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void f1(PlayerData user){
jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
//当前测试先注释调用f2,后面放开用于测试事务转播机制
//f2(user);
}
@Transactional
public void f2(PlayerData user){
jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
}
public static class PlayerData{
public double playerHeigth;
public String playerName;
public int teamId;
public String teamName;
}
}
@SpringBootApplication
public class TxDemo {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(TxDemo.class);
TxService tx1Service = context.getBean("txService", TxService.class);
TxService.PlayerData playerData = new TxService.PlayerData();
playerData.playerHeigth = 1.98;
playerData.playerName = "科比";
playerData.teamName = "湖人";
playerData.teamId = 1001;
tx1Service.f1(playerData);
}
}
跟踪调用f1,在TransactionAspectSupport类中看到如下核心逻辑
事务处理简化版本如下:
//开启事务
TransactionInfo txInfo = createTransactionIfNecessary()
try{
//调用目标方法
invocation.proceedWithInvocation
}catch(Throwable e){
//回滚事务
completeTransactionAfterThrowing(txInfo)
throw ex;
}finally{
//清理事务
cleanupTransactionInfo(txInfo)
}
//提交事务
commitTransactionAfterReturning(txInfo)
开启事务
开启事务的两个核心操作:创建TransactionStatus、创建TransactionInfo
序列图:
核心流程描述:
- 从resources中获取当前线程的数据库连接,若存在,将事务状态封装为TransactionStatus返回,否则进入2
- 获取一个数据库连接,并关闭自动提交,将连接封装为ConnectionHolder并绑定到resources中
- 将事务信息封装为TransactionInfo绑定到transactionHolder中,并返回
核心类
TransactionInterceptor事务拦截器
开启处理事务的地方,最顶层实现自Advice,这就是前面Aop讲解5种通知一样的原理,效果如同Around
DataSourceTransactionManager资源管理器
拥有一个DataSource成员,提供事务接口,如:doBegin、doSuspend、doResume、doCommit、doRollback
在DataSourceTransactionManagerAutoConfiguration中被扫描到容器中
DataSource数据源
A factory for connections to the physical data source that
this DataSource object represents. A DataSource object
is the preferred means of getting a connection
连接到实体数据源的抽象,类似工厂模式,能够从数据源获取连接。常见的有:HikariDataSource、DruidDataSource。在yml文件中,spring.datasource下的信息将会封装为一个DataSource实例
TransactionSynchronizationManager事务同步管理器
为一个全局静态类,所有成员变量和函数都是静态的
/**
* Central delegate that manages resources and transaction synchronizations per thread
* 这里不好翻译,结合后面的代码会知道,是为每个事务管理回调接口
*/
public abstract class TransactionSynchronizationManager {
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
//线程变量,值为一个map,key为dataSource,value为ConnectionHolder。每个线程在同一个数据源下拥有各自的数据库连接
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
//线程变量,值为一个set,在事务执行的各个阶段(提交前、提交后 、完成前、完成后)能够调用遍历set中的回调
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
}
TransactionInfo事务信息
/**
* used to hold transaction information 用于保存事务信息
*/
protected static final class TransactionInfo {
//事务管理,提供事务操作接口
@Nullable
private final PlatformTransactionManager transactionManager;
//传播机制与隔离信息的抽象
@Nullable
private final TransactionAttribute transactionAttribute;
private final String joinpointIdentification;
//事务状态
@Nullable
private TransactionStatus transactionStatus;
//旧事务,如果有挂起操作,以前的事务会被存在到这个字段
@Nullable
private TransactionInfo oldTransactionInfo;
}
TransactionStatus事务状态
/**
* Representation of the status of a transaction 事务状态的呈现
*/
public class DefaultTransactionStatus extends AbstractTransactionStatus {
//实际保存DataSourceTransactionObject值,DataSourceTransactionObject中拥有ConnectionHolder
@Nullable
private final Object transaction;
//是否是新事务 REQUIRES_NEW开启新事务,为真。REQUIRED不存在时创建,为真
private final boolean newTransaction;
//是否是新同步,翻译成是否是新回调可能更好 这与前面讲的TransactionSynchronization有关,表示回调是在以前的事务中,还是新的事务中 REQUIRES_NEW时为真
private final boolean newSynchronization;
private final boolean readOnly;
private final boolean debug;
//挂起的资源,与传播机制有关。REQUIRES_NEW、NOT_SUPPORTED挂起前面的资源
@Nullable
private final Object suspendedResources;
}
/**
* Resource holder wrapping a JDBC Connection.jdbc connection的封装
*/
public class ConnectionHolder extends ResourceHolderSupport {
@Nullable
private ConnectionHandle connectionHandle;
//数据库连接
@Nullable
private Connection currentConnection;
private boolean transactionActive = false;
@Nullable
private Boolean savepointsSupported;
private int savepointCounter = 0;
}
transactionInfoHolder为ThreadLocal变量,每个线程都拥有自己的TransactionInfo信息,并发时互不影响,再加上TransactionInterceptor没有其他共享变量,所以即使全局只有一个TransactionInterceptor实例,也是线程安全的,再回顾前面事务回滚、清理事务、提交事务流程只需要TransactionInfo参数,所以整个流程也就并发安全了
执行sql
执行sql语句时只需要与开始事务时使用的相同连接就可以了,是如何做到的呢?这里以JdbcTemplate为例
引入spring-jdbc后,JdbcTemplate会扫描到容器中,且会注入yml文件配置的dataSource。执行sql时会进入如下流程
回滚事务
使用当前线程数据源对应的连接发送rollback语句,移除resources中当前线程数据源对应的ConnectionHolder,开启自动提交,将连接置为NOT_IN_USE
清理事务
提交事务
使用当前线程数据源对应的连接发送commit语句,移除resources中当前线程数据源对应的ConnectionHolder,开启自动提交,将连接置为NOT_IN_USE
以上流程为执行f1(不调用f2)时的主流程。其中忽略了判断事物是否存在、开启新事务、事务恢复、提交回滚时标记、上级提交回滚等存在事务传播时逻辑,下面通过事务传播来具体讲解这些细节
传播机制与坑
7中传播机制,网上已经有很多资料,这里结合源码只讲两种
REQUIRED
Support a current transaction, create a new one if none exists
支持当前事务,如果不存在就创建一个事务
public void f1(PlayerData user){
jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
f2(user);
}
@Transactional
public void f2(PlayerData user){
jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
int i = 1/0;
}
上述代码似乎符合REQUIRED事务传播的描述,f2发现f1没有事务,就创建一个。但实际运行结果两条sql语句都会成功,原因是整个过程中都没有开启事务,这里涉及的是代理的知识点(可以回顾前面的文章spring之Bean(2))。当类的接口被切面注解或事务标注时,普通bean会被封装为proxyBean,原始的bean会变为targetBean被proxyBean包裹着。调用接口时若方法具有Transactional注解就会添加TransactionIntercepter拦截器。这里f1没有注解所以无法添加拦截器,进入f1后调用f2的this指针是targetBean,不是proxyBean,更无法进入添加拦截器流程,所以f2也没有事务
要让调用f2具有事务只需要将targeBean变为proxyBean即可。注入自己。此时运行结果,f1能正常更新,f2会回滚
@Autowired
private TxService txService;
public void f1(PlayerData user){
jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
txService.f2(user);
}
注入自己与调用其他service是一样的道理
支持当前事务,如果不存在就创建一个事务。前提是当前被调用的方法是由proxyBean调用的
目前测试了后面句,不存在就创建事务,下面测试前半句支持当前事务。f1中调用f2时打断点
@Transactional
public void f1(PlayerData user){
jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
txService.f2(user);
}
@Transactional
public void f2(PlayerData user){
jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
}
调用图:数字表示执行顺序,数字后的txInfo表示进入该流程时需要的参数
在执行f1流程开启事务时此线程数据源下已经获取了connection,进入f2流程再开启事务时
此时connectionHolder不为空且被激活,进入handleExistingTransaction流程。在所有条件都排出后进入Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED流程。生成一个新的transactionStatus状态信息,信息中的newTransaction为false。从开启事务序列图可知,transactionStatus会被封装进transactionInfo,而transactionInfo会绑定到当前线程
从transactionInfoHolder获取事务信息即为f1开启事务时创建的信息赋值到oldTransactionInfo。将f2创建的事务信息赋值到当前线程中
可以看到f2在REQUIRED模式下,是否是新事务,是否是新同步都为假。这就是源码注释中支持当前事务的本质
那么我们看看这两个字段在f2提交或回滚时是如何起作用的,在f2函数中执行sql处设置断点。
提交
在前面f1不调用f2时会在这儿进入提交,但此时f2的txInfo2中的transactionStatus的newTransaction为假,由上图可知,所以不会进入提交。此时f2执行完毕从调用图的第7步返回,进入第9步,f1中的txInfo1中的transactionStatus的newTransaction为真,一起提交
回滚
在f2函数最后一样添加int i=1/0,测试回滚。在int i=1/0处设置断点,由图可知,此时newTransaction为假,不会回滚。进入else流程将连接信息设置rollbackOnly。f2流程执行完后,进入f1的回滚,f1中的txInfo1中的transactionStatus的newTransaction为真,一起回滚
REQUIRES_NEW
Create a new transaction, and suspend the current transaction if one exists
创建一个新的事务,如果当前已存在事务,则将其挂起。 在学习了REQUIRED模式后应该可以猜到大致的执行流程,f2开始事务时会获取一个新的数据库连接,f2的txStatus中的newTransaction和newSynchronization会为真。f2的提交和回滚都会在新的事务中执行,而不是回到f1中。下面通过debug源码来证实一下
@Transactional
public void f1(PlayerData user){
jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
txService.f2(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void f2(PlayerData user){
jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
}
新连接
在调用f2处设置断点,在handleExistingTransaction进入开启新事务流程
再次进入绑定txInfo的地方
提交
在新的连接中独立提交,与前面只调用f1(内部不调用f2)相同
回滚
在新的连接中独立回滚,与前面只调用f1(内部不调用f2)相同
事务执行阶段回调
上面的截图中会看到一个newSynchronization的字段,指明回调是否是新的,即是否在新事务中回调。
示例代码
@Transactional
public void f1(PlayerData user){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void beforeCommit(boolean readOnly) {
System.out.println("f1 beforeCommit:"+readOnly);
}
@Override
public void beforeCompletion() {
System.out.println("f1 beforeCompletion");
}
@Override
public void afterCommit() {
System.out.println("f1 afterCommit");
}
@Override
public void afterCompletion(int status) {
System.out.println("f1 afterCompletion");
}
});
jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
System.out.println("update f1...");
txService.f2(user);
}
@Transactional
public void f2(PlayerData user){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void beforeCommit(boolean readOnly) {
System.out.println("f2 beforeCommit:"+readOnly);
}
@Override
public void beforeCompletion() {
System.out.println("f2 beforeCompletion");
}
@Override
public void afterCommit() {
System.out.println("f2 afterCommit");
}
@Override
public void afterCompletion(int status) {
System.out.println("f2 status");
}
});
jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
System.out.println("update f2...");
}
效果如图:由于f2使用的f1的事务,所以f1调用完f2后才会进入提交阶段调用同步接口
将f2的传播类型改为REQUIRES_NEW
效果如图:f2在新事务中运行,f2进入提交阶段后就会调用同步接口,f2处理完后f1进入提交阶段调用同步接口
同步调用接口的顺序如下
总结
当方法或类具有事务注解,方法被调用时实际是由代理对象调用的,目标方法执行前会添加TransactionInterceptor拦截器,拦截器全局只有一个,顶层实现自Advice,效果如同Around。代理对象调用方法时实际的执行流程时开启事务、执行目标方法、回滚事务、清理事务、提交事务
开启事务时总会先从TransactionSynchronizationManager的ThreadLocal变量resources中获取当前数据源对应的Connection,如果connection已经存在,则会进入事务存在流程,根据当前事务传播机制决定是加入当前事务还是开启新事务,获取到的连接信息会被绑定到resource中并被包装为TransactionStatus,在txStatus中有两个重要的字段newTransaction、newSynchronization来决定后面的提交或回滚是在当前事务执行还是新事务执行,txStatus会被包装为TransactionInfo,在txInfo中有一个oldTxInfo用来保存上一次的事务,txInfo会被绑定到TransactionInterceptor的ThreadLocal变量transactionInfoHolder中
执行sql时常用的jdbcTemplate或其他方式会得到当前数据源的抽象,最终从resources中获取到当前线程的数据库连接
回滚或提交事务时执行完rollback或commit语句后,会释放resources中连接,开启自动提交,并将连接置为NOT_IN_USE状态
清理事务时会解绑transactionInfoHolder中的txInfo
完结🎉