前言👀~
上一章我们介绍了Spring AOP,今天来讲解Spring事务
如果各位对文章的内容感兴趣的话,请点点小赞,关注一手不迷路,讲解的内容我会搭配我的理解用我自己的话去解释如果有什么问题的话,欢迎各位评论纠正 🤞🤞🤞
个人主页:N_0050-CSDN博客
相关专栏:java SE_N_0050的博客-CSDN博客 java数据结构_N_0050的博客-CSDN博客 软件测试_N_0050的博客-CSDN博客 MySQL_N_0050的博客-CSDN博客 java EE_N_0050的博客-CSDN博客
什么是事务?
回顾之前数据库中的事务,事务是一组操作的集合,在数据库中可以把事务看作是一组SQL语句,例如转账这个操作就会涉及到多条SQL语句也就是一组集合,这个操作要么都成功要么都失败,所以事务具有原子性
为什么需要事务?
上面的例子中就说明为什么需要事务了,如果转账过程中转账方扣款成功,但是接收方未收到收款就会出现问题,所以我们使用事务可以解决这个问题,让一组操作要么都成功要么都失败
事务的操作:
1. 开启事务:start transaction/ begin (⼀组操作前开启事务)
2. 提交事务:commit (这组操作全部成功,提交事务)
3. 回滚事务:rollback (这组操作中间任何⼀个操作出现异常,回滚事务)
Spring 中事务的实现
1.编程式事务
类似MySQL事务,手动开启事务,手动提及事务,手动回滚事务。SpringBoot 内置了两个对象:DataSourceTransactionManager 事务管理器,用来获取事务(开启事务),提交或回滚事务。TransactionDefinition 是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus
• 代码演示,先把对应的数据准备好
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Date createTime;
private Date updateTime;
}
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(`user_name`,`password`)values(#{userName},#{password})")
Integer insert(@Param("userName") String userName, @Param("password") String password);
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public Integer insert(String userName, String password) {
return userInfoMapper.insert(userName, password);
}
}
观察输出结果,返回注册成功,观察一下日志也是插入成功返回结果也是1
但是数据库中没有数据!!!说明事务回滚了
我们修改一下代码试试
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
//事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
//事务的属性
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("/registry")
public String registry(String userName, String password) {
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
dataSourceTransactionManager.commit(transaction);
return "注册成功";
}
}
观察输出结果,接口返回的还是注册成功,再看看日志这次多了一条语句,提交了sqlSession
观察数据库,id为2,说明上条数据确实有插入但是事务回滚了
2.声明式事务
通过@Transactional注解来实现自动开启和提交事务(推荐),这个注解注意应当加到我们Service里对应的是业务逻辑层。无需手动开启事务和提交事务,进⼊方法时⾃动开启事务,⽅法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。如果所在方法没有抛出异常时,事务就提交
@Transactional注解可以用来修饰⽅法或类:
1.修饰⽅法时:只有修饰public ⽅法时才⽣效(修饰其他⽅法时不会报错,也不生效)
2.修饰类时:对 @Transactional 修饰的类中所有的 public ⽅法都生效
• 下面进行演示,正常情况
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
return "注册成功";
}
}
观察输出结果,可以发现事务有提交那就上面没有问题了
• 如果在方法执行过程中,出现异常,且异常未被捕获,我们看看结果会是什么样的
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
int a = 10 / 0;
return "注册成功";
}
}
观察输出结果,报异常了,事务也未进行提交。说明方法执行过程中,出现异常,且异常未被捕获就进行事务回滚操作
• 如果异常被程序捕获的话,结果会不会不一样呢?
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
try {
int a = 10 / 0;
} catch (Exception e) {
log.error("程序出错");
}
return "注册成功";
}
}
观察输出结果,接口没问题,日志上面我设置了对应输出的内容,我们可以得出异常被程序捕获的话,方法就被认为是成功执行,依然会提交事务
从数据库中也可以得出,上一段代码提交异常回滚了,因为id的变化
补充:@Transactional注解也是AOP一种实现的体现,对于事务这一类事情进行集中处理,在整个方法执行前先开启事务,如果发生异常就进行回滚,如果没有就正常提交
如果需要事务进行回滚, 有以下两种方式:
1. 重新抛出异常:
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
try {
int a = 10 / 0;
} catch (Exception e) {
log.error("程序出错");
throw e;//补获后再抛出
}
return "注册成功";
}
}
观察输出结果,没有提交事务的日志,说明事务进行回滚了
2. 手动回滚事务:
我们可以选择重新抛异常的方式
使用 TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使用 setRollbackOnly 设置 setRollbackOnly
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
try {
int a = 10 / 0;
} catch (Exception e) {
log.error("程序出错");
//捕获异常后设置事务回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功";
}
}
观察输出结果,没有事务提交的日志,那就说明事务进行了回滚
小结:@Transactional注解它不并关注方法内部是怎么处理的,它就看你这个异常到底有没有处理掉。例如公司的一个小部门发生了一些小事情,只要没有发酵到哈高层那边,就当没有这件事情,哪怕部门里的人知道,就当没有这件事情
@Transactional详解
@Transactional 注解当中的三个常见属性:
1. rollbackFor
异常回滚属性,指定能够触发事务回滚的异常类型,可以指定多个异常类型
• 下面进演示非运行时异常情况
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) throws IOException {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
try {
int a = 10 / 0;
} catch (Exception e) {
log.error("程序出错");
throw new IOException();
}
return "注册成功";
}
}
观察输出结果,虽然异常捕获后重新抛出了,但是事务还是成功提交了
• 运行时异常情况
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String userName, String password) {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
try {
int a = 10 / 0;
} catch (Exception e) {
log.error("程序出错");
throw new RuntimeException();
}
return "注册成功";
}
}
观察输出结果,没有事务提交的日志,说明事务进行回滚了
小结:@Transactional 默认只在遇到运行时异常和Error时才会回滚,⾮运行时异常不回滚,即Exception的⼦类中,除了RuntimeException及其⼦类
• 使用rollbackFor指定触发遇到啥样的类型就回滚
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Transactional(rollbackFor = {Exception.class, Error.class})
@RequestMapping("/registry")
public String registry(String userName, String password) throws IOException {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
try {
int a = 10 / 0;
} catch (Exception e) {
log.error("程序出错");
throw new IOException();
}
return "注册成功";
}
}
这次抛IOException观察输出结果,没有事务提交的日志,说明事务进行回滚了。说明我们设置的rollback生效了
小结:在Spring的事务管理中,默认只在遇到运行时异常RuntimeException和Error时才会回滚。如果需要回滚指定类型的异常,可以通过rollbackFor属性来指定
2. Isolation
事务的隔离级别,默认值为 Isolation.DEFAULT。和MySQL的隔离级别差不多,下面简单回顾一下MySQL的隔离级别再解释Spring 事务隔离级别
MySQL 事务隔离级别:
• SQL 标准定义了四种隔离级别,MySQL 全都⽀持这四种隔离级别
• 1.读未提交(READ UNCOMMITTED):该隔离级别的事务可以读取到其他事务未进行提交的数据。因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读
• 2. 读提交(READ COMMITTED):该隔离级别不会有脏读的问题,但由于在事务的执行中可以读取到其他事务提交的结果,所以 在不同时间的相同 SQL 查询可能会得到不同的结果,这种现象叫做不可重复读
• 3. 可重复读(REPEATABLE READ):事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也就可以确保同⼀事务多次查询的结果⼀致。但是其他事务新插⼊的数据,是可以感知到的,这也就引发了幻读问题。可重复读,是 MySQL 的默认事务隔离级别
• ⽐如此级别的事务正在执行时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,自己重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去, 这个现象叫幻读
• 4. 串行化(SERIALIZABLE):序列化,事务最⾼隔离级别。它会强制事务排序,使之不会发⽣冲突。从而解决了上述发生的问题,但执行效率低
Spring 中事务隔离级别有5 种:
• 1. Isolation.DEFAULT:以连接的数据库的事务隔离级别为主
• 2. Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中 READ UNCOMMITTED
• 3. Isolation.READ_COMMITTED:读已提交,对应SQL标准中 READ COMMITTED
• 4. Isolation.REPEATABLE_READ:可重复读,对应SQL标准中 REPEATABLE READ
• 5. Isolation.SERIALIZABLE:串行化,对应SQL标准中 SERIALIZABLE
• 可以通过Isolation属性设置事务的隔离级别
3. propagation
事务的传播机制,默认值为 Propagation.REQUIRED,我们可以通过 propagation 属性来指定传播⾏为
什么是事务传播机制?
事务传播机制就是多个事务方法存在调用关系时, 事务是如何在这些方法间进行传播的
例子:比如有两个⽅法A, B都被 @Transactional 修饰,A⽅法调⽤B⽅法,A⽅法运行时, 会开启⼀个事务. 当A调⽤B时, B⽅法本⾝也有事务, 此时B⽅法运行时,是加⼊A的事务,还是创建⼀个新的事务呢?这个就涉及到了事务的传播机制
• 事务隔离级别解决的是多个事务同时调用⼀个数据库的问题
• 事务传播机制解决的是⼀个事务在多个节点(方法)中传递的问题
Spring 事务传播机制有以下 7 种:
• 1. Propagation.REQUIRED :默认的事务传播级别,如果当前存在事务, 则加⼊该事务,如果当前没有事务,则创建⼀个新的事务。如图中方法1有事务了方法2直接用方法1的事务
• 2. Propagation.SUPPORTS: 如果当前存在事务,则加⼊该事务。如果方法1有事务,方法2和方法1共用一个事务。如图中如果方法1当前没有事务(虽然方法2加了@Transactional )方法2还是以⾮事务的⽅式继续运行。所以从这可以得出并不是加了@Transactional注解事务就会生效的
• 3. Propagation.MANDATORY: 强制性,如果当前存在事务,则加⼊该事务,如果当前没有事务,抛出异常。如图中这个简单方法1有事务方法2直接用,如果没有直接抛异常
• 4. Propagation.REQUIRES_NEW: 创建⼀个新的事务,如果当前存在事务,则把当前事务挂起,也就是说不管外部⽅法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开启自己的事务,且开启的事务相互独立,互不干扰
• 5. Propagation.NOT_SUPPORTED: 以⾮事务⽅式运行,如果当前存在事务,则把当前事务挂起(即使方法1有事务,我也不用)
• 6. Propagation.NEVER: 以⾮事务⽅式运行, 如果当前存在事务,则抛出异常。如果方法1存在事务,方法2就抛出异常
• 7. Propagation.NESTED: 如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务(可以看作是子事务)来运行。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。可以看作一个大公司可以有很多子公司,如果子公司发生问题肯定会影响到大公司
Spring 事务传播行为有什么用?
答案就几个字:控制事务的边界
Spring 事务传播机制使用和各种场景演示:
对于以上事务传播机制,我们重点关注以下就可以了:
1. REQUIRED(默认值)
• 下面进行演示,再准备一些数据
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info(`user_name`,`op`)values(#{userName},#{op})")
Integer insertLog(@Param("userName") String userName, @Param("op") String op);
}
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String op) {
return logInfoMapper.insertLog(userName, "用户注册");
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String password) {
return userInfoMapper.insert(userName, password);
}
}
@Slf4j
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserService userService;
@Autowired
private LogInfoService logInfoService;
@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/registry")
public String registry(String userName, String password) throws IOException {
Integer result = userService.insert(userName, password);
log.info("用户插入成功,result:" + result);
Integer result2 = logInfoService.insert(userName, "用户注册");
log.info("日志插入成功,result:" + result2);
return "注册成功";
}
}
观察输出结果,事务提交成功,两条SQL语句都成功执行了
• 我们试试异常情况,会是什么样的
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String op) {
int a = 10 / 0;
return logInfoMapper.insertLog(userName, "用户注册");
}
}
观察输出结果,事务进行回滚了,由于这两者都属于同一事务,一个事务出现异常进行回滚另外一个事务也会进行回滚。这就是默认事务传播级别
2. REQUIRES_NEW
创建⼀个新的事务,如果当前存在事务,则把当前事务挂起,也就是说不管外部⽅法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开启自己的事务,且开启的事务相互独立,互不干扰
我们修改一下上面的代码接着演示,看看输出结果还是不是和上面一样
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer insert(String userName, String password) {
return userInfoMapper.insert(userName, password);
}
}
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String op) {
int a = 10 / 0;
return logInfoMapper.insertLog(userName, "用户注册");
}
}
观察输出结果,第一个事务成功提交,即使第二个事务抛异常了也不受其影响
补充:上面的代码修改一下传播机制,也可以选择在另外一个LogInfoService改成REQUIRES_NEW,效果也是一样的,注意是都要UserService也要修改成REQUIRES_NEW,如果只在LogInfoService加效果就不是这样的了
3.NEVER
以非事务方式运行, 如果当前存在事务,则抛出异常。如果方法1存在事务,方法2就抛出异常
修改一下代码下面进行演示,一个是NEVER,一个是REQUIRES_NEW
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NEVER)
public Integer insert(String userName, String op) {
return logInfoMapper.insertLog(userName, "用户注册");
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer insert(String userName, String password) {
return userInfoMapper.insert(userName, password);
}
}
输出结果,抛异常了但是不影响另外一个REQUIRES_NEW传播机制级别的事务
4.Nested
如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务(可以看作是子事务)来运行。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。可以看作一个大公司可以有很多子公司,如果子公司发生问题肯定会影响到大公司
• 下面先演示都正常的情况下,将两个事务传播机制级别都设置成NESTED
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insert(String userName, String op) {
return logInfoMapper.insertLog(userName, "用户注册");
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insert(String userName, String password) {
return userInfoMapper.insert(userName, password);
}
}
观察输出结果,两个事务都提交成功
• 看看异常情况是怎么样的
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insert(String userName, String op) {
int a = 10 / 0;
return logInfoMapper.insertLog(userName, "用户注册");
}
}
观察输出结果,事务回滚了,和上面举的例子一个意思子公司发生问题肯定会影响到大公司
• 设置回滚事务,看看和required的区别
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insert(String userName, String op) {
try {
int a = 10 / 0;
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return logInfoMapper.insertLog(userName, "用户注册");
}
}
观察输出结果,虽然事务提交成功了,但是只有一个事务成功提交了
• 设置required的看看区别
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String op) {
try {
int a = 10 / 0;
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return logInfoMapper.insertLog(userName, "用户注册");
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String password) {
return userInfoMapper.insert(userName, password);
}
}
观察输出结果,又回到第一种情况了,如果一个事务出现问题进行回滚,另外一个事务也会一起回滚,所以没有一个事务会提交成功
NESTED和REQUIRED 有什么区别?
嵌套事务(NESTED)不会回滚嵌套之前的事务,也就是说嵌套事务可以实现部分事务回滚。REQUIRED 如果回滚就是回滚所有事务,不能实现部分事务的回滚(因为属于同⼀个事务)。可以简单理解为NESTED法人不同,REQUIRED 法人相同
• 1.整个事务如果全部执行成功,⼆者的结果是⼀样的
• 2.如果事务⼀部分执行成功,REQUIRED加⼊事务会导致整个事务全部回滚。NESTED嵌套事务可以实现局部回滚,不会影响上⼀个⽅法中执⾏的结果
补充:嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务进⼊之后相当于新建了⼀个保存点,⽽滚回时只回滚到当前保存点。REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加⼊事务的区别
以上便是本章Spring事务的知识点,这章概念偏多,好了我们下一章再见💕