Spring_事务
在mysql阶段的文章中,已经介绍过事务了。本篇文章是对mysql事务的总结和对使用Spring框架来实现事务操作的讲解。
事务回顾
什么是事务
事务时一组操作的集合,是一个不可分割的操作。
事务会把所有操作作为一个整体,一起向数据库提交或者撤销操作请求。所以这组操作要么同时成功,要么同时失败。
为什么需要事务
我们在程序开发的时候,会有事务的需求。
比如:转账操作。
第一步:A:-100元
第二步:B:+100元
事务的操作
事务的操作主要有三步:
1、开启事务:start transaction(一组操作开启事务)
2、提交事务:commit(这组操作全部成功,提交事务)
3、回滚事务:rollback(这组操作中间任何一个操作出现异常,回滚事务)
Spring中事务的实现
Spring中的事务实现操作分为两类:
1、编程式事务(手动写代码操作事务)
2、声明式事务(利用注解自动开启和提交事务)
假设现在有需求:用户注册,注册时在日志表中插入一条操作记录。
数据准备:
DROP TABLE
IFEXISTS user_info;
CREATE TABLE user_info (`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR ( 128 ) NOT NULL,`password` VARCHAR ( 128 ) NOT NULL,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now(),PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '日志表';-- 操作日志表
DROP TABLE
IFEXISTS log_info;
CREATE TABLE log_info (`id` INT PRIMARY KEY auto_increment,`user_name` VARCHAR ( 128 ) NOT NULL,`op` VARCHAR ( 256 ) NOT NULL,`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';
代码准备:
1、创建项目,引入SpringWeb,Mybatis,mysql等依赖
2、配置文件
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 5028driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration: # 配置打印 MyBatis⽇志log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true #配置驼峰⾃动转换
实体类:
Userinfo:
@Data
public class Userinfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;
}
Loginfo:
@Data
public class LogInfo {private Integer id;private String userName;private String op;private Date createTime;private Date updateTime;
}
Mapper:
UserinfoMapper:
@Mapper
public interface UserinfoMapper {@Insert("insert into user_info(user_name,password)values (#{userName},#{password})")Integer insert(String userName,String password);
}
LoginfoMapper:
@Mapper
public interface LoginfoMapper {@Insert("insert into log_info(user_name,op) values (#{userName},#{op})")Integer insertLog(String name,String op);
}
Service:
UserService:
@Service
public class UserService {@Autowiredprivate UserinfoMapper userinfoMapper;public Integer insert(String userName,String password){return userinfoMapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;public Integer login(String userName,String op){return loginfoMapper.insertLog(userName,op);}
}
Controller:
UserController:
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/r1")public boolean login(String userName,String password){userService.insert(userName,password);return true;}
}
Spring编程式事务(了解)
Spring手动操作事务有三个操作步骤:
- 开启事务
- 提交事务
- 回滚事务
SpringBoot内置了两个对象:
- DataSourceTransactionManager 事务管理器,用来开启、提交或回滚事务
- TransactionDefinition是事务的属性,在获取事务的时候需要将TransactionDefinition传递进去从而获得一个事务TransactionStatus
下面是代码实现:
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;//JDBC事务管理器@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;//定义事务属性@Autowiredprivate TransactionDefinition transactionDefinition;@RequestMapping("/r1")public boolean login(String userName,String password){//开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);//用户注册userService.insert(userName,password);//提交事务dataSourceTransactionManager.commit(transactionStatus);//回滚事务
// dataSourceTransactionManager.rollback(transactionStatus);return true;}
}
使用postMan进行测试:
提交事务:
回滚事务:
刷新之后数据库的数据并没有增加:
Spring声明式事务@Transactional
声明式事务只要在需要事务的方法上添加@Transactional注解就可以实现了。无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。
代码实现:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用户注册userService.insert(userName,password);return true;}
}
日志:
刷新数据库,发现数据插入成功:
修改程序,使它出现异常:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用户注册userService.insert(userName,password);//制造异常int a = 10/0;return true;}
}
重新测试:
日志:
刷新数据库,发现并没有数据插入:
对比日志:
那如果我们需要让它发生异常时不发生回滚呢?
此时我们可以使用try-catch将异常捕获住,代码修改如下:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用户注册userService.insert(userName,password);try {//制造异常int a = 10/0;} catch (Exception e) {e.getMessage();}return true;}
}
重新测试:
日志:
数据库:
那我们如果在异常捕获后需要事务进行回滚呢?有以下两种方式:
1、重新抛出异常
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用户注册userService.insert(userName,password);try {//制造异常int a = 10/0;} catch (Exception e) {//将异常重新抛出throw e;}return true;}
}
测试:
2、手动回滚事务
使用TransationAspectSupport.currentTransactionStatus()得到当前事务,并使用setRollbackOnly设置setRollbackOnly。
代码:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用户注册userService.insert(userName,password);try {//制造异常int a = 10/0;} catch (Exception e) {//手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return true;}
}
测试:
@Transactional详解
1、rollbackFor
上面我们已经知道了,@Transactional注解会开始事务并且自动提交/回滚事务。
我们将异常类型改为IOException再进行测试:
@Transactional@RequestMapping("/r2")public boolean login2(String userName,String password) throws IOException {//用户注册userService.insert(userName,password);if(true){throw new IOException();}return true;}
测试:
此时我们发现虽然程序已经抛出异常,但是事务仍然提交了:
数据库也新增了一条数据:
咦?这是为什么呢?不是抛出异常后,事务就应该自动回滚吗?
这是因为事务回滚的默认是遇到运行时异常进行回滚,我们上面的算数异常就属于运行时异常的子类。因此,能够正常进行回滚。
如何解决呢?
通过@Transactional中的rollbackfor属性进行解决:
@Transactional(rollbackFor = Exception.class)@RequestMapping("/r2")public boolean login2(String userName,String password) throws IOException {//用户注册userService.insert(userName,password);if(true){throw new IOException();}return true;}
测试:
可以看到事务发生了回滚:
数据库:
对上面内容的总结:
事务隔离级别
回顾Mysql事务隔离级别
1、读未提交:读未提交,也叫未提交读。该隔离级别的事务可以看到其他事务中未提交的事务。
因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称为脏数据,这个问题称之为脏读
脏读问题:
2、读提交:读已提交,也叫提交读,该隔离级别的事务能读取到已提交事务的数据。
该隔离级别不会有脏读问题,但由于在事务执行中可以读取到其他事务提交的结果,所以在不同的时间的相同sql查询可能会得到不同的结果,这种现象叫做不可重复读(前后多次读取,数据内容不一致)
不可重复读:
3、可重复读(mysql默认的隔离级别):事务不会读到其他事务对已有数据的修改,即使其他事务已经提交,也可以确保同一事务多次查询结果一致,但是其他事务新插入的数据,是可以感知到的,这也就引发了幻读问题。
此隔离级别事务执行时,另一个事务成功插入了某条数据,但因为它每次查询的结果都是一样的(修改能查询到是因为它涉及到了表中的所有数据行),所以会导致查询不到这条数据,这个现象称为幻读(前后多次读取,数据总量不同)。
幻读:
4、串行化:序列化,事务最高隔离级别。它会强制事务排序,使之不会发生冲突,从而解决了脏读,不可重复读和幻读问题,但因为执行效率第,所以真正使用的场景并不多。
Spring事务隔离级别
Spring中的事务隔离级别有5种:
1、Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
2、Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准的READ UNCOMMITTED。
3、Isolation.READ_COMITTED:读已提交,对应SQL标准中的READ COMMITTED。
4、Isolation.REPEATABLE_READ:可重复读,对应SQL标准中REPEATABLE READ。
5、Isolation.SERIALIZABLE:串行化,对应SQL标准中的SERIALIZABLE。
我们可以通过@Transactional中的islation属性设置事务隔离级别:
//设置为读已提交@Transactional(isolation = Isolation.READ_COMMITTED)@RequestMapping("/r3")public boolean login3(String userName,String password) throws IOException {//用户注册userService.insert(userName,password);if(true){throw new IOException();}return true;}
Spring事务传播机制
事务传播机制:是多个事务方法存在调用关系时,事务时如何在这些方法间进行传播的。
比如:Controller中的方法A调用Service中的方法B,它们都是被@Transactional修饰。A方法运行时,会开启事务,当A调用B时,B方法本身也有事务,此时方法B运行时,是加入A的事务还是在创建一个新的事务呢?
这就涉及到了事务的传播机制。
打个比方,公司的流程管理:
执行任务之前需要先写执行文档,任务执行结束,再写总结汇报。
此时A部门有一项工作是和B部门一起干的,此时B部门是直接使用A部门的文档,还是新建一个文档呢?
事务隔离级别解决的是多个事务同时调用一个数据库的问题:
而事务传播机制解决的是一个事务再多个方法中传递的问题
事务传播级别有哪些
@Transactional注解支持事务传播机制的设置,通过propagation属性来指定传播行为。
Spring事务传播机制有以下七种:
1、Propagtion.REQUIRED:默认的事务传播级别。如果当前存在事务,则加入该事务。如果没有事务,则创建一个新的事务。
2、Propagtion.SUPPORTS:如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行。
3、Propagtion.MANDATORY:强制性。如果当前存在事务,则加入该事务。如果当前没有事务,则抛出异常
4、Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起,也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法都会开启新的事务且开启的事务相互独立,互不干扰。
5、Propagtion.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起(不使用)。
6、Propagtion.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
7、Propagtion.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。如果当前没有事务则该取值等价于Propagtion.REQUIRED。
举例记忆:
事务传播机制场景演示
此时,用户注册不仅要在用户表中添加数据,在日志表中也需要进行登记。
REQUIRE
Controller:
@RequestMapping("/user2")
@RestController
public class UserController2 {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@Transactional@RequestMapping("/register")public boolean register(String userName,String password){/*** 用户表和注册表的插入理应再Service完成* 此处为了方便,直接在Controller中完成*/if(!StringUtils.hasLength(userName)||!StringUtils.hasLength(password)){return false;}Integer result = userService.insert(userName,password);System.out.println("result:"+result);//插入日志表Integer insert = logService.insert(userName, "用户注册");System.out.println("insert:"+insert);return true;}
}
Service:
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);return result;}
}
UserSerVice:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
mapper:
UserinfoMapper:
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info (user_name,password) values (#{userName},#{password})")Integer insert(String userName,String password);}
LoginfoMapper:
@Mapper
public interface LoginfoMapper {@Insert("insert into log_info(user_name,op) values (#{userName},#{op}) ")Integer insertLog(String userName,String op);
}
我们尝试在其中的一个Service中制造异常:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);//制造异常int a = 10/0;return result;}
}
测试:
从日志上可以看出,事务发生了回滚:
总结:
REQUIRE_NEW
修改Service代码即可:
UserService:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);//制造异常int a = 10/0;return result;}
}
再次进行测试,通过日志可以看到,日志表的事务发生了回滚,而用户表的事务提交了:
总结:
NEVER
UserService:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.NEVER)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NEVER)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);return result;}
}
这里我们不制造异常,但是让Controller存在事务进行测试:
可以看到仍然报了500:
NESTED
UserService:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);return result;}
}
测试没有异常的情况:
事务得到了提交:
测试有异常发生的情况:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);int a = 10/0;return result;}
}
事务回滚:
看起来NESTED传播机制好像跟REQUIRE机制没什么区别:但实际上NESTED可以实现部分回滚,使得其他事务能够被提交。
部分回滚:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);try {int a = 10/0;} catch (Exception e) {//部分回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return result;}
}
重新测试:
事务得到提交:
总结: