Spring:声明式事务
回顾事务
- 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
- 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。
事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用。
事务四个特性ACID
- 原子性(atomicity)
事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用 - 一致性(consistency)
一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中 - 隔离性(isolation)
可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏 - 持久性(durability)
事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中
spring中的事务
Spring事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务,下面通过讲解Spring的事务接口来了解Spring实现事务的具体策略。
Spring事务管理涉及的接口的联系如下:
事务管理器
Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。
Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。此接口的内容如下:
Public interface PlatformTransactionManager()...{
// 由TransactionDefinition得到TransactionStatus对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
Void commit(TransactionStatus status) throws TransactionException;
// 回滚
Void rollback(TransactionStatus status) throws TransactionException;
}
从这里可知具体的具体的事务管理机制对Spring来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以Spring事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、JPA。比如JDBC实现事务管理的机制是这样的:
JDBC事务
如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会为你处理事务边界。为了使用DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
基本事务属性的定义
上面讲到的事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法来得到事务,这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。
那么什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面,分别是:
- 传播行为
- 隔离级别
- 是否只读
- 事务超时
- 回滚规则
而TransactionDefinition接口内容如下:
public interface TransactionDefinition {
int getPropagationBehavior(); // 返回事务的传播行为
int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getTimeout(); // 返回事务必须在多少秒内完成
boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}
传播行为
事务的第一个方面是传播行为(propagation behavior)。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:
传播行为 | 含义 |
---|---|
propagation_requierd | 表示当前方法必须运行在事务中。如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。 |
propagation_supports | 支持当前事务,如果没有当前事务,就以非事务方法执行。 |
propagation_mandatory | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
propagation_required_new | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
propagation_not_supported | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
propagation_never | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
propagation_nested | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
隔离界别
事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。
脏读、不可重复读、幻象读概念说明:
- 脏读:指当一个事务正字访问数据,并且对数据进行了修改,而这种数据还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据还没有提交那么另外一个事务读取到的这个数据我们称之为脏数据。依据脏数据所做的操作肯能是不正确的。
- 不可重复读:指在一个事务内,多次读同一数据。在这个事务还没有执行结束,另外一个事务也访问该同一数据,那么在第一个事务中的两次读取数据之间,由于第二个事务的修改第一个事务两次读到的数据可能是不一样的,这样就发生了在一个事物内两次连续读到的数据是不一样的,这种情况被称为是不可重复读。
- 幻象读:一个事务先后读取一个范围的记录,但两次读取的纪录数不同,我们称之为幻象读(两次执行同一条 select 语句会出现不同的结果,第二次读会增加一数据行,并没有说这两次执行是在同一个事务中)
隔离级别 | 含义 |
---|---|
isolation_default | 使用后端数据库默认的隔离级别 |
isolation_read_uncommitted | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 |
isolation_read_committed | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 |
isolation_repeatable_read | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 |
Iisolation_serializable | 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 |
是否只读
事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。
事务超时
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
回滚规则
事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与EJB的回滚行为是一致的)
但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。
事务状态
上面讲到的调用PlatformTransactionManager接口的getTransaction()的方法得到的是TransactionStatus接口的一个实现,这个接口的内容如下:
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事物
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}
可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。
Spring中的事务实现的几种方式
Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring支持编程式事务管理和声明式的事务管理。
编程式事务
- 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
- 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码
声明式事务
- 一般情况下比编程式事务好用。
- 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。
声明式事务有三种方式
1、基于 TransactionProxyFactoryBean的声明式事务管理
2、 基于 @Transactional 的声明式事务管理
3、基于Aspectj AOP配置事务
这里用的就是这个
转账的事务管理
(基于spring整合mybatis)
场景:
账户之间A向B转账,转账的整个过程就可以看作是一个事务,这个事务又可以分为两个动作
1、A减少对应的金额
2、B增加对应的金额
如果动作1成功了,动作2失败了,那么事务就是没有提交成功,则需要回滚。
只有动作1和动作2都成功了,这个事务才算是真正完成。
mysql创建表
CREATE TABLE `account` (
`id` int(20) auto_increment PRIMARY KEY COMMENT '用户id',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`pwd` varchar(30) DEFAULT NULL COMMENT '密码',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Account账户类
import lombok.Data;//lombok插件
@Data//getter,setter,toString...
public class Account {
private int id;
private String name;
private String pwd;
private int money;
}
代理接口
AccountMapper.java
public interface AccountMapper {
Account getAccount(int id);//查找账户
int updateAccount(Account account);//更新账户
}
代理接口映射文件
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cong.dao.AccountMapper">
<select id="getAccount" parameterType="int" resultType="account">
select * from mybatis.account where id = #{id};
</select>
<update id="updateAccount" parameterType="account">
update mybatis.account set name = #{name},pwd = #{pwd},money = #{money}
where id = #{id};
</update>
</mapper>
代理接口实现类
AccountMapperIplm.java
public class AccountMapperImpl extends SqlSessionDaoSupport implements AccountMapper {
@Override
public Account getAccount(int id) {
return getSqlSessionTemplate().getMapper(AccountMapper.class).getAccount(id);
}
@Override
public int updateAccount(Account account) {
return getSqlSessionTemplate().getMapper(AccountMapper.class).updateAccount(account);
}
}
业务层接口
public interface AccountService {
void transaction(int fromId,int targetId,int money);//转账
}
业务接口实现类
import com.cong.dao.AccountMapper;
import com.cong.pojo.Account;
public class AccountServiceImpl implements AccountService {
private AccountMapper accountMapper;
public void setAccountMapper(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Override
public void transaction(int fromId, int targetId, int money) {
//1根据id查询转出账户
Account source = accountMapper.getAccount(fromId);
//2根据名称查询转入账户
Account target = accountMapper.getAccount(targetId);
//3转出账户减钱
source.setMoney(source.getMoney() - money);
//4转入账户加钱
target.setMoney(target.getMoney() + money);
//5更新转出账户
accountMapper.updateAccount(source);
int i = 1/0;//模拟出错
//6更新转入账户
accountMapper.updateAccount(target);
}
}
mybatis配置文件
mybatis-config.xml,留下别名,以容易看出是个整合的项目
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.cong.pojo"/>
</typeAliases>
</configuration>
mybatis整合spring配置文件
spring-dao.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--datasource-->
<!--配置数据源:数据源有非常多,可以使用第三方的,也可使使用Spring的-->
<bean id="dataSources" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!--sqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSources"/>
<!--关联Mybatis-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:com/cong/dao/*Mapper.xml"/>
<!--<property name="typeAliases" value="com.cong.pojo.Account"/>-->
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!--利用构造器注入,没有setter-->
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
</beans>
spring配置文件
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--导入整合的配置文件-->
<import resource="classpath:spring-dao.xml"/>
<bean id="accountMapper" class="com.cong.dao.AccountMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSession"/>
</bean>
<bean id="accountService" class="com.cong.service.AccountServiceImpl">
<property name="accountMapper" ref="accountMapper"/>
</bean>
</beans>
测试
@Test
public void test2() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = (AccountService) context.getBean("accountService");
accountService.transaction(1,2,90);//id为1的账户向id为2的账户转账90元
}
数据库中的内容
id | name | pws | money |
---|---|---|---|
1 | cong | ajd123. | 100 |
2 | rainbow | 1889869... | 5000 |
此时,没有事务的支持,转账的过程中会出错
帐号1的金额减少了90,而账户2的金额并没有增加90,执行之后的结果会变成这样
id | name | pws | money |
---|---|---|---|
1 | cong | ajd123. | 10 |
2 | rainbow | 1889869... | 5000 |
基于Aspectj AOP配置事务
修改applicationContext.xml,添加事务的支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--导入整合的配置文件-->
<import resource="classpath:spring-dao.xml"/>
<bean id="accountMapper" class="com.cong.dao.AccountMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSession"/>
</bean>
<bean id="accountService" class="com.cong.service.AccountServiceImpl">
<property name="accountMapper" ref="accountMapper"/>
</bean>
<!--事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSources"/>
</bean>
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transaction" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--配置aop织入事务-->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.cong.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
其中id="transactionManager"
就是创建一个事务管理器,因为
- 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
- 就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。
配置好事务管理器后我们需要去配置事务的通知
然后再通过aop进行横切,就可以进行事务的管理了。
将数据库中的值恢复到初始状态,再测试一下,数据库中的值不变,证明事务起到了作用。
将int i = 1/0;//模拟出错
注释掉,再次测试,结果如下
id | name | pws | money |
---|---|---|---|
1 | cong | ajd123. | 10 |
2 | rainbow | 1889869... | 5090 |
至此,转账的事务管理完成
思考问题?
为什么需要配置事务?
- 如果不配置,就需要我们手动提交控制事务;
- 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!