需要明确一点:Spring 使用 BeanPostProcessor 来处理 Bean 中的标注
重要参考链接:
https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html
目录
3、Spring @Transactional的传播行为(propagation属性解释)
4、Spring @Transactional的隔离级别(同理于事务的隔离级别)(isolation属性解释)
2、接口PlatformTransactionManager
4、类TransactionTemplate :简化了编程式事务和异常的处理
2、基于 TransactionTemplate 的编程式事务管理
3.1 基于 TransactionInterceptor 的声明式事务管理
3.2 基于 TransactionProxyFactoryBean的声明式事务管理
3.4 基于 @Transactional 的声明式事务管理
5.3 方法加事务的时间点 比对 方法中db操作对数据库加锁的时间点
项目中常遇到的疑问点:
1、加的@Transactional关键字什么时候会失效(至于private、protected方法这些暂时不验证了,因为已经说要放在public上了,至于为什么,去研究Aop、动态代理)
2、同一个bean内,A方法调用B方法,事务分别加在A、B上,会有怎样的作用范围(已测试)
3、TransactionTemplate和@Transactional区别与联系(目前理解一个是主动去加入事务,一个是通过AOP(织入)的方式加入事务 )
4、事物控制范围应该包含哪些操作,比如对文件的操作是否应该包含在事物内 ( 或者说锁数据库是从加事务开始呢?还是从操作数据库开始呢?)
回答:从5.3.1中1中的测试方法可以看出,应该是从操作数据库开始的,而且在底层是InnoDB引擎的前提下,这个读写数据库的过程都是不加锁的
5、如何理解事务,是不是指的都是对数据库的操作呢(是)
6、Transactional如何实现的事务的原子性(测试时可以发现,有多个db操作的事务,执行完一个db操作后立即查看数据库,是不会有变化的,在整个事务结束后,才能看到数据库的变化)
7、mybatis如何进行事务管理(去研究mybatis事务管理)
8、数据库的不同隔离级别是如何对应低层操作的(数据库的不同隔离级别是如何操作数据库加锁的? 去研究数据库隔离级别与锁)
9、多服务器下如何实现事务性(去研究分布式事务)
一、事务的基本概念:
1、事务的概念(事物是用来做什么的)
事务是一系列的数据库的操作,是数据库应用程序的基本逻辑单元,用来保证对数据库的正确修改,保持数据的完整性。事务处理技术主要包括数据库恢复技术和并发控制技术
(解决了我之前的一个疑问:为什么看到事务时,有老师会问到数据库事务,难道事务分为数据库事务和其他类型事务,原来说的事务就是针对数据库而言)
2、事务的四个特性(ACID)
原子性(Atomicity):每个事物是一个不可分割的整体,只有所有的操作单元执行成功,整个事务才成功,否则此次事务就失败,所有执行成功的操作单元必须撤销,数据库回到此次事务之前的状态。
一致性(Consistency):在执行一次事务后,关系数据的完整性和业务逻辑的一致性不能被破坏。例如A与B转账结束后,他们的资金总额是不能改变的
隔离性(Isolation):在并发环境中,一个事物所做的修改必须与其他事务所做的修改相隔离、例如一个事物查看的数据必须是其他并发事务修改之前或修改完毕的数据,不能是修改中的数据
持久性(Durability):事务结束后,对数据的修改是永久保存的,即使因系统故障导致重启数据库系统,数据依然是修改后的状态。
3、事务的四种隔离级别
b、读未提交:TRANSACTION_READ_UNCOMMITTED(Read Uncommitted),可能会发生脏读、不可重复读、幻读
c、读已提交:TRANSACTION_READ_COMMITTED(Read Committed),可能会发生不可重复读、幻读
d、可重复读:TRANSACTION_REPEATABLE_READ(Repeatable Read),可能会发生幻读
e、可序列化:TRANSACTION_SERIALIZABLE(Serializable),效率不是很高
4、事务中会出现的问题 (不同隔离级别面对的问题)
a、脏读:读取的数据是别人没有提交过的。假使乙对数据A进行了更新,但是还没提交,这时甲读取了数据A,然后乙发生了回滚,数据A又变回原来的样子了,这时甲读取的数据A就是一个错误的数据,这就是脏读。
解决办法:对数据A这行进行加锁。如果要写,就加排它锁,如果要读,就加共享锁。
b、不可重复读:在一个事务中,对同一个数据进行两次读取,读取的内容不一样。假使甲对数据A进行第一次读取,读取到A的值为1,这时释放A的共享锁(保证不脏读),这时乙对数据A进行加一操作,释放排它锁,这时甲又对A进行读取,发现A的值变成2了,这就会发生不可重复读的现象。
解决办法: 在甲的事务中,等结束了甲的事务之后再释放A的共享锁。
c、幻读:在一张表中,甲先读取属性isUse值为false的项目,发现一共有20个,这时乙又在这张表中插入了一个isUse属性为false的项目。甲又查询了一下,发现变成21个了,感觉产生了幻觉,莫名其妙就多了一个。这就是幻读。
解决办法:使用范围锁,对表进行加锁。
二、@Transactional的情况
包括以下主要内容:
- Spring @Transactional的配置;
- Spring @Transactional的传播行为
- Spring @Transactional的隔离级别;
- Spring @Transactional的工作原理;
- Spring @Transactional的注意事项;
- Spring @Transactional自我调用中的问题。
事务注解方式: @Transactional
- 标注在类前:标示类中所有方法都进行事务处理
- 标注在接口、实现类的方法前:标示方法进行事务处理
1、 Spring @Transactional的配置
1.1 xml配置文件
xml配置文件中,添加事务管理器bean配置。
<!-- 事务管理器配置,单数据源事务 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="pkGouDataSource" />
</bean>
<!-- 使用annotation定义事务 开启事务-->
<tx:annotation-driven transaction-manager="txManager" />
解释:DataSourceTransactionManager:位于org.springframework.jdbc.datasource包中,提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理。
1.2 spring boot自动配置:
spring boot 会自动配置一个 DataSourceTransactionManager
,我们只需在方法(或者类)加上 @Transactional
注解,就自动纳入 Spring 的事务管理了。
解释:
Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added. For example, if HSQLDB
is on your classpath, and you have not manually configured any database connection beans, then Spring Boot auto-configures an in-memory database.
You should only ever add one @SpringBootApplication
or @EnableAutoConfiguration
annotation. We generally recommend that you add one or the other to your primary @Configuration
class only.
(我理解的是我的maven依赖中有什么,就会自动配置什么)(突然觉的我要去看下早已经下载好的spring boot书)
2、@Transactional的属性:
属性 | 类型 | 描述 |
---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum: Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int (in seconds granularity) | 事务超时时间设置 |
rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 |
noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |
3、Spring @Transactional的传播行为(propagation属性解释)
事务传播行为介绍 : 6种
propagation:传播 mandatory:强制的
@Transactional(propagation=Propagation.REQUIRED) | 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下) |
@Transactional(propagation=Propagation.NOT_SUPPORTED) | 容器不为这个方法开启事务 |
@Transactional(propagation=Propagation.REQUIRES_NEW) | 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务 |
@Transactional(propagation=Propagation.MANDATORY) | 必须在一个已有的事务中执行,否则抛出异常 |
@Transactional(propagation=Propagation.NEVER) | 必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反) |
@Transactional(propagation=Propagation.SUPPORTS) | 如果其他bean调用这个方法,在其他bean中声明事务,那就用事务。如果其他bean没有声明事务,那就不用事务 |
思考:如何理解没有事务就加入一个事务,事务是什么级别的呢?是谁判断有没有的?判断的依据是什么?
我的理解是spring本身判断有没有事务,判断的依据就是,开始加载项目的时候扫描的bean上有没有@Transactional, 但是TransactionTemplate是代码主动加事务
事务超时设置:
@Transactional(timeout=30) //默认是30秒
4、Spring @Transactional的隔离级别(同理于事务的隔离级别)(isolation属性解释)
@Transactional(isolation = Isolation.READ_UNCOMMITTED) | 读取未提交数据(会出现脏读, 不可重复读) 基本不使用 |
@Transactional(isolation = Isolation.READ_COMMITTED)(SQLSERVER默认) | 读取已提交数据(会出现不可重复读和幻读) |
@Transactional(isolation = Isolation.REPEATABLE_READ) | 可重复读(会出现幻读) |
@Transactional(isolation = Isolation.SERIALIZABLE) | 串行化 |
思考:
1)事务的隔离级别必须达到串行话,才能实现并发环境下的线程安全性。但是为什么一半都选择默认的呢?
原因:一是串行化肯定是效率更低的,二是需要加事务的操作逻辑一般是对多张数据表的修改操作,『可重复读』的隔离级别可以实现这种操作下的多线程的安全性。
2)那么『可重复读』的隔离级别是如何加锁的呢?
首先确认『可重复读』隔离级别可以解决『不可重复读』的问题。
不可重复读:在一个事务中,对同一个数据进行两次读取,读取的内容不一样。假使甲对数据A进行第一次读取,读取到A的值为1,这时释放A的共享锁(保证不脏读),这时乙对数据A进行加一操作,释放排它锁,这时甲又对A进行读取,发现A的值变成2了,这就会发生不可重复读的现象。
解决办法: 在甲的事务中,等结束了甲的事务之后再释放A的共享锁。(这是我理解以来的一个大坑)
解决办法:Mysql默认的存储引擎是InnoDB。MySql大多数『事务型存储引擎』的实现都不是简单的行级锁,基于提升并发性能的考虑,他们一般都同时实现了多版本并发控制(MVCC)。
真正解决方式:
对应代码中的逻辑就是:之前一个逻辑就是一个方法上加了事务注解,方法中有个对一条数据库记录的更新操作,然后彦江哥告诉我这就相当于对它加锁了,当时很不理解。
现在再理解就是:这个查询/更新操作会对这条记录加共享锁/排它锁,这个锁会直都事务结束才释放,所有其他逻辑对这条记录的操作要等释放锁后才能进行。
But,如果事务中的一个数据库操作是添加一条记录,另外一套逻辑是查询表的总记录数,这时候『可重复读』的隔离级别就不能再并发环境下保持正常了。
再理解原先彦江讲的这个逻辑:
由于MVCC的作用,这个查询/更新操作会对这条记录加共享锁/排它锁,而是通过版本控制的方式实现串行化的效果,所以多个线程可以同时操作数据,但是读取到的数据值可能不一样。
5、 Spring @Transactional的工作原理
三、事务管理中重要的API
Spring 框架中,涉及到事务管理的 API 大约有100个左右,其中最重要的有三个:TransactionDefinition、PlatformTransactionManager、TransactionStatus。我认为这是和事务执行直接相关的三个类。另外项目中还常遇到的和事务相关的类:TransactionTemplate、TransactionCallbackWithoutResult
1、接口TransactionDefinition
DefaultTransactionDefinition是其一个实现类
里面的属性如下:
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;
int PROPAGATION_NESTED = 6;
// Use the default isolation level of the underlying datastore.All other levels correspond to the JDBC isolation levels.
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE = 8;
// Use the default timeout of the underlying transaction system, or none if timeouts are not supported.(使用底层事务系统的默认超时,如果不支持超时,则使用“无”。)
int TIMEOUT_DEFAULT = -1;
原来之前一直看到的事务的隔离级别、传播机制、超时等都是在源码里写好的啊。
理解:TIMEOUT_DEFAULT = -1,使用低层事务系统的默认超时,那么用这个事务的时候都是对数据库的操作,那就是默认使用数据库的默认事务超时时间喽
ISOLATION_DEFAULT = -1; 默认的隔离级别是低层数据库的隔离级别,所有其他的隔离级别都是对应JDBC隔离级别的
2、接口PlatformTransactionManager
用于执行具体的事务操作,主要方法:
// Return a currently active transaction or create a new one, according to the specified propagation behavior.
// @return transaction status object representing the new or current transaction
1) TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
// Commit the given transaction, with regard to its status.
2) void commit(TransactionStatus status) throws TransactionException;
// Perform a rollback of the given transaction.
3) void rollback(TransactionStatus status) throws TransactionException;
据底层所使用的不同的持久化 API 或框架,PlatformTransactionManager 的主要实现类大致如下:
- DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况。
- HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
- JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。
- 另外还有JtaTransactionManager (数据访问技术:分布式事务)、JdoTransactionManager(JDO)、JmsTransactionManager等等。
(上面内容参考 : https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html, 目前我只关注DataSourceTransactionManager, 其他的内容先不关注)
3、接口TransactionStatus
PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象。返回的TransactionStatus 对象可能代表一个新的或已经存在的事务。
TransactionStatus 接口提供了一个简单的控制事务执行和查询事务状态的方法。方法包括:
1)
boolean isNewTransaction();
void setRollbackOnly();
// Return whether the transaction has been marked as rollback-only
boolean isRollbackOnly();
// 是否completed或者rolled back
boolean isCompleted();
其实现类:
DefaultTransactionStatus、MultiTransactionStatus、SimpleTransactionStatus
4、类TransactionTemplate :简化了编程式事务和异常的处理
class TransactionTemplate extends DefaultTransactionDefinition
implements TransactionOperations, InitializingBean{
// 依赖进了这个Manager
@Nullable
private PlatformTransactionManager transactionManager;
}
其中最重要的方法就是execute
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
// 开启事务
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
说明:在看了基于底层 API 的编程式事务管理的代码后,在看这段代码就非常容易理解了。
由上面的方法追踪创建事务的配置(即创建事务TransactionDefinition是怎么设置的):
TransactionStatus status = this.transactionManager.getTransaction(this);
原来 TransactionTemplate extends DefaultTransactionDefinition implements TransactionDefinition (TransactionTemplate好会玩~)
debug会发现这个对象(TransactionTemplate)的默认值:
解释:
PROPAGATION_REQUIRED = 0; // Support a current transaction; create a new one if none exists. (Propagation.REQUIRED级别)
隔离级别和超时时间都是默认使用底层数据库相应的隔离级别和超时设置
5、接口TransactionCallback
@FunctionalInterface
public interface TransactionCallback<T> {
@Nullable
T doInTransaction(TransactionStatus status);
}
TransactionCallbackWithoutResult:是TransactionCallback的一个实现,允许事务的操作没有返回结果。
public abstract class TransactionCallbackWithoutResult implements TransactionCallback<Object>{
@Override
@Nullable
public final Object doInTransaction(TransactionStatus status) {
doInTransactionWithoutResult(status);
return null;
}
protected abstract void doInTransactionWithoutResult(TransactionStatus status);
}
四、编程式事务管理和声明式事务管理
编程式事务管理包括:基于底层 API 的编程式事务管理、基于 TransactionTemplate 的编程式事务管理
1、基于底层 API 的编程式事务管理:
在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法。
代码示例(还没有实践过):
public class BankServiceImpl implements BankService {
private BankDao bankDao;
private TransactionDefinition txDefinition;
private PlatformTransactionManager txManager;
public boolean transfer(Long fromId, Long toId, double amount) {
// 开启一个事务
TransactionStatus txStatus = txManager.getTransaction(txDefinition);
boolean result = false;
try {
result = bankDao.transfer(fromId, toId, amount);
// 事务的提交
txManager.commit(txStatus);
} catch (Exception e) {
result = false;
// 事务的回滚
txManager.rollback(txStatus);
System.out.println("Transfer Error!");
}
return result;
}
}
基于底层API的事务管理示例配置文件:
(这个应该还是有问题)
<bean id="bankService" class="footmark.spring.core.tx.programmatic.origin.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
<property name="txManager" ref="transactionManager"/>
<property name="txDefinition">
<bean class="org.springframework.transaction.support.DefaultTransactionDefinition">
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
</bean>
</property>
</bean>
解释:
在类中增加了两个属性:一个是 TransactionDefinition 类型的属性,它用于定义一个事务;另一个是 PlatformTransactionManager 类型的属性,用于执行事务管理操作。
如果方法需要实施事务管理,我们首先需要在方法开始执行前启动一个事务,调用PlatformTransactionManager.getTransaction(...) 方法便可启动一个事务。创建并启动了事务之后,便可以开始编写业务逻辑代码,然后在适当的地方执行事务的提交或者回滚。
缺点:
事务管理的代码散落在业务逻辑代码中,破坏了原有代码的条理性,并且每一个业务方法都包含了类似的启动事务、提交/回滚事务的样板代码。幸好,Spring 也意识到了这些,并提供了简化的方法,这就是 Spring 在数据访问层非常常见的模板回调模式。看下面基于 TransactionTemplate 的编程式事务管理。
2、基于 TransactionTemplate 的编程式事务管理
@Autowired
private TransactionTemplate transactionTemplate;
public void updateAfterFail(String instructionId, String errorMsg) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
// 1. 更新docDiff
DocDiffWithBLOBs docDiff = new DocDiffWithBLOBs();
docDiff.setStatus(InstructionStatusEnum.FAIL.getCode());
DocDiffExt ext = new DocDiffExt();
ext.setInstructionId(instructionId);
ext.setOldStatus(InstructionStatusEnum.INIT.getCode());
docDiffDao.updateByExt(docDiff, ext);
// 2. 更新processInstruction
InstructionDO instructionDO = new InstructionDO();
instructionDO.setStatus(InstructionStatusEnum.FAIL.getCode());
instructionDO.setErrMsg(errorMsg);
InstructionDOExt instructionDOExt = new InstructionDOExt();
instructionDOExt.setInstructionId(instructionId);
instructionDOExt.setOldStatus(InstructionStatusEnum.INIT.getCode());
processInstructionDAO.updateByExt(instructionDO, instructionDOExt);
}
});
}
通常我们都是使用的不需要返回结果的事务,代码写的方式如下:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {。。。。。}
下面方法的返回类型是void
但是如果我们需要返回结果怎么办呢?这是项目中的一段代码:
public NextDocInfo skipDoc(SkipRequest skipInfo) {
........
........
return transactionTemplate.execute(new TransactionCallback<NextDocInfo>() {
@Override
public NextDocInfo doInTransaction(TransactionStatus status) {
// 更新文档状态
DocLabelInfo updateDoc = new DocLabelInfo();
updateDoc.setStatus(DocLabelStatusEnum.COMPLETE.getCode());
updateDoc.setIsValid(false);
updateDoc.setLabel(StringUtils.EMPTY);
updateDoc.setDocId(docInfo.getDocId());
int i = docLabelInfoDao.updateRecordByDocId(updateDoc);
if (i < 1) {
throw new DoclabelException(DoclabelErrorCode.CONCURRENT_ERROR);
}
// 清空句子的标注
SentenceLabelInfoWithBLOBs updateSen = new SentenceLabelInfoWithBLOBs();
updateSen.setDocId(docInfo.getDocId());
updateSen.setLabel(StringUtils.EMPTY);
i = sentenceLabelInfoDao.updateLabelByDocId(updateSen);
if (i < 1) {
throw new DoclabelException(DoclabelErrorCode.CONCURRENT_ERROR);
}
// 如果没有待标注的文档就更新子任务状态
NextDocInfo nextDocInfo = getNextDoc(docInfo.getSubTaskId());
if (!nextDocInfo.getHaveNext() && subTask.getStatus().equals(SubTaskStatusEnum.INIT.getCode())) {
SubTaskInfo update = new SubTaskInfo();
update.setStatus(SubTaskStatusEnum.SUBMITTED.getCode());
update.setSubTaskId(docInfo.getSubTaskId());
i = subTaskInfoDao.updateByIdAndStatus(update, subTask.getStatus());
if (i < 1) {
throw new DoclabelException(DoclabelErrorCode.CONCURRENT_ERROR);
}
}
return nextDocInfo;
}
});
}
注意,这个地方用的是
transactionTemplate.execute(new TransactionCallback<NextDocInfo>(), 下面的方法的返回类型是NextDocInfo
3、声名式事务
Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。因为事务管理本身就是一个典型的横切逻辑,正是 AOP 的用武之地。
和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
3.1 基于 TransactionInterceptor 的声明式事务管理
配置了一个 TransactionInterceptor 来定义相关的事务规则,他有两个主要的属性:一个是 transactionManager,用来指定一个事务管理器,并将具体事务相关的操作委托给它;另一个是 Properties 类型的 transactionAttributes 属性,它主要用来定义事务规则,该属性的每一个键值对中,键指定的是方法名,方法名可以使用通配符,而值就表示相应方法的所应用的事务属性。
明显的缺点就是:配置文件太多。我们必须针对每一个目标对象配置一个 ProxyFactoryBean。另外,虽然可以通过父子 Bean 的方式来复用 TransactionInterceptor 的配置,但是实际的复用几率也不高;这样,加上目标对象本身,每一个业务类可能需要对应三个 <bean/> 配置,随着业务类的增多,配置文件将会变得越来越庞大,管理配置文件又成了问题。
<beans...>
......
<bean id="transactionInterceptor"
class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="transfer">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean id="bankServiceTarget"
class="footmark.spring.core.tx.declare.origin.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
</bean>
<bean id="bankService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="bankServiceTarget"/>
<property name="interceptorNames">
<list>
<idref bean="transactionInterceptor"/>
</list>
</property>
</bean>
......
</beans>
3.2 基于 TransactionProxyFactoryBean的声明式事务管理
用于将TransactionInterceptor 和 ProxyFactoryBean 的配置合二为一。我们把这种配置方式称为 Spring 经典的声明式事务管理。
但是看下面的配置,明显的缺点就是需要为每一个业务类配置TransactionProxyFactoryBean
<beans......>
......
<bean id="bankServiceTarget"
class="footmark.spring.core.tx.declare.classic.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
</bean>
<bean id="bankService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="target" ref="bankServiceTarget"/>
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="transfer">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
......
</beans>
3.3 基于 <tx> 命名空间的声明式事务管理
在此基础上,Spring 2.x 引入了 <tx> 命名空间,结合使用 <aop> 命名空间,带给开发人员配置声明式事务的全新体验,配置变得更加简单和灵活。另外,得益于 <aop> 命名空间的切点表达式支持,声明式事务也变得更加强大。
由于使用了切点表达式,我们就不需要针对每一个业务类创建一个代理对象了。
<beans......>
......
<bean id="bankService"
class="footmark.spring.core.tx.declare.namespace.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
</bean>
<tx:advice id="bankAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="bankPointcut" expression="execution(* *.transfer(..))"/>
<aop:advisor advice-ref="bankAdvice" pointcut-ref="bankPointcut"/>
</aop:config>
......
</beans>
3.4 基于 @Transactional 的声明式事务管理
除了基于命名空间的事务配置方式,Spring 2.x 还引入了基于 Annotation 的方式,具体主要涉及@Transactional 标注。@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
@Transactional(propagation = Propagation.REQUIRED)
public boolean transfer(Long fromId, Long toId, double amount) {
return bankDao.transfer(fromId, toId, amount);
}
Spring 使用 BeanPostProcessor 来处理 Bean 中的标注,因此我们需要在配置文件中作如下声明来激活该后处理 Bean,如清单13所示:
<
tx:annotation-driven
transaction-manager
=
"transactionManager"
/>
虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 小组建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
疑问1:什么叫基于接口的代理?还有什么代理方式?
疑问2:AOP的本质在这指的是什么?
(参考这个链接:https://juejin.im/post/5b90e648f265da0aea695672, 专门整理下)
如果不是对遗留代码进行维护,则不建议再使用基于 TransactionInterceptor 以及基于TransactionProxyFactoryBean 的声明式事务管理方式,但是,学习这两种方式非常有利于对底层实现的理解。
虽然列举了四种声明式事务管理方式,但是这样的划分只是为了便于理解,其实后台的实现方式是一样的,只是用户使用的方式不同而已。
五、代码验证:
5.1 事务起作用的范围
5.1.1 同一个类中的方法测试
说明:下面测试满足如下几点
- 下面的方法抛出异常后都没有被捕获,
- 抛出的异常是在代码逻辑中的db操作完成后再抛出异常,
- A和B在同一个类中
- A方法是public(不是public,junit也测试不到), junit测试直接调用A方法
1、service中有A和B方法,A调用B,A加了@Transactional, B不加, B中抛出了异常,
结果:A、B方法的数据库操作(变更操作)都回滚
解释:@Transactional对A方法的所有逻辑都起到了作用
2、service中有A和B方法,A调用B, A不加,B加了@Transactional,A方法抛出异常
结果:两种情况
A方法中调用B之前抛出异常,A的db操作不回滚, B方法也不会被执行
A方法中调用B之后抛出异常,A、B中的db操作都不回滚
解释:B上加的@Transactional不起作用
3、service中有A和B方法,A调用B,A加了@Transactional, B方法内部使用了TransactionTemplate, 并且TransactionTemplate内部的方法抛出异常
结果:A、B所有的db操作都回滚
解释:意料之中,即使B中没有TransactionTemplate都会全部回滚,其实我想知道的是,B中的这个事务和A中的这个事务有什么关系?是一个事务呢还是另启一个事务呢?看下面的5.2.1节
4、service中有A和B方法,A调用B,A不加@Transactional, B方法内部使用了TransactionTemplate, 并且TransactionTemplate内部的方法抛出异常
结果:只有B中TransactionTemplate内部分db操作才会滚,其余的db操作都不回滚
解释:说明了TransactionTemplate和@Transactional启动事务的方式不一样,TransactionTemplate是主动加个事务,而@Transactional是需要借助AOP,扫描到该注解后通过动态代理的方式来加事务的。
额外说明:解释了我的一个疑问,@Transactional不能加在间接调用的方法上,TransactionTemplate可以嘛?答案是可以的
5.1.2 两个类中的方法测试
说明:下面测试满足如下几点
- 下面的方法抛出异常后都没有被捕获,
- 抛出的异常是在代码逻辑中的db操作完成后再抛出异常,
- A和B不在同一个类中
- A方法是public(不是public,junit也测试不到), junit测试直接调用A方法
- A、B方法都是接口方法,都是public
1、service1和service2中分别有A和B方法,A调用B,A加了@Transactional, B不加, B中抛出了异常
结果:A和B中的db操作全部回滚
解释:B方法加入到了A的事务中,及@Transactional对A方法的所有逻辑都起到了作用
2、service1和service2中分别有A和B方法,A调用B,A不加,B加了@Transactional, B中抛出了异常
结果:A中调用B方法之前的db操作不回滚, B中的db全部回滚
解释:B中的事务起作用了!
But, 如果B方法不是public, 可是不设置为public, 就很可能在A的类中调用不到啊,作用不考虑(额外说明,加@Trasactional的方法必须是public,至于为什么,后期可以写个wiki专门讲这个)
5.2 事务的传播机制
5.2.1 同一个类中的两个方法测试
主要为了解决上面3中我的疑问
无论是使用@Transactional还是TransactionTemplate,一般都是使用它的默认的传播行为和隔离级别
默认传播行为是Propagation.REQUIRED, 表示如果有事务则加入,没有,则新建一个。
检验方式:使用下面语句打印事务名字
String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();
说明:我的A方法是com.baidu.doclabel.biz.service.impl.ProcessJobServiceImpl.deleteJob(String jobId),
B方法是com.baidu.doclabel.biz.service.impl.ProcessJobServiceImpl.deleteInstruction(InstructionDO ido)
1、service中有A和B方法,A调用B,A加了@Transactional, B方法内部使用了TransactionTemplate, 都是默认的隔离级别
结果:两个事务的名字一致,我打印的结果都是
com.baidu.doclabel.biz.service.impl.ProcessJobServiceImpl.deleteJob
解释:B中的事务加入了A事务中
额外说明:终于解决了我一直以来的疑问
2、service中有A和B方法,A调用B,A加了@Transactional, B方法内部使用了TransactionTemplate, 并且把B事务的传播行为设置为Propagation.REQUIRES_NEW(无论是否存在事务,都启动一个信息事务,原来的事务挂起,等当前事务结束后,在继续执行原来的事务)
设置方式:
修改这里的传播行为,设置为:
TransactionDefinition.PROPAGATION_REQUIRES_NEW
结果:
A方法中的事务名字为com.baidu.doclabel.biz.service.impl.ProcessJobServiceImpl.deleteJob
B方法中的事务名字为null
为什么为空呢?我把transactionTemplate的bean手动添加了name
@Bean(value = "transactionTemplate")
public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate();
transactionTemplate.setTransactionManager(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.setName("transactionTemplate new name");
return transactionTemplate;
}
果然,再看,B中事务的名字为 transactionTemplate new name
反推1中的测试,如果这里传播行为是默认的,name重新设置,发现B中的事务还是和A中的一致, 证明了1中的结论
5.2.2 不同类中的方法的测试
理论上来说应该是满足2.3节传播行为的说明的。
测试方式是:service1和service2中分别有A和B方法,A调用B,A和B都加@Transactional,设置B方法的传播行为,看两个事务的名字
1、A和B方法都是默认的传播行为
结果:名字都是com.baidu.doclabel.biz.service.impl.ProcessJobServiceImpl.deleteJob
2、B事务的传播行为设置为Propagation.REQUIRES_NEW, 设置方式 : @Transactional(propagation = Propagation.REQUIRES_NEW)
结果:
A方法中的事务名字为com.baidu.doclabel.biz.service.impl.ProcessJobServiceImpl.deleteJob
B方法中的事务名字为com.baidu.doclabel.biz.service.impl.ProcessTestServiceImpl.deleteInstruction
解释:果然书上说的没有骗我!
5.3 方法加事务的时间点 比对 方法中db操作对数据库加锁的时间点
(在理解MVCC的基础上再测试)
5.3.1 可重复读级别 测试方法:
@Test
public void testMultiThreads(){
CountDownLatch latch = new CountDownLatch(2);
String jobId = "20200303000000399594";
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
processJobService.deleteJob(jobId);
latch.countDown();
}
});
thread1.start();
// 目的是让第二个线程晚一丢丢执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
ProcessJobDO processJobDO = new ProcessJobDO();
processJobDO.setJobId(jobId);
processJobDO.setJobName("testname2");
processJobDAO.updateByJobId(processJobDO);
latch.countDown();
}
});
thread2.start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
第一个线程中执行方法 processJobService.deleteJob(jobId)
1、第一个线程先等待一段时间再去进行db操作:
结果:第二个先执行成功,第一个再执行,而且第一个打印出来的jobName是第二个更新后的结果
解释:第一个事务开启了事务后,不是立马锁数据库的,或者不是立马锁数据库记录的。 Mysql查找数据时,InnoDB只查找版本(DB_TRX_ID)早于当前事务版本的数据行。这个事务指的应该是数据库操作的事务,而不是@Transactional关键字标志的方法 代表的事务开始的时间。
@Transactional(rollbackFor = DoclabelException.class)
public void deleteJob(String jobId) {
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ProcessJobDO processJobDO = processJobDAO.queryByJobId(jobId);
if (processJobDO == null) {
return;
}
System.out.println("================first query:" + processJobDO.getJobName() + "=======================");
........
........
}
2、第一个线程立即执行查询的db操作,下面的睡眠时间是为了保证事务不结束
结果:第二个线程在第一个线程表示的事务之前执行成功,并成功更新了这条数据,并且是在第一个线程第一个次打印jobName之后执行,第一个线程两次打印的jobName都是第二个线程更新之前的结果。
解释:完美的诠释了版本控制(MVCC), MVCC是通过保存数据在某一时间点的快照来实现的。即,不管是否需要执行多长时间,同一个事务内看到的数据都是一致的。
@Transactional(rollbackFor = DoclabelException.class)
public void deleteJob(String jobId) {
// 设置 job
ProcessJobDO processJobDO = processJobDAO.queryByJobId(jobId);
if (processJobDO == null) {
return;
}
System.out.println("================first query:" + processJobDO.getJobName() + "=======================");
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
processJobDO.setEnabled(EnableEnum.DISABLED.getStatus());
processJobDAO.updateByJobId(processJobDO);
ProcessJobDO processJobDO001 = processJobDAO.queryByJobId(jobId);
System.out.println("================second query:" + processJobDO001.getJobName() + "=======================");
。。。。。
}
5.3.2 序列化级别测试
测试方法:两个线程,第一个线程中有一个事务,两次查询表的全量,第二个线程增加表的一条记录
结果:
其中查询语句用的是count(*)
第二个线程的结果在第一个事务的两句打印之间之前执行完成,第一线程的第一次答应结果为771, 第二次也为771, 但是最终两个线程执行完成后,总的记录数位772(amazing !)
@Test
public void testMultiThreads() {
CountDownLatch latch = new CountDownLatch(2);
String jobId = "20200303000000399594";
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Integer num = processJobDAO.countALL();
System.out.println("================fist countAll=" + num + " ================");
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer num2 = processJobDAO.countALL();
System.out.println("================second countAll=" + num2 + " ================");
}
});
} finally {
latch.countDown();
}
}
});
thread1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
ProcessJobDO processJobDO = new ProcessJobDO();
processJobDO.setJobId("testtesttesttest");
processJobDO.setJobName("testJobName");
processJobDO.setJobType("DIFF");
processJobDO.setBizDefine("docVerify");
processJobDO.setStatus("COMPLETE");
processJobDO.setFilePath("");
processJobDO.setExtInfo("");
processJobDO.setCreateUser("zhangsan");
processJobDO.setGroupId(0L);
processJobDO.setCreateTime(new Date());
processJobDO.setModifyTime(new Date());
processJobDO.setEnabled("1");
// processJobDAO.updateByJobId(processJobDO);
processJobDAO.createAndSaveProcessJob(processJobDO);
System.out.println("================i_change_jobname success ================");
latch.countDown();
}
});
thread2.start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
5.3.3 总结
虽然事务的隔离级别是可重复读的级别,但是测试结果表明完全实现了串行化的需求,这都是MVCC的功劳。