业务系统的数据,一般最后都会落入到数据库中,例如 MySQL
、Oracle
等主流数据库,不可避免的,在数据更新时,有可能会遇到错误,这时需要将之前的数据更新操作撤回,避免错误数据。
Spring
的声明式事务能帮我们处理回滚操作,让我们不需要去关注数据库底层的事务操作,可以不用在出现异常情况下,在 try / catch / finaly 中手写回滚操作。
Spring
的事务保证程度比行业中其它技术(例如 TCC
/ 2PC
/ 3PC
等)稍弱一些,但使用 Spring
事务已经满足大部分场景,所以它的使用和配置规则也是值得学习的。
接下来一起学习 Spring
事务是如何使用以及实现原理吧。
使用例子
1.创建数据库表
create table test.user( id int auto_increment primary key, name varchar(20) null, age int(3) null) engine=InnoDB charset=utf8;
2.创建对应数据库表的 PO
public class JdbcUser { private Integer id; private String name; private Integer age; ...(使用 ctrl + N 进行代码补全 setter 和 getter) }
3.创建表与实体间的映射
在使用 JdbcTemplate
时很纠结,在 Java
类中写了很多硬编码 SQL
,与 MyBatis
使用方法不一样,为了示例简单,使用了 JdbcTemplate
,不过还是建议朋友们用 MyBatis
,让代码风格整洁。
public class UserRowMapper implements RowMapper { @Override public Object mapRow(ResultSet rs, int rowNum) throws SQLException { JdbcUser user = new JdbcUser(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setAge(rs.getInt("age")); return user; } }
4.创建数据操作接口
public interface UserDao { /** * 插入 * @param user 用户信息 */ void insertUser(JdbcUser user); /** * 根据 id 进行删除 * @param id 主键 */ void deleteById(Integer id); /** * 查询 * @return 全部 */ List<JdbcUser> selectAll(); }
5.创建数据操作接口实现类
业务系统的数据,一般最后都会落入到数据库中,例如 MySQL
、Oracle
等主流数据库,不可避免的,在数据更新时,有可能会遇到错误,这时需要将之前的数据更新操作撤回,避免错误数据。
Spring
的声明式事务能帮我们处理回滚操作,让我们不需要去关注数据库底层的事务操作,可以不用在出现异常情况下,在 try / catch / finaly 中手写回滚操作。
Spring
的事务保证程度比行业中其它技术(例如 TCC
/ 2PC
/ 3PC
等)稍弱一些,但使用 Spring
事务已经满足大部分场景,所以它的使用和配置规则也是值得学习的。
接下来一起学习 Spring
事务是如何使用以及实现原理吧。
使用例子
1.创建数据库表
create table test.user( id int auto_increment primary key, name varchar(20) null, age int(3) null) engine=InnoDB charset=utf8;
2.创建对应数据库表的 PO
public class JdbcUser { private Integer id; private String name; private Integer age; ...(使用 ctrl + N 进行代码补全 setter 和 getter) }
3.创建表与实体间的映射
在使用 JdbcTemplate
时很纠结,在 Java
类中写了很多硬编码 SQL
,与 MyBatis
使用方法不一样,为了示例简单,使用了 JdbcTemplate
,不过还是建议朋友们用 MyBatis
,让代码风格整洁。
public class UserRowMapper implements RowMapper { @Override public Object mapRow(ResultSet rs, int rowNum) throws SQLException { JdbcUser user = new JdbcUser(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setAge(rs.getInt("age")); return user; } }
4.创建数据操作接口
public interface UserDao { /** * 插入 * @param user 用户信息 */ void insertUser(JdbcUser user); /** * 根据 id 进行删除 * @param id 主键 */ void deleteById(Integer id); /** * 查询 * @return 全部 */ List<JdbcUser> selectAll(); }
5.创建数据操作接口实现类
具体位置在:org.springframework.transaction.annotation.Transactional
属性 | 类型 | 作用 |
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | 枚举:Propagation | 可选的事务传播行为 |
isolation | 枚举:Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 设置读写或只读事务,默认是只读 |
rollbackFor | Class 数组,必须继承自 Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名称数组,必须继承自 Throwable | |
noRollbackFor | Class 数组,必须继承自 Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名称数组,必须继承自 Throwable |
事务的传播性 Propagation
- REQUIRED
这是默认的传播属性,如果外部调用方有事务,将会加入到事务,没有的话新建一个。
- PROPAGATION_SUPPORTS
如果当前存在事务,则加入到该事务;如果当前没有事务,则以非事务的方式继续运行。
- PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
事务的隔离性 Isolation
- READ_UNCOMMITTED
最低级别,只能保证不读取
物理上损害的数据,允许脏读
- READ_COMMITTED
只能读到已经提交的数据
- REPEATABLE_READ
可重复读
- SERIALIZABLE
串行化读,读写相互阻塞
这里只是简单描述了一下这两个主要属性,因为底层与数据库相关,可以看下我之前整理过的 MySQL锁机制
Spring 中实现逻辑
介绍完如何使用还有关键属性设定,本着知其然,知其所以然的学习精神,来了解代码是如何实现的吧。
解析
之前在解析自定义标签时提到,AOP
和 TX
都使用了自定义标签,按照我们上一篇 AOP
的学习,再来一遍解析自定义标签的套路:事务自定义标签。
定位到 TxNamespaceHandler
类的初始化方法:
@Override public void init() { registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); // 使用 AnnotationDrivenBeanDefinitionParser 解析器,解析 annotation-driven 标签 registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); }
根据上面的方法,Spring
在初始化时候,如果遇到诸如 <tx:annotation-driven>
开头的配置后,将会使用 AnnotationDrivenBeanDefinitionParser
解析器的 parse
方法进行解析。
public BeanDefinition parse(Element element, ParserContext parserContext) { registerTransactionalEventListenerFactory(parserContext); String mode = element.getAttribute("mode"); // AspectJ 另外处理 if ("aspectj".equals(mode)) { // mode="aspectj" registerTransactionAspect(element, parserContext); if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { registerJtaTransactionAspect(element, parserContext); } } else { // mode="proxy" AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); } return null; }
Spring
中的事务默认是以 AOP
为基础,如果需要使用 AspectJ
的方式进行事务切入,需要在 mode
属性中配置:
<tx:annotation-driven mode="aspectj"/>
本篇笔记主要围绕着默认实现方式,动态 AOP
来学习,如果对于 AspectJ
实现感兴趣请查阅更多资料~
注册 InfrastructureAdvisorAutoProxyCreator
与 AOP
一样,在解析时,会创建一个自动创建代理器,在事务 TX
模块中,使用的是 InfrastructureAdvisorAutoProxyCreator
。
首先来看,在默认配置情况下,AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext)
做了什么操作:
private static class AopAutoProxyConfigurer { public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { // 注册 InfrastructureAdvisorAutoProxyCreator 自动创建代理器 AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); // txAdvisorBeanName = org.springframework.transaction.config.internalTransactionAdvisor String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME; if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { Object eleSource = parserContext.extractSource(element); // Create the TransactionAttributeSource definition. // 创建 TransactionAttributeSource 的 bean RootBeanDefinition sourceDef = new RootBeanDefinition( "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); // 注册 bean,并使用 Spring 中的定义规则生成 beanName String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); // 创建 TransactionInterceptor 的 bean RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); // 创建 TransactionAttributeSourceAdvisor 的 bean RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); // 将 sourceName 的 bean 注入 advisor 的 transactionAttributeSource 属性中 advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); // 将 interceptorName 的 bean 注入到 advisor 的 adviceBeanName 属性中 advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); if (element.hasAttribute("order")) { // 如果配置了 order 属性,则加入到 bean 中 advisorDef.getPropertyValues().add("order", element.getAttribute("order")); } // 以 txAdvisorBeanName 名字注册 advisorDef parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); // 创建 CompositeComponentDefinition CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName)); parserContext.registerComponent(compositeDef); } } }
在这里注册了代理类和三个 bean
,这三个关键 bean
支撑了整个事务功能,为了待会更好的理解这三者的关联关系,我们先来回顾下 AOP
的核心概念:
- Pointcut
定义一个切点,可以在这个被拦截的方法前后进行切面逻辑。 - Advice
用来定义拦截行为,在这里实现增强的逻辑,它是一个祖先接口org.aopalliance.aop.Advice
。还有其它继承接口,例如MethodBeforeAdvice
,特定指方法执行前的增强。 - Advisor
用来封装切面的所有信息,主要是上面两个,它用来充当Advice
和Pointcut
的适配器。
回顾完 AOP
的概念后,继续来看下这三个关键 bean
:
- TransactionInterceptor: 实现了
Advice
接口,在这里定义了拦截行为。 - AnnotationTransactionAttributeSource:封装了目标方法是否被拦截的逻辑,虽然没有实现
Pointcut
接口,但是在后面目标方法判断的时候,实际上还是委托给了AnnotationTransactionAttributeSource.getTransactionAttributeSource
,通过适配器模式,返回了Pointcut
切点信息。 - TransactionAttributeSourceAdvisor: 实现了
Advisor
接口,包装了上面两个信息。
这三个 bean
组成的结构与 AOP
切面环绕实现的结构一致,所以先学习 AOP
的实现,对事务的了解会有所帮助