《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 JDBC 事务管理 阅读笔记 33

《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();
            }
        }
    }
}
相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
一隅论数智2 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码2 天前
别再前后端各写一套表单校验了
java·后端
大勇前进2 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
vipxieliang2 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
yuzhi_liu2 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile2 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白802 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端