.
上一次我们讲到,Spring实现事务的方式有两种,并且,为实现这两种方式,我们做了一些准备工作,那么接下来,我将带着大家,来继续学习事务的相关知识
编程式事务
SpringBoot内置了两个对象
DataSourceTransactionManager 事务管理器. 用来获取事务(开启事务), 提交或回滚事务
TransactionDefinition 是事务的属性, 在获取事务的时候需要将TransactionDefinition 传递进去从而获得⼀个事务TransactionStatus
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class UserInfoController {@Autowiredprivate UserInfoService userInfoService;@Autowired// JDBC 事务管理器private DataSourceTransactionManager dataSourceTransactionManager;@Autowired// 定义事务属性private TransactionDefinition transactionDefinition;@RequestMapping("/login")public String login(String userName,String password){//开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);Integer result = userInfoService.insert(userName,password);//提交事务dataSourceTransactionManager.commit(transactionStatus);//回滚事务
// dataSourceTransactionManager.rollback(transactionStatus);if(result<=0){return "添加失败";}else {return "添加成功";}}
}
运行程序: http://127.0.0.1:8080/login?userName=zhangsan&password=123456
可以看到,当我们的代码中使用事务提交的时候,数据库中确实被插入了userName=zhangsan&password=123456,但是如果采用事务回滚,那么既是打印的日志显示插入成功,数据库中却并没有新增数据
声明式事务@Transactional
@RestController
public class UserInfoController2 {@Autowiredprivate UserInfoService userInfoService;@Transactional( isolation = READ_COMMITTED,rollbackFor = Exception.class)@RequestMapping("/login2")public String login(String userName,String password) throws IOException {Integer result = userInfoService.insert(userName,password);if(result<=0){return "添加失败";}else {Integer i = 10/0;return "添加成功";}}
}
运行程序,可以看到,当程序出现异常过后,事务就会自动回滚,但是当不存在 Integer i = 10/0;这一句的时候,数据库就会正常新增数据.
@Transactional 作用
可以用来修饰方法或类:
• 修饰方法时: 只有修饰public 方法时才生效(修饰其他方法时不会报错, 也不生效)[推荐]
• 修饰类时: 对 @Transactional 修饰的类中所有的 public 方法都生效.
方法/类被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束之后, 自动提交事务.
如果在方法执行过程中, 出现异常, 且异常未被捕获, 就进行事务回滚操作.
如果异常被程序捕获, 方法就被认为是成功执行, 依然会提交事务.
对上述代码进行修改过后,
@RestController
public class UserInfoController2 {@Autowiredprivate UserInfoService userInfoService;@Transactional( isolation = READ_COMMITTED,rollbackFor = Exception.class)@RequestMapping("/login2")public String login(String userName,String password) throws IOException {Integer result = userInfoService.insert(userName,password);if(result<=0){return "添加失败";}else {try {Integer integer = 10/0;}catch (Exception e){e.printStackTrace();}return "添加成功";}}
}
执行程序,可以看到,程序虽然报错,但是事务依旧提交成功了,数据库的数据已经成功新增了,如果在捕获了异常的情况下,想要将事务进行回滚,有以下两种方式
抛出异常
@RestController
public class UserInfoController2 {@Autowiredprivate UserInfoService userInfoService;@Transactional@RequestMapping("/login2")public String login(String userName,String password){Integer result = userInfoService.insert(userName,password);if(result<=0){return "添加失败";}else {try {Integer integer = 10/0;}catch (Exception e){throw e;}return "添加成功";}}
}
手动回滚
@RestController
public class UserInfoController2 {@Autowiredprivate UserInfoService userInfoService;@Transactional@RequestMapping("/login2")public String login(String userName,String password){Integer result = userInfoService.insert(userName,password);if(result<=0){return "添加失败";}else {try {Integer integer = 10/0;}catch (Exception e){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return "添加成功";}}
}
@Transactional 详解
rollbackFor
: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
可以看到,以上两种方式,都可以在捕获异常的情况下进行事务的回滚,但是有特殊情况,@RestController默认只在遇到运行时异常和Error时抛出异常才会回滚, 非运行时异常不回滚. 即Exception的子类中, 除了RuntimeException及其子类.
这里我们假设抛出一个IOException
@RestController
public class UserInfoController3 {@Autowiredprivate UserInfoService userInfoService;@Transactional@RequestMapping("/login2")public String login(String userName,String password) throws IOException {Integer result = userInfoService.insert(userName,password);if(result<=0){return "添加失败";}else {if (true){throw new IOException();}return "添加成功";}}
}
结果显示,虽然出现了异常,但事务仍旧提交了
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通
过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚
@RestController
public class UserInfoController3 {@Autowiredprivate UserInfoService userInfoService;@Transactional(rollbackFor = {Exception.class,Error.class})@RequestMapping("/login2")public String login(String userName,String password) throws IOException {Integer result = userInfoService.insert(userName,password);if(result<=0){return "添加失败";}else {if (true){throw new IOException();}return "添加成功";}}
}
结果显示,事务得到了回滚,数据库中并未添加数据
事务隔离级别
SQL 标准定义了四种隔离级别, MySQL 全都⽀持. 这四种隔离级别分别是:
- 读未提交(READ UNCOMMITTED): 读未提交, 也叫未提交读. 该隔离级别的事务可以看到其他事务中未提交的数据.因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读
- 读提交(READ COMMITTED): 读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据,该隔离级别不会有脏读的问题.但由于在事务的执⾏中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读
- 可重复读(REPEATABLE READ): 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同⼀事务多次查询的结果⼀致, 但是其他事务新插⼊的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.
⽐如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是⼀样的, 所以会导致查询不到这条数据, ⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因). 明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去, 这个现象叫幻读. - 串⾏化(SERIALIZABLE): 序列化, 事务最⾼隔离级别. 它会强制事务排序, 使之不会发⽣冲突, 从⽽解决了脏读, 不可重复读和幻读问题, 但因为执⾏效率低, 所以真正使⽤的场景并不多
在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
Spring 中事务隔离级别
Spring 中事务隔离级别有5 种:
4. Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.
5. Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ UNCOMMITTED
6. Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
7. Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
8. Isolation.SERIALIZABLE : 串行化, 对应SQL标准中 SERIALIZABLE
通过以下代码进行实现