目录
一个程序中不可能没有事务,而 Spring 中,事务的实现方式分为两种:编程式事务(手动写代码操作事务)和声明式事务(利用注解⾃动开启和提交事务)。因为编程式事务实现相对麻烦,而声明式事务实现极其简单,接下来,我们介绍声明式事务。
1. Spring 声明式事务@Transactional使用
声明式事务的实现很简单, 只需要在需要事务的方法上添加 @Transactional 注解就可以实现了。@Transactional注解的作用是:无需手动开启事务和提交事务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 如果中途发生了没有处理的异常会自动回滚事务。
@Transactional可以用来修饰类或者方法:
- 修饰方法时: 只有修饰public 方法时才⽣效(修饰其他方法时不会报错, 也不生效)
- 修饰类时: 对 @Transactional 修饰的类中所有的 public 方法都生效
方法/类被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束之后, 自动提交事务。
代码实现:
@Slf4j
@RestController
@RequestMapping("/trans")
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/r1")
public String r1(String userName, String password) {
Integer result = userService.insertUser(userName, password);
log.info("数据插入成功,result:"+result);
return "注册成功";
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public Integer insertUser(String userName, String password) {
return userInfoMapper.insertUser(userName, password);
}
}
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(user_name, `password`) values(#{userName},#{password})")
Integer insertUser(String userName, String password);
}
执行代码之前,数据库中的数据如下:
运行程序,访问http://127.0.0.1:8080/trans/r1?userName=lisi&password=123,响应结果如下:
观察日志,事务提交成功:
查看数据库,数据插入成功:
接下来,我们修改程序, 使之出现异常:
运行程序,访问:http://127.0.0.1:8080/trans/r1?userName=lisi&password=1234,响应结果:
观察后端日志,虽然日志显示数据插入成功,但事务并没有进行提交,进行了回滚:
查看数据库,并没有插入数据:
如果我们不使用@Transactional注解,同样进行上述操作:
运行程序,访问:http://127.0.0.1:8080/trans/r1?userName=lisi&password=1234,响应结果:
观察后端日志,此时没有事务相关的日志,并且日志上显示数据插入成功:
查看数据库,发现数据插入成功了:
这就是使用事务和不使用事务的区别,当方法发生异常时,如果使用了事务,事务会进行回滚,数据不会插入成功;而不使用事务,即使发生异常,已经插入的数据并不会进行回滚。
2. @Transactional详解
通过上⾯的代码, 我们学习了 @Transactional 的基本使⽤。接下来我们学习 @Transactional 注解的使用细节,
@Transactional 注解当中的三个常见属性:
- rollbackFor: 异常回滚属性。指定能够触发事务回滚的异常类型,可以指定多个异常类型
- Isolation: 事务的隔离级别。默认值为 Isolation.DEFAULT
- propagation: 事务的传播机制。默认值为 Propagation.REQUIRED
2.1. rollbackFor
接下来,我们模拟几种异常情况,来演示事务回滚。
- 修改代码:
运行程序,访问http://127.0.0.1:8080/trans/r2?userName=lisi11&password=12345,响应结果:
观察后端日志,事务并没有提交成功,而是进行了回滚:
查看数据库,并没有插入新的数据:
- 接下来,我们继续修改代码:
运行程序,访问http://127.0.0.1:8080/trans/r3?userName=lisi11&password=12345,响应结果:
观察后端日志,事务提交成功:
查看数据库,数据插入成功:
为什么同样是异常,有的异常,事务进行了回滚,而有的异常,事务并没有回滚?
其实,异常的事务回滚是有条件的,@Transactional 默认只在遇到运行时异常和Error时才会回滚, 非运行时异常不回滚,即Exception的子类中, 除了RuntimeException及其⼦类。
我们希望当发生非运行时异常时,事务也进行回滚,应该怎么做呢?
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚。
- 修改代码:
运行程序,访问http://127.0.0.1:8080/trans/r4?userName=lisi11&password=12345,响应结果:
观察后端日志,事务没有提交,事务进行了回滚:
查看数据库,并没有新的数据插入:
- 对比r1,继续修改代码,针对异常进行try catch:
运行程序,访问http://127.0.0.1:8080/trans/r5?userName=lisi11&password=12345,响应结果:
观察后端日志,事务提交成功:
查看数据库,新的数据插入成功:
总结:如果我们对异常进行了捕获,事务就进行提交。
如果发生异常,我们对异常进行了捕获,也希望事务进行回滚,应该怎么做呢?
两种做法:
- 继续把异常抛出去
- 手动设置回滚
演示做法1:
程序运行前,数据库的数据如下:
运行程序,访问http://127.0.0.1:8080/trans/r6?userName=lisi11&password=1111,响应结果:
观察后端日志,事务进行了回滚:
查看数据库,没有新的数据插入:
演示做法2:
手动设置事务回滚:使用 TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使用 setRollbackOnly 设置 setRollbackOnly
运行程序,访问http://127.0.0.1:8080/trans/r7?userName=lisi11&password=1111,响应结果:
观察后端日志,事务并没有提交,事务进行了回滚:
查看数据库,没有新的数据插入:
总结@Transactional注解:
- 方法执行前,开启事务
- 执行方法
- 方法正常运行,事务进行提交。方法发生运行时异常且没有进行捕获或者发生Error,事务进行回滚。方法发生运行时异常且程序进行了捕获,事务提交;如果既希望捕获又不希望事务提交,继续抛出异常或者手动设置事务回滚。其余异常,方法不回滚,如果需要回滚,设置rollbbackFor。
3. 事务的隔离级别
3.1. MySQL事务的隔离级别
SQL 标准定义了四种隔离级别,MySQL 全都⽀持。这四种隔离级别分别是:
- READ-UNCOMMITTED(读未提交) :最低的隔离级别,该隔离级别的事务可以读到其他事务中未提交的数据,可能会导致脏读、幻读或不可重复读。
因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数
据称之为脏数据, 这个问题称之为脏读。
- READ-COMMITTED(读已提交) :该隔离级别的事务允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
该隔离级别不会有脏读的问题.但由于在事务的执行中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读。
- REPEATABLE-READ(可重复读) : 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交。也就可以确保同⼀事务多次查询的结果⼀致, 可以阻止脏读和不可重复读。但是对其他事务新插⼊的数据, 是可以感知到的。这也就引发了幻读问题。可重复读, 是 MySQL 的默认事务隔离级别。
比如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是⼀样的, 所以会导致查询不到这条数据, 自己重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但自己就是插⼊不进去, 这种现象叫做幻读。
- SERIALIZABLE(串行化) :事务最高的隔离级别。它会强制所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,该级别可以防止脏读、不可重复读以及幻读。
3.2. Spring事务的隔离级别
Spring 定义了一个枚举类:Isolation
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Spring 中事务隔离级别有5 种:
- Isolation.DEFAULT: 以连接的数据库的事务隔离级别为主,MySQL 默认采用的REPEATABLE_READ 隔离级别, Oracle 默认采用的 READ_COMMITTED 隔离级别。
- Isolation.READ_UNCOMMITTED: 最低的隔离级别,读未提交, 对应SQL标准中 READ UNCOMMITTED
- Isolation.READ_COMMITTED: 读已提交,允许读取并发事务已经提交的数据,对应SQL标准中 READ COMMITTED
- Isolation.REPEATABLE_READ: 可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,对应SQL标准中 REPEATABLE READ
- Isolation.SERIALIZABLE:串行化,最高的隔离级别,它会强制所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,对应SQL标准中 SERIALIZABLE
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置:
@Transactional(isolation = Isolation.DEFAULT)
@RequestMapping("/r7")
public String r7(String userName, String password) {
// 代码略
return "注册成功";
}