《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();
            }
        }
    }
}
相关推荐
实名上网宋凯宣7 小时前
Django
后端·python·django
利威尔xu9 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
逃逸线LOF10 小时前
Ribbon负载均衡原理(Irule组件、源码分析)
后端·spring cloud·ribbon
慢云智慧空间10 小时前
从设备智能到空间理解,空间智能终端解决方案商如何重新定义行业标准
后端
利威尔xu10 小时前
Go IM 源码实战(13):30 个会话只有 2 个红点,未读数错在三条路径
redis·后端·微服务·面试·golang
薛定谔的猫-菜鸟程序员11 小时前
软件架构设计师开发资料大全(从入门到精通)
后端·python·系统架构·flask·架构设计师
云创智城-yuncitys11 小时前
YunParking 4.0:JDK 17 与 Spring Boot 3.2 的停车云升级
java·开发语言·spring boot
IT_陈寒12 小时前
SpringBoot多环境配置把我坑惨了,这个踩坑经验你最好看看
前端·人工智能·后端
码事漫谈13 小时前
DeepSeek 搞了个 "Super-agent",但不是超级智能
后端
吴凡wudianmei13 小时前
从 0 搭一个 AI Agent(四):SSE 跑通之后,三处“契约错配”与它们的修复边界
前端·后端·aigc