目录
1.什么是事务?
2.Spring中的事务
1.编程式事务(了解)
2.声明式事务
@Transactional注解的作用范围
@Transactional注解包含的属性
@Transactional注意事项
@Transactional工作原理
3.Spring事务失效的场景有哪些?
1.代码中有try/catch
2.数据库不支持事务
3.非public修饰的方法
4.timeout超时
5.调用类内部@Transactional方法
定义:在一组操作中,这组操作被视为整体,具有原子性,要么全部执行成功,要么全部失败回滚.
之前在学习数据库的事务时举过一个经典例子:A向B转账100元,B从A接收100元
这组操作就要被视为一个整体,要么全部执行成功,100元从A到B,要么全部失败,A的100元还在A中
保证了数据的完整性和一致性
回顾事务的特点有四个:原子性、一致性、隔离性和持久性
原子性: 指事务的所有操作要么全部执行成功,要么全部回滚
一致性: 指事务前后,数据的完整性和一致性始终得到保证
隔离性: 指并发的事务之间是相互隔离的,互不影响
持久性: 指一旦事务提交,所做的修改将永久生效
Spring中的事务操作分为两类:
编程式事务是通过在代码中显式地编写事务管理相关的代码来实现事务管理的方式
DataSourceTransactionManager和TransactionDefinition是实现编程式事务的两个重要类
DataSourceTransactionManager是Spring框架提供的一种事务管理器,它与数据源(DataSource)紧密关联,可以用于管理基于JDBC的事务。在使用DataSourceTransactionManager之前,需要先配置数据源和事务管理器等相关信息。
下面是使用DataSourceTransactionManager进行编程式事务的步骤:
1) 配置数据源和事务管理器 在Spring的配置文件中,配置数据源和DataSourceTransactionManager等相关配置信息。
2) 获取DataSourceTransactionManager对象 在编程中,通过依赖注入或手动创建的方式获取DataSourceTransactionManager对象。
3) 开启事务 在代码中通过调用DataSourceTransactionManager对象的getTransaction()方法来开启一个事务。
4) 做业务操作 在事务管理的代码块中执行具体的业务逻辑。
5) 提交或回滚事务 根据具体情况决定是否提交或回滚事务。可以调用DataSourceTransactionManager对象的commit()或rollback()方法来提交或回滚事务。
TransactionDefinition是Spring框架中定义事务属性的接口,包含了事务的传播行为(Propagation)、隔离级别(Isolation)、超时时间(Timeout)、是否只读(ReadOnly)等属性。它可以用于在编程式事务中控制事务行为,也可以用于配置声明式事务的属性。
package com.example.demo.controller;
import com.example.demo.service.UserService;
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
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//编程式事务需要的两个对象
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/del")
public int del(Integer id){
if (id==null || id<=0) return 0;
//1.开启事务
TransactionStatus transactionStatus = null;
int result = 0;
try{
transactionManager.getTransaction(transactionDefinition);
//业务操作,删除用户
result = userService.del(id);
System.out.println("删除: "+result);
//2.提交事务
//transactionManager.commit(transactionStatus);
}
catch (Exception e){
if (transactionStatus!=null){
//回滚事务
transactionManager.rollback(transactionStatus);
}
}
return result;
}
}
package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper {
int del(@Param("id")Integer id);
}
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public int del(Integer id){
return userMapper.del(id);
}
}
UserMapper.xml
delete from userinfo where id = #{id}
运行:
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@207fd604]
JDBC Connection [HikariProxyConnection@1675146723 wrapping com.mysql.cj.jdbc.ConnectionImpl@5a20e9d3] will be managed by Spring
==> Preparing: delete from userinfo where id = ?
==> Parameters: 3(Integer)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@207fd604]
删除: 1
编程式事务更加灵活,但是需要开发人员手动编写事务管理代码,不如声明式事务简洁
声明式事务是一种基于AOP(面向切面编程)实现的事务管理方式,使用注解或XML配置的方式来定义事务的属性,它的本质是利用Spring AOP来拦截方法调用,从而实现对事务的控制
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id){
if (id==null || id<=0) return 0;
return userService.del(id);
}
}
因此事务是能够提交的,向其中添加异常代码,看是否能回滚
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id){
if (id==null || id<=0) return 0;
int result = userService.del(id);
int num = 100/0;
return result;
}
}
这里因为出现异常,进行了回滚,导致删除的操作没有提交
1.在方法上使用@Transactional注解
当@Transactional注解标注在方法上时,它会覆盖类级别上的注解,并使该方法拥有独立的事务属性
2.在类上使用@Transactional注解
当@Transactional注解标注在类上时,它会被应用于该类中的所有方法,除非被某个子类覆盖掉,如果在子类中标注了@Transactional注解,它会覆盖父类上的注解,并按照子类的事务属性来执行
3.在接口上使用@Transactional注解
当@Transactional注解标注在接口上时,只有在实现该接口的类中定义了该方法并标注了@Transactional注解时,该注解才会生效
public interface UserService {
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
void updateUserInfo(User user);
void deleteUserById(int id);
}
只有实现UserService接口的类定义了updateUserInfo方法并标注了@Transactional注解时,该注解才会生效。其他实现类中的updateUserInfo方法不受影响
当给异常代码添加try-catch后执行会是什么结果?
添加异常处理后,此时控制台打印出报错信息,但是会发现事务已经成功提交,也就是发生异常之后没有进行回滚
再看经典例子,如果A向B转账100元,B接受之前发生了异常没能成功接收,但是A已经把钱成功转出,那么100元凭空消失了
为什么会出现这种问题?
@Transactional注解是用来管理事务的,当一个方法使用了该注解时,在方法执行过程中会创建一个事务,并在方法执行完成后根据方法的执行结果来决定是否提交或回滚事务。
对于整个事务而言,只有当抛出RuntimeException及其子类或Error异常时才会回滚事务。这是因为Spring框架默认将RuntimeException及其子类和Error看作是系统级异常,需要进行回滚处理。
而当我们在@Transactional注解下的方法中添加try-catch语句来捕获异常时,如果捕获异常后不再抛出RuntimeException及其子类或Error异常,则默认认为该异常已经被处理掉了,不会触发事务的回滚操作。
如果我们希望在捕获到异常后继续回滚事务,可以手动抛出RuntimeException或其子类或Error异常
例如:
@RequestMapping("/del")
@Transactional
public int del(Integer id) throws Exception{
if (id==null || id<=0) return 0;
int result = userService.del(id);
try {
int num = 100/0;
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}
删除id=5的user
JDBC Connection [HikariProxyConnection@1548165318 wrapping com.mysql.cj.jdbc.ConnectionImpl@5ccce90] will be managed by Spring
==> Preparing: delete from userinfo where id = ?
==> Parameters: 5(Integer)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53546945]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53546945]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53546945]
2023-05-30 10:34:59.502 ERROR 16556 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at com.example.demo.controller.UserController2.del(UserController2.java:21) ~[classes/:na]
再次查看数据库:
执行删除sql后,5没有被成功删除, 发生了回滚
另一种解决方案:手动回滚事务(使用情况较多)
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
Spring中通过TransactionInterceptor类来实现对@Transactional注解方法的拦截和事务管理。当Spring容器扫描到带有@Transactional注解的方法时,会自动生成一个事务拦截器(TransactionInterceptor),并将其织入到目标方法所在的代理对象中,从而生成一个带有事务管理功能的代理对象。
当客户端调用带有@Transactional注解的方法时,实际调用的是这个代理对象中被TransactionInterceptor类增强后的方法。在被增强的方法内部,TransactionInterceptor会根据事务管理的参数配置和@Transactional注解的属性,决定是否开启新的事务,以及如何进行事务的控制。
具体来说,当@Transactional注解中没有指定事务管理的参数配置时,Spring会使用默认的参数配置;当有参数配置时,则会根据这些配置来决定事务的创建、提交和回滚等操作
当目标对象实现了接口时,Spring框架会自动选择基于JDK动态代理(JDK Dynamic Proxy)的代理方式来生成代理对象
当目标对象没有实现接口时,Spring框架会选择使用基于CGLIB的代理方式来生成代理对象
3.Spring事务失效的场景有哪些?
这里的“失效”是指不符合我们的预期结果
如上述注意事项所说,当事务执行过程中抛出了异常,但该异常被业务代码捕获并处理了,并没有再将异常继续往上抛出,这时事务也会失效。这是因为Spring只会对unchecked异常(RuntimeException及其子类)和Error进行事务回滚,如果异常被捕获了却没有再次抛出,则Spring无法自动回滚事务。
当使用Spring Framework处理事务时,底层的数据库必须支持事务,否则事务将失效。例如MySQL的MyISAM引擎不支持事务操作,若使用MyISAM作为数据库引擎,则Spring事务将无效
当 @Transactional 修饰的方法为非 public 时,事务就失效了,比如以下代码当遇到异常之后,不能自动实现回滚:
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
int del(Integer id){
if (id==null || id<=0) return 0;
int result = userService.del(id);
int num = 100/0;
return result;
}
}
mysql> select*from userinfo;
+----+-----------+--------------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+-----------+--------------+-------+---------------------+---------------------+-------+
| 1 | lisi3 | 456789 | | 2022-12-06 17:10:48 | 2022-12-06 18:10:48 | 1 |
| 3 | zhangsan | 123456 | | 2023-05-18 17:21:49 | 2023-05-18 17:21:49 | 1 |
| 4 | wangwu | 123456 | | 2023-05-18 17:36:28 | 2023-05-18 17:36:28 | 1 |
| 5 | testuser | testpassword | NULL | 2023-05-30 03:10:00 | 2023-05-30 03:10:00 | 1 |
| 6 | testuser2 | testpassword | NULL | 2023-05-30 03:10:00 | 2023-05-30 03:10:00 | 1 |
+----+-----------+--------------+-------+---------------------+---------------------+-------+
5 rows in set (0.00 sec)
当程序出现运行时异常时,我们预期的结果是事务应该实现自动回滚,也就是删除用户失败,然而当我们查询数据库时,却发现事务并未执行回滚操作,成功删除了
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional(timeout = 3)
public int del(Integer id) throws InterruptedException{
if (id==null || id<=0) return 0;
int result = userService.del(id);
return result;
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public int del(Integer id) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
return userMapper.del(id);
}
}
没有正常删除成功
当在 @Transactional 上,设置了一个较小的超时时间时,如果方法本身的执行时间超过了设置的 timeout 超时时间,那么就会导致本来应该正常删除数据的方法执行失败
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del2")
public int del2(Integer id) {
return del(id);
}
@Transactional
public int del(Integer id) {
if (id==null || id<=0) return 0;
int result = userService.del(id);
int num = 100/0;
return result;
}
}
以上代码我们在添加方法 del 中添加了 @Transactional 声明式事务,并且添加了异常代码,我们预期的结果是程序出现异常,事务进行自动回滚
结果却成功删除了,原因就在于当调用类内部的 @Transactional 修饰的方法时,事务是不会生效的