什么是Spring Data Access?
Spring Framework约包含20个模块,这些模块可以分成7个组:Core Container、Data Access/Integration、Web、AOP (Aspect Oriented Programming)、Instrumentation、Messaging和Test。
Maven依赖关系
Spring Core Container: beans\context\core,Spring的核心基础模块,提供IOC、DI、资源加载、事件支持等等。
spring-aop(Aspect-Oriented Programming): 分离横切关注点,将核心业务逻辑与其他公共逻辑解耦。
spring-tx:基于spring-aop将事务管理与业务代码分离。
JDBC(Java Database Connectivity): 包含在JDK中,java.sql.*、javax.sql.*,(Driver、Connection、SQLException、DataSource),是java访问数据库的一套标准API。包含SPI(Service Provider Interface)的理念。
mysql-connector-java: 是实现jdbc接口的mysql驱动。
druid(德鲁伊):数据库连接池
spring-jdbc:简化jdbc操作,处理事务,处理库异常转换等。
mybatis: ORM框架,处理Java POJO和数据库记录之间的映射。
mybatis-spring: 是MyBatis的子项目,用于将MyBatis整合到spring中。
spring-tx
支持Global transactions、Local transactions;支持Declarative transaction management、Programmatic transaction management
声明式事务管理是常用方式,他对应用代码的侵入性最新最小(仅包括配置和注解),与Spring无侵入的设计理念保持一致。
使用(声明式事务管理)
注解方式和xml方式
<tx:annotation-driven transaction-manager="datasourceTransactionManager"/>
<tx:advice id="txAdvice" transaction-manager="datasourceTransactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Throwable" timeout="20"/>
<tx:method name="*" read-only="true" rollback-for="java.lang.Throwable" timeout="20"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="servicePointcut"
expression="execution(public * com.service.*.*(..)) "/>
<aop:advisor pointcut-ref="servicePointcut" advice-ref="txAdvice" order="1"/>
</aop:config>spring-tx包的META-INF里的配置文件spring.handlers配置了Spring-tx的NameSpaceHandler,以支持tx的自定义标签(tx:annotation-driven、tx:advice等)的解析。TxNamespaceHandler的关键步骤是注册了两个BeanDefinitionParser。
xml的事务配置方式注册了TxAdviceBeanDefinitionParser,会解析成TransactionInterceptor,依赖于AOP配置成切面的通知。
注解方式则注册了AnnotationDrivenBeanDefinitionParser,注册BeanFactoryTransactionAttributeSourceAdvisor。最终会在postProcessAfterInitialization中判断Bean的任意方法是否有Transactional注解来决定创建包含TransactionInterceptor的代理。
TransactionInterceptor源码
拦截器的主要内部流程:
public class TransactionInterceptor implements MethodInterceptor, Serializable {
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// 获取方法的事务属性(propagationBehavior、timeout、readonly等)
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
// 通过事务属性获取对应的PlatformTransactionManager(支持多个事务管理器、多个数据源)
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 获取方法名(类名+.+方法名), 仅用于打debug/trace级别的日志
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 方法无事务属性,或者不是回调型事务管理器(WebSphere, IBM的软件平台,主要用于银行、电信行业)
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 如果需要事务,则从数据源getConnection,connection.setAutoCommit(false)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 执行实际的方法(sql)
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 判断是否回滚,需要回滚则调用rollback
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 恢复上一个ThreadLocal的事务信息
cleanupTransactionInfo(txInfo);
}
// 正常提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
...
}
}创建事务(createTransactionIfNecessary)
创建事务是由PlatformTransactionManager.getTransaction实现。
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}Spring文档称PlatformTransactionManager是一个SPI(Service Provider Interface)。spring-tx模块提供这个最简单的事务管理器接口,并提供一个抽象类AbstractPlatformTransactionManager实现了这个接口。抽象类主要包含一些处理事务的公共逻辑,如处理事务传播等。具体的实现类,如spring-jdbc的DataSourceTransactionMananger则负责与数据库API的具体交互(例如开启事务的方法封装了getConnection、setAutoCommit(fasle)、setTransactionIsolation)。
createTransactionIfNecessary的流程:
Transaction propagation
事务传播的判断逻辑
为什么要挂起事务保存上一个事务信息?
connection信息和TransactionSynchronization均会保存在ThreadLocal里。connection放在ThreadLocal里是为了方便Mybatis获取当前事务连接,执行sql;TransactionSynchronization放在ThreadLocal里是为了方便在事务的不同生命周期进行回调。进入一个事务切面后,会记录上一个切面里的事务信息,清除并设置当前事务的信息,当前事务切面完成后,恢复上一个事务的信息。
REQUIRED_NEW事务传播举栗:
Transaction Propagation源码
(删除了log代码)
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
// 从TheadLocal获取DataSourceTransactionObject(主要封装的是Connection对象)
Object transaction = doGetTransaction();
if (definition == null) {
definition = new DefaultTransactionDefinition();
}
//transaction对象里有Connection,表示存在事务
if (isExistingTransaction(transaction)) {
return handleExistingTransaction(definition, transaction, debugEnabled);
}
if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
}
// 傲娇的PROPAGATION_MANDATORY,没有事务就抛异常
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
// 需要开启事务
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
//实际开始事务
doBegin(transaction, definition);
//设置readOnly、isolation等ThreadLocal信息
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
// 不需要事务,包装一个null对象返回
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
}关于@Transactional注解
public @interface Transactional {
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 {};
}- propagation是每次进入切面时均会判断,用于判断是否需要创建新事务等;
- isolation/timeout/readOnly只有实际去创建新的事务时才会使用,才会去设置。或者说在同一个事务里,进入子函数时,这个属性是继承的;
- rollbackFor不是继承的,每个函数自己的rollBack均会生效(在都是PROPAGATION_REQUIRED情况下,事务中某一步异常回滚,整个事务都会回滚;如果子事务的rollBack和父事务的rockBack冲突,则仍然会回滚,但spring会抛出UnexpectedRollbackException);
以timeout为例,如果说所有地方全是PROPAGATION_REQUIRED,那timeout会以第一个函数的事务定义为准,整个事务的timeout是第一个切面里设定的timeout。
其他
注解方式和XML方式混用
混用会创建两个切面。两个切面的执行顺序依赖于order属性,order越小,越先执行。
混用的方式与不同类间互相调用的效果是完全一样的,因为他们都是创建了多个事务切面。Transactional的各个属性的含义与之前讨论的完全一致。所以,在混用的情况下,实际生效的propagation是order最大的切面;而timeout等属性则是order最小的切面决定的。
切面的排序算法:TimSort、Topological sorting(aspectj方式)。如果order相同则排序结果是不确定的(与多个因素有关:声明顺序、是否实现了特殊的BeanPostProcessor等)。
spring aop的默认order为Integer.MAX_VALUE,即默认为最后执行。
举个栗子:在已经有XML配置的情况下,如果想用注解的方式让某个函数没有事务,则必须要配置注解方式的order比xml方式的order大。
@TransactionalEventListener
如何让某个函数在事务结束之后在执行?
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
someFunction(event);
}
});利用Spring提供的API注册一个afterCommit的TransactionSynchronization,则会在事务结束的时候进行回调.
但这种方式直接用了Spring的API,与Spring无侵入的设计是不符的。在Spring 4.2之后,提供了一个新的注解@TransactionEventListener。
TransactionEventListener原理(接收到事件的时候创建一个事务回调,并注册回调).
@Async如何结合@TransactionEventListener?
@Async
@TransactionalEventListener(fallbackExecution = true, phase = TransactionPhase.AFTER_COMPLETION)
public void handleCustomerEsUpdate(Event event) {
...
}发出事件 -> spring找到对应的Lisenter(Listener里封装了实际处理函数的class和method),通过反射调用实际处理函数,注册事务回调 -> 事务回调时,被Async的切面拦截,异步执行。
@Transactional与@Async
Async强制覆盖了aop的order为Integer.MIN_VALUE(spring认为Async应该是aop链中的第一个advisor,参考https://jira.spring.io/browse/SPR-7147),而且order不可配置;Transactional没有覆盖,仍然为默认的Integer.MAX_VALUE,order可配置。所以异步切面会先于事务切面执行。
(如果是相反的情况,事务切面先于Async切面执行,由于spring事务管理重度依赖ThreadLocal,所以异步线程里面感知不到事务,导致Async注解和Transactional注解同时使用时,Transactional注解会失效。具体原因是:在Spring开启事务之后,设置Connection到当前线程,然后立马开启一个新线程,mybatis执行实际的SQL代码时,通过ThreadLocal获取不到Connection,开启新的Connection,也不会设置autoCommit,那么这个函数整体将没有事务。)
举个栗子:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
Object object = new Object();
object.setKey(Integer.MAX_VALUE);
objectMapper.insert(object);
otherService.methodB();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Async
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
Object object = objectMapper.getByKey(Integer.MAX_VALUE);
System.out.println(object);
}函数A里开启事务,插入对象 -> 函数B异步化(新线程) -> 函数B开启新事务,此时A事务没结束,B无法看到A的提交,所以B里面读到的对象将是null。
Spring-jdbc
封装简化JDBC的操作,核心类包括JdbcTemplate、DataSourceTransactionManager、DataSourceUtils、SQLExceptionTranslator等等。
由于我们用的是ORM(mybatis),与spring-jdbc关系不大,仅仅是过程中某些地方框架里会用到DataSourceTransactionManager、DataSourceUtils以及SQLExceptionTranslator(通过特定数据库厂商的特定ErrorCode和通用的SQLState将SQLEception转换为DataAccessException)。
Mybatis-spring
mybatis:持久层框架,处理接口、POJO和数据库记录之间的映射。
mybatis-spring:用于将mybatis整合到spring中。如何整合呢?
首先我们在项目中写了很多Mybatis的*Mapper.java, 但并没有加@Component等注解,首先看Mapper是如何注册成为Spring Bean。
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="com.**.mapper"/>
</bean>MapperScannerConfigurer是一个BeanDefinitionRegistryPostProcessor,扫描mapper包,将Mapper接口注册成为MapperFactoryBean(FactoryBean),调用mybatis的代码为Mapper创建了代理。
Mapper代理里会获取数据库连接,执行SQL,处理返回结果。
获取连接的实现类是mybatis-spring的SpringManagedTransaction,里面核心只有一句话DataSourceUtils.getConnection(this.datasource)。DataSourceUtils是spring-jdbc的工具类,此工具类会优先获取spring transaction的连接,没有获取到表示没有事务,则直接调用datasource.getConnection。
public class SpringManagedTransaction implements Transaction {
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();//获取新的connection对象
}
return this.connection;//在同一个事务里,始终是一个connection对象
}
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
..
}所以总结下来就是,mybatis-spring将Mapper注册成为spring bean,创建mybatis的MapperProxy代理Mapper,并对mybatis进行了配置(如配置SpringManagedTransaction用于mybatis获取数据库连接)。而mybatis层则是获取连接,处理接口和SQL的映射关系,执行SQL,处理返回结果。另外如果过程中出现异常,最终是利用的spring-jdbc的异常翻译器将SQLException转换为spring的DataAccessException。
transactionTimeout的实现
背景
transactional的这个几个属性isolation/timeout/propagation中只有timeout是依赖于mybatis实现的,其他属性spring-tx自己均有处理。
而mybatis在2016年3月(mybatis 3.4.0)才加上对事务timeout的支持。commit见https://github.com/mybatis/mybatis-3/commit/2e12fdda01ca88a88487543cfaaed818bbc416aa。
<tx:advice id="myBatisTxAdvice" transaction-manager="myBatisTransactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" timeout="25"/>如果项目的mybatis版本小于3.4.0,上述的timeout配置是没用的。
如果做mybatis/spring升级,这个timeout设置会生效,某些大事务会触发TransactionTimeoutException,可以提前做下大事务拆分或增大部分方法的timeout。
原理
事务超时的底层实现是依赖于PrepareStatement超时,每执行一次SQL,获取PrepareStatement都会检查事务的剩余时间(如果此时剩余时间<0,抛出TransactionTimeoutException),并将剩余时间设置到PrepareStatement里,依赖数据库驱动实现PrepareStatement的超时(新开线程检查PrepareStatement是否超时,超时则创建新的Connection发送取消查询的命令, 参考https://www.jianshu.com/p/2deaf51bf715)。
当前PrepareState的超时 = 当前事务的剩余时间。事务超时 = 业务代码时间 + 多个PrepareStatement的超时时间。
具体原理是,spring-tx开启事务时,将timeout设置到ThreadLocal中,txObject.getConnectionHolder().setTimeoutInSeconds(timeout); TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder())。而mybatis从Connection获取PreparedStatement,并将timeout应用到PrepareStatement上。
举个栗子, sleep(26s) + SQL,这种会超时,但是反过来的栗子,SQL + sleep(26s),SQL执行时事务还有很多剩余时间,不会超时。
public void timeOut() {
try {
Thread.sleep(26000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object object = objectMapper.getByKey(Integer.MAX_VALUE);
}
public void notTimeOut() {
Object object = objectMapper.getByKey(Integer.MAX_VALUE);
try {
Thread.sleep(26000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}参考
https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/html/index.html
备注:本文基于spring 4.3.14, mybatis 3.4.1