在mysql阶段的文章中,已经介绍过事务了。本篇文章是对mysql事务的总结和对使用Spring框架来实现事务操作的讲解。
事务回顾
什么是事务
事务时一组操作的集合,是一个不可分割的操作。
事务会把所有操作作为一个整体,一起向数据库提交或者撤销操作请求。所以这组操作要么同时成功,要么同时失败。
为什么需要事务
我们在程序开发的时候,会有事务的需求。
比如:转账操作。
第一步:A:-100元
第二步:B:+100元
事务的操作
事务的操作主要有三步:
1、开启事务:start transaction(一组操作开启事务)
2、提交事务:commit(这组操作全部成功,提交事务)
3、回滚事务:rollback(这组操作中间任何一个操作出现异常,回滚事务)
Spring中事务的实现
Spring中的事务实现操作分为两类:
1、编程式事务(手动写代码操作事务)
2、声明式事务(利用注解自动开启和提交事务)
假设现在有需求:用户注册,注册时在日志表中插入一条操作记录。
数据准备:
sql
DROP TABLE
IF
EXISTS 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
IF
EXISTS 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、配置文件
bash
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false
username: root
password: 5028
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰⾃动转换
实体类:
Userinfo:
java
@Data
public class Userinfo {
private Integer id;
private String userName;
private String password;
private Date createTime;
private Date updateTime;
}
Loginfo:
java
@Data
public class LogInfo {
private Integer id;
private String userName;
private String op;
private Date createTime;
private Date updateTime;
}
Mapper:
UserinfoMapper:
java
@Mapper
public interface UserinfoMapper {
@Insert("insert into user_info(user_name,password)values (#{userName},#{password})")
Integer insert(String userName,String password);
}
LoginfoMapper:
java
@Mapper
public interface LoginfoMapper {
@Insert("insert into log_info(user_name,op) values (#{userName},#{op})")
Integer insertLog(String name,String op);
}
Service:
UserService:
java
@Service
public class UserService {
@Autowired
private UserinfoMapper userinfoMapper;
public Integer insert(String userName,String password){
return userinfoMapper.insert(userName,password);
}
}
LogService:
java
@Service
public class LogService {
@Autowired
private LoginfoMapper loginfoMapper;
public Integer login(String userName,String op){
return loginfoMapper.insertLog(userName,op);
}
}
Controller:
UserController:
java
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/r1")
public boolean login(String userName,String password){
userService.insert(userName,password);
return true;
}
}
Spring编程式事务(了解)
Spring手动操作事务有三个操作步骤:
- 开启事务
- 提交事务
- 回滚事务
SpringBoot内置了两个对象:
- DataSourceTransactionManager 事务管理器,用来开启、提交或回滚事务
- TransactionDefinition是事务的属性,在获取事务的时候需要将TransactionDefinition传递进去从而获得一个事务TransactionStatus
下面是代码实现:
java
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
//JDBC事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
//定义事务属性
@Autowired
private 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注解就可以实现了。无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。
代码实现:
java
@RequestMapping("/trans")
@RestController
public class TransController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/r1")
public boolean login(String userName,String password){
//用户注册
userService.insert(userName,password);
return true;
}
}

日志:
刷新数据库,发现数据插入成功:
修改程序,使它出现异常:
java
@RequestMapping("/trans")
@RestController
public class TransController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/r1")
public boolean login(String userName,String password){
//用户注册
userService.insert(userName,password);
//制造异常
int a = 10/0;
return true;
}
}
重新测试:
日志:
刷新数据库,发现并没有数据插入:
对比日志:
那如果我们需要让它发生异常时不发生回滚呢?
此时我们可以使用try-catch将异常捕获住,代码修改如下:
java
@RequestMapping("/trans")
@RestController
public class TransController {
@Autowired
private 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、重新抛出异常
java
@RequestMapping("/trans")
@RestController
public class TransController {
@Autowired
private 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。
代码:
java
@RequestMapping("/trans")
@RestController
public class TransController {
@Autowired
private 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再进行测试:
java
@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属性进行解决:
java
@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属性设置事务隔离级别:
java
//设置为读已提交
@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:
java
@RequestMapping("/user2")
@RestController
public class UserController2 {
@Autowired
private UserService userService;
@Autowired
private 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:
java
@Service
public class LogService {
@Autowired
private LoginfoMapper loginfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String op) {
Integer result = loginfoMapper.insertLog(userName, op);
return result;
}
}
UserSerVice:
java
@Service
public class UserService {
@Autowired
private UserInfoMapper mapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insert(String userName, String password) {
return mapper.insert(userName,password);
}
}
mapper:
UserinfoMapper:
java
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info (user_name,password) values (#{userName},#{password})")
Integer insert(String userName,String password);
}
LoginfoMapper:
java
@Mapper
public interface LoginfoMapper {
@Insert("insert into log_info(user_name,op) values (#{userName},#{op}) ")
Integer insertLog(String userName,String op);
}
我们尝试在其中的一个Service中制造异常:
java
@Service
public class LogService {
@Autowired
private 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:
java
@Service
public class UserService {
@Autowired
private UserInfoMapper mapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer insert(String userName, String password) {
return mapper.insert(userName,password);
}
}
LogService:
java
@Service
public class LogService {
@Autowired
private 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:
java
@Service
public class UserService {
@Autowired
private UserInfoMapper mapper;
@Transactional(propagation = Propagation.NEVER)
public Integer insert(String userName, String password) {
return mapper.insert(userName,password);
}
}
LogService:
java
@Service
public class LogService {
@Autowired
private 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:
java
@Service
public class UserService {
@Autowired
private UserInfoMapper mapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insert(String userName, String password) {
return mapper.insert(userName,password);
}
}
LogService:
java
@Service
public class LogService {
@Autowired
private LoginfoMapper loginfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insert(String userName, String op) {
Integer result = loginfoMapper.insertLog(userName, op);
return result;
}
}
测试没有异常的情况:
事务得到了提交:
测试有异常发生的情况:
java
@Service
public class LogService {
@Autowired
private 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可以实现部分回滚,使得其他事务能够被提交。
部分回滚:
java
@Service
public class LogService {
@Autowired
private 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;
}
}
重新测试:
事务得到提交:
总结: