Spring 的事务管理分为两种:
- 编程式事务管理:通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。
- 声明式事务管理:建立在 AOP 之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务管理不需要入侵代码,并且提供了 @Transactional 注解更为简单、快捷地进行事务操作,推荐使用
事务属性介绍
事务隔离级别指的是若干个并发的事务之间的隔离程度,一般可分为以下五个级别:
- DEFAULT:默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这个值就是READ_COMMITTED。
- READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。
- READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
- REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
- SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
Spring事务的传播行为
Spring事务的传播行为是指在多个事务方法相互调用时,如何处理事务的传播和边界。Spring框架定义了以下七种事务传播行为:
-
REQUIRED(默认值):如果当前存在事务,则加入该事务,如果没有事务,则创建一个新事务。被调用方法将在同一个事务中执行。
-
SUPPORTS:如果当前存在事务,则加入该事务,如果没有事务,则以非事务方式执行。被调用方法将根据调用方是否有事务来决定是否在事务中执行。
-
MANDATORY:要求当前存在事务,如果没有事务,则抛出异常。
-
REQUIRES_NEW:无论当前是否存在事务,都会创建一个新事务。如果当前存在事务,会将当前事务挂起并创建一个新事务。
-
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,会将当前事务挂起。
-
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
-
NESTED:如果当前存在事务,则在当前事务的嵌套事务中执行。如果没有事务,则创建一个新事务。嵌套事务是外部事务的一部分,具有独立的保存点,并可以独立地提交或回滚。
这些传播行为可以通过@Transactional
注解的propagation
属性来指定,也可以在XML配置文件中通过<tx:method>
元素的propagation
属性来指定。
选择合适的事务传播行为非常重要,它决定了事务的范围和边界,对于保证数据的一致性和完整性至关重要。根据具体的业务需求和调用关系,选择适当的事务传播行为可以确保事务的正确执行
事务常用配置
- readOnly:该属性用于设置当前事务是否为只读事务,设置为 true 表示只读,false 则表示可读写,默认值为 false。
@Transactional(readOnly=true)
- rollbackFor:该属性用于设置需要进行回滚操作的异常类数组,当方法中抛出指定异常时,则进行事务回滚操作。
// 指定单一异常类
@Transactional(rollbackFor=RuntimeException.class);
// 指定多个异常类
@Transactional(rollbackFor={RuntimeException.class, Exception.class});
- rollbackForClassName:和 rollbackFor 类似,该属性用来设置需要进行回滚操作的异常类名称数组。
// 指定单一异常类名称
@Transactional(rollbackForClassName="RuntimeException")
// 指定多个异常类名称
@Transactional(rollbackForClassName={"RuntimeException","Exception"})
- noRollbackFor:该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。
// 指定单一异常类
@Transactional(noRollbackFor=RuntimeException.class)
// 指定多个异常类
@Transactional(noRollbackFor={RuntimeException.class, Exception.class})。
- noRollbackForClassName:和 noRollbackFor 类似,该属性用来设置不需要进行回滚操作的异常类名称数组。
// 指定单一异常类名称
@Transactional(noRollbackForClassName="RuntimeException")
// 指定多个异常类名称
@Transactional(noRollbackForClassName={"RuntimeException","Exception"})
- propagation: 该属性用于设置事务的传播行为。
@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
-
isolation:该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置。
-
timeout:该属性用于设置事务的超时秒数,默认值为 -1 表示永不超时。
XML 方式使用
在 applicationContext.xml中配置声明式事务
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 事务配置
属性:name:对哪些方法起作用,例如:insert* 表示所有以 insert 开头的方法名称。
一般只需要对增、删、改方法添加事务
rollback-for:指定需要进行事务回滚的异常类,默认是 uncheck 异常
其它属性一般默认即可
-->
<tx:method name="insert*" rollback-for="java.lang.Exception"/>
<tx:method name="delete*" rollback-for="java.lang.Exception"/>
<tx:method name="update*" rollback-for="java.lang.Exception"/>
</tx:attributes>
</tx:advice>
<!-- 编写 aop,对目标生成代理,进行事务的通知 -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut id="txPointcut" expression="execution (* com.cskt.service.*ServiceImpl.*(..))"/>
<!-- 将切点和事务的通知整合 -->
<aop:advisor advice-ref="advice" pointcut-ref="txPointcut"/>
</aop:config>
在UserMapper 接口中添加插入方法
@Insert("insert into user(id,`name`, money) " +
"values(#{id},#{name}, #{money})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
在 UserService 接口中添加方法定义
// 正常插入
int insertTest1(User user);
// 插入后抛出异常
int insertTest2(User user);
在 UserServiceImpl 类中添加方法实现
@Override
public int insertTest1(User user) {
return userMapper.insert(user);
}
@Override
public int insertTest2(User user) {
int count = userMapper.insert(user);
int i = 1 / 0; //会抛出一个异常
return count;
}
进行测试:
@Test
public void insertTest() {
ApplicationContext context=new
ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService=(UserService) context.getBean("userService");
User user = new User(4,"时分",200.0);
System.out.println("insertTest1:" + userService.insertTest1(user));
System.out.println();
User user2 = new User(5,"时秒",800.0);
System.out.println("insertTest2:" + userService.insertTest2(user2));
}
测试结果:
只有 insertTest1() 方法的记录插入成功
注解方式使用
XML 方式开启事务的注解支持
<!-- 开启注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
开启事务的注解支持之后,可以不用使用 XML,而是直接将 @Transactional 注解作用在类或者方法上
@Service("userService")
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public List<User> getAllUser() {
User user = new User(4,"时分",200.0);
insertTest1(user);
User user2 = new User(5,"时秒",800.0);
insertTest2(user2);
return userMapper.getAllUser();
}
}
进行测试:
@Test
public void getAllUser(){
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService=(UserService) context.getBean("userService");
List<User> userList= userService.getAllUser();
System.out.println(userList.toString());
}
测试结果:
1、脏读
脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也解释可能最终不会存到数据库中,也就是不存在的数据,读到了并一定最终存在的数据,这就是脏读
简单来说就是一个事务读到另外一个事务还没有提交的数据
2、不可重复读
不可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况
简单来说就是一个事务先后读取同一条记录,但两次读取的数据不同
3、幻读
幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读
简单来说就是一个事务按照条件查询时,没有查询到,但是插入时,发现数据已存在,好像出现了 "幻影"
举例:
1、事务A,查询是否存在 id=5 的记录,没有则插入,这是我们期望的正常业务逻辑
2、这个时候 事务B 新增的一条 id=5 的记录,并提交事务
3、事务A,再去查询 id=5 的时候,发现还是没有记录(因为这里是在RR级别下研究(可重复读),所以读到依然没有数据)
4、事务A,插入一条 id=5 的数据
最终 事务A 提交事务,发现报错了。这就很奇怪,查的时候明明没有这条记录,但插入的时候 却告诉我 主键冲突,这就好像幻觉一样。这才是所有的幻读