1.事务机制
事务介绍
事务:数据库中多个操作合并在一起形成的操作序列,事务特征(ACID)
作用:
- 当数据库操作序列中个别操作失败时,提供一种方式使数据库状态恢复到正常状态(A),保障数据库即使在异常状态下仍能保持数据一致性(C)(要么操作前状态,要么操作后状态)
- 当出现并发访问数据库时,在多个访问间进行相互隔离,防止并发访问操作结果互相干扰(I)
Spring 事务一般加到业务层,对应着业务的操作,Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的,Spring 只提供统一事务管理接口
Spring 在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。程序是否支持事务首先取决于数据库 ,比如 MySQL ,如果是 Innodb 引擎,是支持事务的;如果 MySQL 使用 MyISAM 引擎,那从根上就是不支持事务的
保证原子性:
- 要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚
- 在 MySQL 中,恢复机制是通过回滚日志(undo log) 实现,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,直接利用回滚日志中的信息将数据回滚到修改之前的样子即可
- 回滚日志会先于数据持久化到磁盘上,这样保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务
隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
- TransactionDefinition.ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别,Oracle 默认采用的 READ_COMMITTED隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- TransactionDefinition.ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- TransactionDefinition.ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- TransactionDefinition.ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)
分布式事务:允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源,全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高
在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE
传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题,也就是方法嵌套:
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行
//外层事务 Service A 的 aMethod 调用内层 Service B 的 bMethod class A { @Transactional(propagation=propagation.xxx) public void aMethod { B b = new B(); b.bMethod(); } } class B { @Transactional(propagation=propagation.xxx) public void bMethod {} }
支持当前事务的情况:
- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务则加入该事务;如果当前没有事务则创建一个新的事务
- 内外层是相同的事务,在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚
- 工作流程:
- 线程执行到 serviceA.aMethod() 时,其实是执行的代理 serviceA 对象的 aMethod
- 首先执行事务增强器逻辑(环绕增强),提取事务标签属性,检查当前线程是否绑定 connection 数据库连接资源,没有就调用 datasource.getConnection(),设置事务提交为手动提交 autocommit(false)
- 执行其他增强器的逻辑,然后调用 target 的目标方法 aMethod() 方法,进入 serviceB 的逻辑
- serviceB 也是先执行事务增强器的逻辑,提取事务标签属性,但此时会检查到线程绑定了 connection,检查注解的传播属性,所以调用 DataSourceUtils.getConnection(datasource) 共享该连接资源,执行完相关的增强和 SQL 后,发现事务并不是当前方法开启的,可以直接返回上层
- serviceA.aMethod() 继续执行,执行完增强后进行提交事务或回滚事务
- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
不支持当前事务的情况:
- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起
- 内外层是不同的事务,如果 bMethod 已经提交,如果 aMethod 失败回滚 ,bMethod 不会回滚
- 如果 bMethod 失败回滚,ServiceB 抛出的异常被 ServiceA 捕获,如果 B 抛出的异常是 A 会回滚的异常,aMethod 事务需要回滚,否则仍然可以提交
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起
- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常
其他情况:
- TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务(两个事务没有关系)来运行
- 如果 ServiceB 异常回滚,可以通过 try-catch 机制执行 ServiceC
- 如果 ServiceB 提交, ServiceA 可以根据具体的配置决定是 commit 还是 rollback
- 应用场景:在查询数据的时候要向数据库中存储一些日志,系统不希望存日志的行为影响到主逻辑,可以使用该传播
requied:必须的、supports:支持的、mandatory:强制的、nested:嵌套的
超时属性
事务超时,指一个事务所允许执行的最长时间,如果超过该时间限制事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为 -1
只读属性
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务;只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中
读操作为什么需要启用事务支持:
- MySQL 默认对每一个新建立的连接都启用了
autocommit模式,在该模式下,每一个发送到 MySQL 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务 - 执行多条查询语句,如果方法加上了
@Transactional注解,这个方法执行的所有 SQL 会被放在一个事务中,如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的收益。如果不加@Transactional,每条 SQL 会开启一个单独的事务,中间被其它事务修改了数据,比如在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则这次整体的统计查询将会出现读数据不一致的状态
2.核心对象
事务对象
J2EE 开发使用分层设计的思想进行,对于简单的业务层转调数据层的单一操作,事务开启在业务层或者数据层并无太大差别,当业务中包含多个数据层的调用时,需要在业务层开启事务,对数据层中多个操作进行组合并归属于同一个事务进行处理
Spring 为业务层提供了整套的事务解决方案:
PlatformTransactionManager
TransactionDefinition
TransactionStatus
PTM
PlatformTransactionManager,平台事务管理器实现类:
DataSourceTransactionManager 适用于 Spring JDBC 或 MyBatis
HibernateTransactionManager 适用于 Hibernate3.0 及以上版本
JpaTransactionManager 适用于 JPA
JdoTransactionManager 适用于 JDO
JtaTransactionManager 适用于 JTA
管理器:
JPA(Java Persistence API)Java EE 标准之一,为 POJO 提供持久化标准规范,并规范了持久化开发的统一 API,符合 JPA 规范的开发可以在不同的 JPA 框架下运行
非持久化一个字段:
static String transient1; // not persistent because of static final String transient2 = “Satish”; // not persistent because of final transient String transient3; // not persistent because of transient @Transient String transient4; // not persistent because of @TransientJDO(Java Data Object)是 Java 对象持久化规范,用于存取某种数据库中的对象,并提供标准化 API。JDBC 仅针对关系数据库进行操作,JDO 可以扩展到关系数据库、XML、对象数据库等,可移植性更强
JTA(Java Transaction API)Java EE 标准之一,允许应用程序执行分布式事务处理。与 JDBC 相比,JDBC 事务则被限定在一个单一的数据库连接,而一个 JTA 事务可以有多个参与者,比如 JDBC 连接、JDO 都可以参与到一个 JTA 事务中
此接口定义了事务的基本操作:
| 方法 | 说明 |
|---|---|
| TransactionStatus getTransaction(TransactionDefinition definition) | 获取事务 |
| void commit(TransactionStatus status) | 提交事务 |
| void rollback(TransactionStatus status) | 回滚事务 |
Definition
TransactionDefinition 此接口定义了事务的基本信息:
| 方法 | 说明 |
|---|---|
| String getName() | 获取事务定义名称 |
| boolean isReadOnly() | 获取事务的读写属性 |
| int getIsolationLevel() | 获取事务隔离级别 |
| int getTimeout() | 获取事务超时时间 |
| int getPropagationBehavior() | 获取事务传播行为特征 |
Status
TransactionStatus 此接口定义了事务在执行过程中某个时间点上的状态信息及对应的状态操作:
| 方法 | 说明 |
|---|---|
| boolean isNewTransaction() | 获取事务是否处于新开始事务状态 |
| voin flush() | 刷新事务状态 |
| boolean isCompleted() | 获取事务是否处于已完成状态 |
| boolean hasSavepoint() | 获取事务是否具有回滚储存点 |
| boolean isRollbackOnly() | 获取事务是否处于回滚状态 |
| void setRollbackOnly() | 设置事务处于回滚状态 |
3.编程式
控制方式
编程式、声明式(XML)、声明式(注解)
环境准备
银行转账业务
包装类
public class Account implements Serializable { private Integer id; private String name; private Double money; ..... }DAO层接口:AccountDao
public interface AccountDao { //入账操作 name:入账用户名 money:入账金额 void inMoney(@Param("name") String name, @Param("money") Double money); //出账操作 name:出账用户名 money:出账金额 void outMoney(@Param("name") String name, @Param("money") Double money); }业务层接口提供转账操作:AccountService
public interface AccountService { //转账操作 outName:出账用户名 inName:入账用户名 money:转账金额 public void transfer(String outName,String inName,Double money); }业务层实现提供转账操作:AccountServiceImpl
public class AccountServiceImpl implements AccountService { private AccountDao accountDao; public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } @Override public void transfer(String outName,String inName,Double money){ accountDao.inMoney(outName,money); accountDao.outMoney(inName,money); } }映射配置文件:dao / AccountDao.xml
<mapper namespace="dao.AccountDao"> <update id="inMoney"> UPDATE account SET money = money + #{money} WHERE name = #{name} </update> <update id="outMoney"> UPDATE account SET money = money - #{money} WHERE name = #{name} </update> </mapper>jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://192.168.2.185:3306/spring_db jdbc.username=root jdbc.password=1234核心配置文件:applicationContext.xml
<context:property-placeholder location="classpath:*.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="accountService" class="service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"/> </bean> <bean class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="domain"/> </bean> <!--扫描映射配置和Dao--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="dao"/> </bean>测试类
ApplicationContext ctx = new ClassPathXmlApplicationContext("ap...xml"); AccountService accountService = (AccountService) ctx.getBean("accountService"); accountService.transfer("Jock1", "Jock2", 100d);
4.声明式
XML
tx使用
删除 TxAdvice 通知类,开启 tx 命名空间,配置 applicationContext.xml
<!--配置平台事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--定义事务管理的通知类-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!--定义控制的事务-->
<tx:attributes>
<tx:method name="transfer" read-only="false"/>
</tx:attributes>
</tx:advice>
<!--使用aop:advisor在AOP配置中引用事务专属通知类,底层invoke调用-->
<aop:config>
<aop:pointcut id="pt" expression="execution(* service.*Service.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>
- aop:advice 与 aop:advisor 区别
aop:advice 配置的通知类可以是普通 Java 对象,不实现接口,也不使用继承关系
aop:advisor 配置的通知类必须实现通知接口,底层 invoke 调用
MethodBeforeAdvice
AfterReturningAdvice
ThrowsAdvice
pom.xml 文件引入依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
tx配置
advice
标签:tx:advice,beans 的子标签
作用:专用于声明事务通知
格式:
<beans>
<tx:advice id="txAdvice" transaction-manager="txManager">
</tx:advice>
</beans>
基本属性:
- id:用于配置 aop 时指定通知器的 id
- transaction-manager:指定事务管理器 bean
attributes
类型:tx:attributes,tx:advice 的子标签
作用:定义通知属性
格式:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
</tx:attributes>
</tx:advice>
method
标签:tx:method,tx:attribute 的子标签
作用:设置具体的事务属性
格式:
<tx:attributes>
<!--标准格式-->
<tx:method name="*" read-only="false"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>
<aop:pointcut id="pt" expression="execution(* service.*Service.*(..))"/><!--标准-->
说明:通常事务属性会配置多个,包含 1 个读写的全事务属性,1 个只读的查询类事务属性
属性:
- name:待添加事务的方法名表达式(支持 * 通配符)
- read-only:设置事务的读写属性,true 为只读,false 为读写
- timeout:设置事务的超时时长,单位秒,-1 为无限长
- isolation:设置事务的隔离界别,该隔离级设定是基于 Spring 的设定,非数据库端
- no-rollback-for:设置事务中不回滚的异常,多个异常使用
,分隔 - rollback-for:设置事务中必回滚的异常,多个异常使用
,分隔 - propagation:设置事务的传播行为
注解
开启注解
XML
标签:tx:annotation-driven
归属:beans 标签
作用:开启事务注解驱动,并指定对应的事务管理器
范例:
<tx:annotation-driven transaction-manager="txManager"/>
纯注解
名称:@EnableTransactionManagement
类型:类注解,Spring 注解配置类上方
作用:开启注解驱动,等同 XML 格式中的注解驱动
范例:
@Configuration
@ComponentScan("com.seazean")
@PropertySource("classpath:jdbc.properties")
@Import({JDBCConfig.class,MyBatisConfig.class,TransactionManagerConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}
public class TransactionManagerConfig {
@Bean //自动装配
public PlatformTransactionManager getTransactionManager(@Autowired DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
配置注解
名称:@Transactional
类型:方法注解,类注解,接口注解
作用:设置当前类/接口中所有方法或具体方法开启事务,并指定相关事务属性
范例:
@Transactional(
readOnly = false,
timeout = -1,
isolation = Isolation.DEFAULT,
rollbackFor = {ArithmeticException.class, IOException.class},
noRollbackFor = {},
propagation = Propagation.REQUIRES_NEW
)
public void addAccount{}
说明:
@Transactional注解只有作用到 public 方法上事务才生效不推荐在接口上使用
@Transactional注解原因:在接口上使用注解,只有在使用基于接口的代理(JDK)时才会生效,因为注解是不能继承的,这就意味着如果正在使用基于类的代理(CGLIB)时,那么事务的设置将不能被基于类的代理所识别
正确的设置
@Transactional的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败默认情况下,事务只有遇到运行期异常 和 Error 会导致事务回滚,但是在遇到检查型(Checked)异常时不会回滚
- 继承自 RuntimeException 或 error 的是非检查型异常,比如空指针和索引越界,而继承自 Exception 的则是检查型异常,比如 IOException、ClassNotFoundException,RuntimeException 本身继承 Exception
- 非检查型类异常可以不用捕获,而检查型异常则必须用 try 语句块把异常交给上级方法,这样事务才能有效
事务不生效的问题
情况 1:确认创建的 MySQL 数据库表引擎是 InnoDB,MyISAM 不支持事务
情况 2:注解到 protected,private 方法上事务不生效,但不会报错
原因:理论上而言,不用 public 修饰,也可以用 aop 实现事务的功能,但是方法私有化让其他业务无法调用
AopUtils.canApply:
methodMatcher.matches(method, targetClass) --true--> return trueTransactionAttributeSourcePointcut.matches(),AbstractFallbackTransactionAttributeSource 中 getTransactionAttribute 方法调用了其本身的 computeTransactionAttribute 方法,当加了事务注解的方法不是 public 时,该方法直接返回 null,所以造成增强不匹配private TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } }情况 3:注解所在的类没有被加载成 Bean
情况 4:在业务层捕捉异常后未向上抛出,事务不生效
原因:在业务层捕捉并处理了异常(try..catch)等于把异常处理掉了,Spring 就不知道这里有错,也不会主动去回滚数据,推荐做法是在业务层统一抛出异常,然后在控制层统一处理
情况 5:遇到检测异常时,也无法回滚
原因:Spring 的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。想针对检测异常进行事务回滚,可以在 @Transactional 注解里使用 rollbackFor 属性明确指定异常
情况 6:Spring 的事务传播策略在内部方法调用时将不起作用,在一个 Service 内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务,事务注解要加到调用方法上才生效
原因:Spring 的事务都是使用 AOP 代理的模式,动态代理 invoke 后会调用原始对象,而原始对象在去调用方法时是不会触发拦截器,就是一个方法调用本对象的另一个方法,所以事务也就无法生效
@Transactional public int add(){ update(); } //注解添加在update方法上无效,需要添加到add()方法上 public int update(){}情况 7:注解在接口上,代理对象是 CGLIB
使用注解
Dao 层
public interface AccountDao { @Update("update account set money = money + #{money} where name = #{name}") void inMoney(@Param("name") String name, @Param("money") Double money); @Update("update account set money = money - #{money} where name = #{name}") void outMoney(@Param("name") String name, @Param("money") Double money); }业务层
public interface AccountService { //对当前方法添加事务,该配置将替换接口的配置 @Transactional( readOnly = false, timeout = -1, isolation = Isolation.DEFAULT, rollbackFor = {},//java.lang.ArithmeticException.class, IOException.class noRollbackFor = {}, propagation = Propagation.REQUIRED ) public void transfer(String outName, String inName, Double money); }public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; public void transfer(String outName, String inName, Double money) { accountDao.inMoney(outName,money); //int i = 1/0; accountDao.outMoney(inName,money); } }添加文件 Spring.config、Mybatis.config、JDBCConfig (参考ioc_Mybatis)、TransactionManagerConfig
@Configuration @ComponentScan({"","",""}) @PropertySource("classpath:jdbc.properties") @Import({JDBCConfig.class,MyBatisConfig.class}) @EnableTransactionManagement public class SpringConfig { }
5.模板对象
Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、RabbitTemplate、JmsTemplate、HibernateTemplate、RestTemplate
JdbcTemplate:提供标准的 sql 语句操作API
NamedParameterJdbcTemplate:提供标准的具名 sql 语句操作API
RedisTemplate:
public void changeMoney(Integer id, Double money) { redisTemplate.opsForValue().set("account:id:"+id,money); } public Double findMondyById(Integer id) { Object money = redisTemplate.opsForValue().get("account:id:" + id); return new Double(money.toString()); }
