目录
一、什么是事务?
事务是访问并可能更新数据库中数据的一个执行单元,这个单元是由一条或多条语句组成,单元里的语句是相互依赖的。
这个单元里的SQL语句一起向系统提交,提交后要么都执行成功,要么都失败。例如:执行到一条SQL语句发生了报错,那么这个单元里已经执行成功的SQL语句也需要回滚操作,也就是返回初始状态
二、事务的四个特征(ACID)【面试常考项】
原子性(Atomicity)
原子是物理当中一种不可分割最小的单位。事务的原子性指事务是一个不可分割的最小执行单位(执行数据库操作)
一致性(Consistency)
事务必须使数据库从一个一致状态变到另一个一致状态。意味着事务完成后,所有的数据都符合预定的规则和约束。例如:转账的例子,张三给李四转账 100 元,张三的账户减去 100 元,李四的账户加 100 元,一致性就是其他事务看到的是 张三没有给李四转账,钱没有变化,张三给李四转账,张三的账户减 100 ,李四的账户加 100,不会出现 张三的账户减了 100,李四的账户没有加。
隔离性(Isolation)
隔离性意味着多个并发事务之间互不影响。即使多个事务同时进行,每一个事务都感觉像是系统中唯一的事务。也就是说,一个事务在读取数据时,不会看到其他未提交事务的更改,这避免了脏读、不可重复读和幻读等问题。
持久性(Durability)
当一个事务成功提交,数据的修改是永久性的,也就是会把数据写到物理存储中保存下来
三、MYSQL操作事务
MYSQL默认是开启事务,且一条 SQL 语句就会生成一个事务去执行,这个事务是自动提交的
1. 查看是否开启自动提交
0 表示关闭了自动提交
1 表示开启了自动提交
SELECT @@autocommit
2. 设置事务自动提交
SET @@autocommit = 0 // 关闭自动提交事务
SET @@autocommit = 1 // 开启自动提交事务
3. 手动提交
commit 手动提交
4. 开启一条事务
5. 回滚
start transaction; 单独开启一条事务
rollback; 回滚操作
遇到报错后进行回滚
这样数据不会被修改
四、事务的并发问题
为了避免事务的并发问题,数据库使用不同的事务隔离级别。
1. 隔离级别
在MySQL中,事务有4种隔离级别,分别为READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ(可重复读)和SERIALIZABLE(串行化)。
隔离级别 | 问题 |
---|---|
READ UNCOMMITTED(读未提交) | 脏读 |
READ COMMITTED(读已提交) | 允许不可重复读 |
REPEATABLE READ(可重复读) | 允许幻读 |
SERIALIZABLE(串行化) | 串行化读,事务只能一个一个执行 |
例如,在“读已提交”(Read Committed)级别,一个事务只能读取已经提交的数据,这就防止了脏读的发生。
为了更直观的感受什么是脏读,不可重复读,幻读和串行化读,我们使用 CMD 命令打开两个窗口来进行演示
2. 脏读
事务A 读取了事务B还没有提交的数据,然后事务B 又进行了回滚操作,这时,事务A读取的数据就是脏数据。
1. 打开数据库
窗口A,窗口B 都连接数据库,然后切换你自己的数据库
2. 切换窗口B的隔离级别
MySQL默认隔离级别是REPEATABLE READ,该级别可以避免脏读。为了方便演示,把隔离级别修改为 READ UNCOMMITTED(读未提交)
// 查看隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
// 修改隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3. 演示脏读
在窗口A 开启事务,修改张三的 money
注意:先不要提交事务
在窗口 B 查看信息
可以看到,在窗口B可以看到还未提交的数据
脏读演示好了,可以在窗口A 使用 roolback;命令回滚事务
那怎么解决脏读呢?使用 READ COMMITTED(读已提交)隔离级别可以避免脏读
4. 重新设置窗口B的隔离级别
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
mysql>
5. 再次演示脏读
先在窗口B查询一下数据
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 900 |
| 2 | lisi | 123 | 200 |
+---------+-----------+----------+-------+
2 rows in set (0.00 sec)
mysql>
在窗口 A 修改张三的 money
开启一个事务
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set money = 1000 where user_id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 1000 |
| 2 | lisi | 123 | 200 |
+---------+-----------+----------+-------+
2 rows in set (0.00 sec)
mysql>
然后在窗口 B 可以看到,没有查询到 窗口 A 未提交的数据
说明 READ COMMITTED(读已提交)隔离级别可以解决脏读的问题
演示完毕,使用 rollback;命令回滚
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 900 |
| 2 | lisi | 123 | 200 |
+---------+-----------+----------+-------+
2 rows in set (0.00 sec)
mysql>
3. 不可重复读
不可重复读是指在访问数据库的数据时,一个事务对同一个数据进行多次读取时,期间其他事务可能对数据进行了更新,所以读取的结果可能不同
这通常发生在事务隔离级别为“读已提交”(Read Committed)时。
1. 切换窗口B 的隔离级别
切换 隔离级别为 READ COMMITTED(读已提交),然后开启事务,查询张三的信息
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where user_id = 1;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 900 |
+---------+-----------+----------+-------+
1 row in set (0.00 sec)
mysql>
在窗口A更新张三的信息
mysql> update user set money = 2000 where user_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 2000 |
| 2 | lisi | 123 | 200 |
+---------+-----------+----------+-------+
2 rows in set (0.00 sec)
mysql>
在窗口B再次查询张三的信息
可以看到,在窗口 B 一个事务的两次查询的结果不一样,其实不可重复读并不算错误,但在有些情况下却不符合实际需求。
不可重复读演示完毕,使用 COMMIT 手动提交事务
mysql> select * from user where user_id = 1;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 2000 |
+---------+-----------+----------+-------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
2. 那怎么解决不可重复读的问题呢?
为了避免不可重复读,可以使用更高一级的事务隔离级别“可重复读”(Repeatable Read)。
在该级别下,一旦事务开始,它会为所有读取操作创建一个快照,这个快照包含了事务开始时的数据状态。
这样一来,即使有其他事务修改并提交了数据,当前事务内的读取操作总是基于开始时的快照,从而保证了数据的重复读取结果一致。
3. 重新设置窗口的隔离级别
修改窗口B的隔离级别为 REPEATABLE READ(可重复读)
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql>
4. 再次演示不可重复读
在窗口B开启一个事务,查询张三的信息
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where user_id = 1;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 2000 |
+---------+-----------+----------+-------+
1 row in set (0.00 sec)
mysql>
在窗口A 修改张三的信息
mysql> update user set money = 3000 where user_id = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 3000 |
| 2 | lisi | 123 | 200 |
+---------+-----------+----------+-------+
2 rows in set (0.00 sec)
mysql>
再次在窗口 B 查询张三的信息
可以看到,在可重复读的隔离级别下,其他事务对数据的更新不会影响这个事务的读取
演示完毕,使用 commit; 提交事务
mysql> select * from user where user_id = 1;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 2000 |
+---------+-----------+----------+-------+
1 row in set (0.00 sec)
mysql>
4. 幻读
指同一个事务两次读取的数据不一致,对于这个事务来说,突然多出几条数据,就好像出现了幻觉。 这是其他事务对数据进行了插入或者删除导致的
注意:可重复读 隔离级别也会发生幻读这个问题,但是一般在多个事务并发的时候才可能会出现,这里为了方便演示,设置 隔离级别为 读已提交
1. 演示幻读
在窗口 B 开启一个事务,查询数据表的所有数据
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 3000 |
| 2 | lisi | 123 | 200 |
+---------+-----------+----------+-------+
2 rows in set (0.00 sec)
mysql>
在窗口A 新增一条数据
mysql> insert into user value('3','test','123',1000);
Query OK, 1 row affected (0.02 sec)
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 3000 |
| 2 | lisi | 123 | 200 |
| 3 | test | 123 | 1000 |
+---------+-----------+----------+-------+
3 rows in set (0.00 sec)
mysql>
在窗口B 再次查询表中所有数据
可以看到,表中也显示了新增的数据
mysql> select * from user;
+---------+-----------+----------+-------+
| user_id | user_name | user_pwd | money |
+---------+-----------+----------+-------+
| 1 | zhangsan | 123 | 3000 |
| 2 | lisi | 123 | 200 |
| 3 | test | 123 | 1000 |
+---------+-----------+----------+-------+
3 rows in set (0.00 sec)
mysql>
五、不可重复读和幻读的区别
不可重复读和幻读的本质上是一样的,两次读取到的数据不一致,但是 不可重复读 是两次读取的同一条记录的结果不一致,幻读是两次读取表中的记录数量不一致
不可重复读重点在于 UPDATE 和 DELETE,而幻读的重点在于 INSERT
六、Mybatis-Spring 配置事务
Sping 声明式事务管理,本次使用的是 Mybatis-Spring 环境
1. 环境配置
1. 使用的部分依赖,和版本
<!-- MyBatis与Spring的集成库,用于简化MyBatis在Spring环境中的使用 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.2</version>
</dependency>
<!-- Spring的事务管理器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.30</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.23</version>
</dependency>
<!-- Spring的JDBC抽象层,提供统一的数据库访问API,隐藏了数据库连接的细节 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.30</version>
</dependency>
<!-- Druid是一个数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.30</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId> <!--Spring 的ioc容器-->
<version>5.3.30</version>
</dependency>
2. Mybatis-Spring 的配置文件
<!--配置数据源,druid-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/"/>
<property name="username" value="root"/>
<property name="password" value="sql123"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager" />
3. Spring 的配置文件
<!--开启包扫描路径-->
<context:component-scan base-package="com"/>
<!--开启自动代理-->
<aop:aspectj-autoproxy/>
4. 开启事务管理
创建配置类,使用 @EnableTransactionManagement 注解开启 Spring 声明式事务管理
2. 使用
@Transactional 的实现依赖于Spring AOP(面向切面编程)。当一个被@Transactional注解的方法被调用时,Spring AOP会在方法执行前后插入代理逻辑来管理事务
在 Service 层的方法上 使用 @Transactional 注解
3. 事务的失效情况
try {
//你的代码
} catch (Exception e) {
// 在捕获事务的异常后,如果对异常进行了处理,那么事务就会失效,不会回滚
// 解决方案是 抛出异常,让上一级处理
throw new RuntimeException(e);
}