《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 JDBC 事务管理 阅读笔记 33
12.3 JDBC 事务管理
Dao 层编程中一个绕不过的重点就是事务。试想如果一组数据库操作没有事务支撑,那么当其中某一条 SQL 语句出现异常时无法回滚之前的操作,业务数据也就没有正确性可言,由此可见事务的重要性。先回顾与事务相关的概念和原生 JDBC 中操作事务的方式,之后再学习 Spring Framework 提供的事务管理和控制机制。
12.3.1 事务回顾
对于事务的概念想必读者都很清楚,简单地说,事务就是一组逻辑操作的组合,它们的执行结果要么全部成功,要么全部失败。JDBC 中的事务有以下 4 个特性。
- 原子性。一个事务就是一个不可再分解的单位,事务中的操作要么全部执行,要么全部不执行,原子性强调的是事务的整体。
- 一致性。事务执行后,所有的数据都应该保持一致的状态,一致性强调的是数据的完整性。
- 隔离性。多个数据库操作并发执行时,多个并发事务执行之间要相互隔离,隔离性强调的是并发的隔离。
- 持久性。事务执行完成后,它对数据的影响是永久性的,持久性强调的是操作的结果。
针对数据库的并发操作,可能会出现一些事务并发问题。事务并发操作中会出现以下 3 种问题。
- 脏读。一个事务读到了另一个事务没有提交的数据。
- 不可重复读。一个事务读到了另一个事务已提交修改的数据。对同一行数据查询两次,结果不一致。
- 幻读。一个事务读到了另一个事务已提交的新增数据。对同一张表查询两次,前后出现新增的行,导致结果不一致。
针对上述3个问题,可以引出事务的4种隔离级别。
- read uncommitted(读未提交)---不解决任何问题。
- read committed(读已提交)---解决脏读。
- repeatable read(可重复读)---解决脏读、不可重复读。
- serializable(可串行化)---解决脏读、不可重复读、幻读。
上述 4 种隔离级别自上而下逐级增高,但并发性能逐级降低。MySQL 中默认的事务隔离级别是 repeatable read 可重复读,Oracle、PostgreSQL 中默认的事务隔离级别是 read committed 读已提交。
12.3.2 原生 JDBC 事务
1.JDBC 事务的基本使用
代码提供了一个非常简单且容易理解的原生 JDBC 事务使用,这是一个很简单的两次数据库请求,中间夹带了一个异常的构造,由于出现除零的算术异常,代码的运行结果是抛出异常,事务回滚。对于原生 JDBC 的事务操作,在开启事务时需要调用 Connection 的 setAutoCommit 方法来关闭自动提交(开启事务),在数据库操作执行完毕后需要手动调用 Connection 的 commit 方法提交事务,如果这期间操作出现异常,也要手动调用 Connection 的 rollback 方法回滚事务。

java
package com.yangjunbo.springboot.jdbc.examplec;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JdbcTransactionApplication {
public static void main(String[] args) throws SQLException {
// 构造数据源
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://192.168.19.201:3306/spring-dao?characterEncoding=utf8");
dataSource.setUsername("spring-dao");
dataSource.setPassword("yyy123456.");
Connection connection = null;
try {
connection = dataSource.getConnection();
// 开启事务
connection.setAutoCommit(false);
// 执行第一条SQL语句
PreparedStatement statement = connection
.prepareStatement("insert into tbl_user (name, tel) values ('test', '12345')");
statement.executeUpdate();
// 制造一个除零异常
int i = 1 / 0;
// 执行第二条SQL语句
statement = connection.prepareStatement("delete from tbl_user where id = 1");
statement.executeUpdate();
// 提交事务
connection.commit();
} catch (Exception e) {
// 回滚事务
connection.rollback();
} finally {
// 关闭连接
if (connection != null) {
connection.close();
}
}
}
}
2.事务保存点
上面的例子很好理解,不过下面要介绍一个读者可能没有接触过的概念:事务保存点。或许在日常开发中会遇到一些特殊的场景,这些场景中的业务逻辑可能需要分段执行,如果出现异常后不需要全部回滚,就可以利用 JDBC 事务中的保存点机制。当程序运行中出现异常时,可以控制事务只回滚到某个保存点处,这样,保存点之前的 SQL 语句仍然会执行。
下面举一个简单的例子,代码中共发送了 3 条 SQL 语句,并且第一条 SQL 语句执行完毕后记录了一个保存点,然后执行剩余两条 SQL 语句。由于执行第二条 SQL 语句之后发生了除零异常,因此需要回滚事务,而由于有保存点的存在,执行 rollback 方法时会将事务回滚到保存点处,之后执行 commit 方法即可提交事务保存点之前的 SQL 语句。

java
package com.yangjunbo.springboot.jdbc.examplec;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Savepoint;
public class TransactionSavepointApplication {
public static void main(String[] args) throws SQLException {
// 构造数据源
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://192.168.19.201:3306/spring-dao?characterEncoding=utf8");
dataSource.setUsername("spring-dao");
dataSource.setPassword("yyy123456.");
Connection connection = null;
Savepoint savepoint = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
// 执行第一条SQL语句
PreparedStatement statement = connection
.prepareStatement("insert into tbl_user (name, tel) values ('test', '12345')");
statement.executeUpdate();
// 记录一个保存点
savepoint = connection.setSavepoint();
// 执行接下来的SQL语句,并构造算术异常
statement = connection.prepareStatement("insert into tbl_account (user_id, money) values (2, 123)");
statement.executeUpdate();
int i = 1 / 0;
statement = connection.prepareStatement("delete from tbl_user where id = 1");
statement.executeUpdate();
connection.commit();
} catch (Exception e) {
if (savepoint != null) {
connection.rollback(savepoint);
connection.commit();
} else {
connection.rollback();
}
} finally {
if (connection != null) {
connection.close();
}
}
}
}