包含了 3 个重要的操作:
依赖 2 个重要的对象:
4. DataSourceTransactionManager
(⽤来获取事务(开启事务)、提交或回滚事务的)
5. TransactionDefinition
(是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus)
application.yml
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
level:
com:
example:
demo: debug
UserMapper.xml
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<insert id="add">
insert into userinfo(username,password) values(#{username},#{password})
insert>
mapper>
UserInfo
import lombok.Data;
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
}
Userservice
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public int add(UserInfo userInfo) {
return userMapper.add(userInfo);
}
}
UserMappper
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
int add(UserInfo userInfo);
}
UserController
import com.example.demo.model.UserInfo;
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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
// JDBC 事务管理器
@Autowired
private DataSourceTransactionManager transactionManager;
// 定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
// 在此方法中使用编程式的事务
@RequestMapping("/add")
public int add(UserInfo userInfo){
//非空校验(验证用户名和密码不能为空)
if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())){
return 0;
}
// 开启事务(获取事务)
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
System.out.println("add 受影响的行数: "+result);
// 提交事务/回滚事务
transactionManager.rollback(transactionStatus); //回滚事务
return result;
}
}
再看我们数据库并没有添加进来:
想要提交的话:
声明式事务的实现很简单,只需要在需要的⽅法上添加 @Transactional
注解就可以实现了,⽆需⼿动开启事务和提交事务,进⼊⽅法时⾃动开启事务,⽅法执⾏完会⾃动提交事务,如果中途发⽣了没有处理的异常会⾃动回滚事务
// 使用声明式事务
@Transactional // 在方法之前,自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,自动回滚事务
@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
// 非空效验【验证用户名和密码不为空】
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) return 0;
int result = userService.add(userInfo);
System.out.println("add 受影响的行数:" + result);
int num = 10 / 0;
return result;
}
已经添加了:
再查数据库并没有添加:
注释掉异常:
再重启就可以添加进来了:
@Transactional 可以⽤来修饰⽅法或类:
修饰⽅法时:需要注意只能应⽤到 public ⽅法上
,否则不⽣效。推荐此种⽤法。
修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效(都会自动的开启和提交(回滚)事务)。
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时, 可以使用该属性指定选择哪个事务管理器 |
transactionManager | 当配置了多个事务管理器时, 可以使用该属性指定选择哪个事务管理器. |
propagation | 事务的传播行为. 默认为 Propagation.REQUIRED |
isolation | 事务的隔离级别. 默认为 Isolation.DAEFAULT |
timeout | 事务的超时事件. 默认值为-1, 如果超过该时间限制但事务还没有完成, 则自动回滚事务. |
readOnly | 指定事务是否只读事务. 默认为 false. 为了忽略那些不需要事务的方法, 比如读取数据, 可以设置为 read-only 为 true |
rollbackFor | 用于指定能够触发事务回滚的异常类型, 可以指定多个异常类型 |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型, 可以指定多个异常类型 |
noRollbackFor | 抛出指定的异常类型, 不回滚事务, 也可以指定多个异常类型 |
noRollbackForClassName | 抛出指定的异常类型, 不回滚事务, 也可以指定多个异常类型 |
// 使用声明式事务
@Transactional // 在方法之前,自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,自动回滚事务
@RequestMapping("/add3")
public int add3(UserInfo userInfo) {
// 非空效验【验证用户名和密码不为空】
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) return 0;
int result = userService.add(userInfo);
System.out.println("add 受影响的行数:" + result);
try {
int num = 10 / 0;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
此时启动程序不会报错:
再看数据库并没有进行回滚:
先删除之前的数据:
启动项目进行测试:
此时并没有成功插入:
并没有插入进来:
事务有4 ⼤特性(ACID),原⼦性、持久性、⼀致性和隔离性,具体概念如下:
原⼦性(Atomicity,或称不可分割性)
⼀致性(Consistency)
隔离性(Isolation,⼜称独⽴性)
持久性(Durability)
原⼦性: ⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。
⼀致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。
持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
隔离性: 数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化
(Serializable)。
Spring 中事务隔离级别可以通过 @Transactional
中的 isolation
属性进⾏设置.
READ UNCOMMITTED:
读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。READ COMMITTED:
读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。REPEATABLE READ:
可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读(Phantom Read)。SERIALIZABLE:
序列化,事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多
脏读:
⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错的。
不可重复读:
⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。
幻读:
⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。
Isolation.DEFAULT:
以连接的数据库的事务隔离级别为主。Isolation.READ_UNCOMMITTED:
读未提交,可以读取到未提交的事务,存在脏读。Isolation.READ_COMMITTED:
读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。Isolation.REPEATABLE_READ:
可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。Isolation.SERIALIZABLE:
串⾏化,可以解决所有并发问题,但性能太低。从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)注意事项:
1. 当 Spring 中设置了事务隔离级别和连接的数据库 (MySQL) 事务隔离级别发生冲突时,那么以 Spring 的为准.
2. Spring 中的事务隔离级别机制的实现是依靠连接数据库支持事务隔离级别为基础.
事务传播机制: Spring 事务传播机制定义了多个包含了事务的⽅法,相互调⽤时,事务是如何在这些⽅法间进⾏传递的。
事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性的),⽽事务传播机制是保证⼀个事务在多个调⽤⽅法间的可控性的(稳定性的)。
事务隔离级别: 解决的是多个事务同时调用数据库的问题(并发事务)
事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题
Spring 事务的传播机制包含以下 7 种 :
1. Propagation.REQUIRED: 默认的事务传播级别, 它表示如果当前存在事务, 则加入该事务; 如果当前没有事务, 则创建一个新的事务.
2. Propagation.SUPPORTS: 如果当前存在事务, 则加入该事务; 如果当前没有事务, 则以非事务的方式继续运行.
3. Propagation.MANDATORY: 如果当前存在事务, 则加入该事务; 如果当前没有事务, 则抛出异常.
4. Propagation.REQUIRES_NEW: 表示创建一个新的事务, 如果当前存在事务, 则把当前事务挂起.
5. Propagation.NOT_SUPPORTED: 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起.
6. Propagation.NEVER: 以非事务方式运行, 如果当前存在事务, 则抛出异常.
7. Propagation.NESTED: 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行; 如果不存在事务, 则创建一个新的事务.
以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
以情侣关系为例来理解以上分类:
先开启事务先成功插⼊⼀条⽤户数据,然后再执⾏⽇志报错,⽽在⽇志报错是发⽣了异常,观察 propagation = Propagation.REQUIRED 的执⾏结果
@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/add4")
public int add4(UserInfo userInfo) {
if (userInfo == null ||
!StringUtils.hasLength(userInfo.getUsername()) ||
!StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int userResult = userService.add(userInfo);
System.out.println("添加用户:" + userResult);
LogInfo logInfo = new LogInfo();
logInfo.setName("添加用户");
logInfo.setDesc("添加用户结果:" + userResult);
int logResult = logService.add(logInfo);
return userResult;
}
结果:整体都回滚了
UserController 类中的代码不变,将添加⽤户和添加⽇志的⽅法修改为 REQUIRES_NEW
不⽀持当前事务,重新创建事务
结果:userinfo不受影响,loginfo插入失败
结果:都添加成功
方法调用流程:
controller/add -> 用户添加方法 -> 日志添加方法
**结果: **
当日志添加方法出现异常之后,嵌套事务的执行结果是:
方法流程:
controller/add -> 用户添加方法 -> 日志添加方法
结果:
当日志添加方法出现异常之后,加入事务的执行结果是: