MyBatis3源码深度解析(七)JDBC单连接事务

文章目录

    • 前言
    • 2.7 JDBC单连接事务
      • 2.7.1 事务的开启与提交
      • 2.7.2 事务隔离级别
        • 2.7.2.1 并发访问问题
          • (1)脏读
          • (2)不可重复读
          • (3)幻读
        • 2.7.2.2 事务隔离级别
          • (1)TRANSACTION_NONE:不支持事务
          • (2)TRANSACTION_READ_UNCOMMITTED:读未提交
          • (3)TRANSACTION_READ_COMMITTED:读提交
          • (4)TRANSACTION_REPEATABLE_READ:可重复读
          • (5)TRANSACTION_SERIALIZABLE:串行化
      • 2.7.3 事务中的保存点
    • 2.8 小结

前言

DatabaseMetaData接口中有一个supportsTransactions()方法,用于判断当前数据源是否支持事务。

事务用于提供数据完整性、正确的应用程序语义和并发访问的数据一致性。所有遵循JDBC规范的驱动程序都需要提供事务支持。

本节研究JDBC中的单连接事务。

2.7 JDBC单连接事务

2.7.1 事务的开启与提交

在JDBC API中,没有对应的方法显式地开启事务,因此何时开启一个新的事务是由JDBC驱动程序或数据库隐式决定的

通常情况下,当SQL语句需要开启事务但目前还没有事务时,会自动地开启一个新的事务。

对于什么时候提交或回滚事务,Connection接口提供了setAutoCommit(boolean autoCommit)commit()rollback()等方法来进行控制。

  1. setAutoCommit(boolean autoCommit)方法用于设置事务是否自动提交,默认情况下事务自动提交是开启的,每个SQL语句执行完毕后会自动地提交事务。
  2. 如果使用setAutoCommit(boolean autoCommit)方法禁用了事务的自动提交,则需要显式地调用commit()方法提交事务,或者调用rollback()方法回滚事务。

禁用事务自动提交一般适用于需要将多个SQL语句作为一个事务提交或者事务由应用服务器管理的情况。

2.7.2 事务隔离级别

事务隔离级别用于表示事务中对数据的操作对其他事务的“可见性”,主要用于解决数据并发访问中的可能会出现的问题,且会直接影响到并发访问的效率。

Connection接口中提供了一个setTransactionIsolation(int level)方法,用于设置当前驱动程序的事务隔离级别。

2.7.2.1 并发访问问题
(1)脏读

脏读是指在一个事务中读取到另一个事务中未提交的数据。例如,A事务修改了一条数据,但是未提交修改,此时A事务对数据的修改对其他事务是可见的,因此B事务中能够读取A事务未提交的修改。一旦A事务回滚,B事务中读取的就是不正确的数据。

下面用一个简单例子来解释。

在数据库中插入一条数据:

在数据库中插入一条数据
编写A事务和B事务的测试代码:

@Test
public void testA() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交(下文解释)connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("A事务读取的用户信息:" + new User(resultSet).toString());// 修改这条记录resultSet.updateString("name", "孙悟空");resultSet.updateRow();System.out.println("A事务修改后的用户信息:" + new User(resultSet).toString());// 此处打一个断点 ...// 回滚事务connection.rollback();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}@Test
public void testB() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("B事务读取的用户信息:" + new User(resultSet).toString());// 此处打一个断点 ...// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}

下面开始模拟脏读产生的过程:

  1. 以Debug方式执行A事务中的查询记录、修改记录操作,停到断点处,控制台打印修改前和修改后的用户数据;

  1. 以Debug方式执行B事务中的查询记录操作,停到断点处,控制台打印查询出来的用户数据,确实是A事务修改后但未提交的用户数据;

  1. 继续执行A事务,回滚修改操作,但B事务已经拿到了A事务修改后的用户数据,如果B事务对修改后的数据进一步处理,就是不符合要求的,这就产生脏读。
(2)不可重复读

不可重复读是指在同一个事务中,对于同一份数据的多次读取可能返回不同的结果。例如,A事务读取了一行数据,但此时B事务中修改了该行数据,A事务中再次读取该行数据将得到不同的结果。

下面用一个简单例子来解释。

在数据库中只有一条数据:

编写A事务和B事务的测试代码:

@Test
public void testA() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("A事务首次读取的用户信息:" + new User(resultSet).toString());// 此处打一个断点 ...// 再次读取记录resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("A事务再次读取的用户信息:" + new User(resultSet).toString());// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}@Test
public void testB() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("B事务读取的用户信息:" + new User(resultSet).toString());// 修改这条记录resultSet.updateString("name", "孙悟空");resultSet.updateRow();System.out.println("B事务修改后的用户信息:" + new User(resultSet).toString());// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}

下面开始模拟不可重复读产生的过程:

  1. 以Debug方式执行A事务中的首次查询记录操作,停到断点处,控制台打印首次查询的用户数据;

  1. 直接执行B事务中的查询记录、修改记录操作,控制台打印处理结果,此时数据库记录也已被修改;


  1. 继续执行A事务,再次以相同的SQL查询用户数据,发现查询的数据是修改后的,这就产生了不可重复读的问题。

(3)幻读

幻读发生在多个事务同时读取和修改数据时。例如,当A事务正在读取一系列数据时,B事务可能会插入一些新的数据,然后提交事务。当A事务再次查询相同的记录集时,它可能会发现一些原本不存在的记录,这会导致数据的不一致性,给用户造成幻觉。

幻读和不可重复读的区别在于,不可重复读侧重于已存在数据的更改,而幻读侧重于新增数据的插入。

下面用一个简单例子来解释。

在数据库中只有一条数据:

编写A事务和B事务的测试代码:

@Test
public void testA() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 查询记录集String sql = "select * from user";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);System.out.println("A事务首次读取的用户信息有:");while (resultSet.next()) {System.out.println(new User(resultSet).toString());}System.out.println("-----------------------");// 此处打一个断点 ...// 再次查询记录集resultSet = statement.executeQuery(sql);System.out.println("A事务再次读取的用户信息有:");while (resultSet.next()) {System.out.println(new User(resultSet).toString());}// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {// 关闭资源DbUtils.close(resultSet, statement, connection);}
}@Test
public void testB() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 插入一条记录String sql = "INSERT INTO USER (NAME, age, phone, birthday) VALUES('user1', 18, '18705464523', '2000-02-21 10:24:30');";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);statement.executeUpdate(sql);// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {// 关闭资源DbUtils.close(resultSet, statement, connection);}
}

下面开始模拟幻读产生的过程:

  1. 以Debug方式执行A事务中的首次查询记录集操作,停到断点处,控制台打印首次查询的用户数据;

  1. 直接执行B事务中的插入记录操作,控制台打印处理结果,此时数据库记录也增加了一条;

  1. 继续执行A事务,再次以相同的SQL查询用户数据集,发现查询的数据还包括B事务新增的,这就产生了幻读的问题。

2.7.2.2 事务隔离级别

JDBC遵循SQL:2003规范,定义了5种事务隔离级别:

(1)TRANSACTION_NONE:不支持事务
(2)TRANSACTION_READ_UNCOMMITTED:读未提交

这种事务隔离级别允许某一事务读取另一事务未提交更改的数据,这意味着可能会出现脏读、不可重复读、幻读现象。

【2.7.2.1 并发访问问题】的三个案例均将事务隔离级别设置为“读未提交”,经过实际测试,确实会发生脏读、不可重复读、幻读现象。

(3)TRANSACTION_READ_COMMITTED:读提交

这种事务隔离级别表示在某一事务中进行任何数据的更改,在提交之前对其他事务都是不可见的,这样可以防止脏读,但不能解决不可重复读、幻读问题。

这是MySQL驱动程序默认的事务隔离级别。

下面继续使用【2.7.2.1 并发访问问题】中的三个案例进行测试。

首先手动将事务隔离级别设置为TRANSACTION_READ_COMMITTED读提交,其余代码保持不变。

// 设置事务隔离级别为:读提交
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
  • 测试脏读问题:已解决
  1. 以Debug方式执行A事务中的查询记录、修改记录操作,停到断点处,控制台打印修改前和修改后的用户数据;

  1. 以Debug方式执行B事务中的查询记录操作,停到断点处,控制台打印查询出来的用户数据,确实是原来的数据,而不是A事务修改后但未提交的用户数据

  1. 继续执行A事务,回滚修改操作,但B事务拿到的是A事务修改前的用户数据,符合要求,脏读问题已被解决。
  • 测试不可重复读问题:未解决

  • 测试幻读问题:未解决

(4)TRANSACTION_REPEATABLE_READ:可重复读

这种事务隔离级别表示在某一事务中对同一数据进行多次读取时,可以得到相同的结果,并且其他事务插入数据的操作对该事务不可见,这样可以防止脏读、不可重复读,但不能解决幻读问题;

下面继续使用【2.7.2.1 并发访问问题】中的三个案例进行测试。

首先手动将事务隔离级别设置为TRANSACTION_REPEATABLE_READ可重复读,其余代码保持不变。

// 设置事务隔离级别为:可重复读
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
  • 测试脏读问题:已解决

  • 测试不可重复读问题:已解决

  1. 以Debug方式执行A事务中的首次查询记录操作,停到断点处,控制台打印首次查询的用户数据;

  1. 直接执行B事务中的查询记录、修改记录操作,控制台打印处理结果,此时记录已被修改;

  1. 继续执行A事务,再次以相同的SQL查询用户数据,发现查询的数据是修改前的,这就解决了不可重复读的问题。

  • 测试幻读问题:未解决
(5)TRANSACTION_SERIALIZABLE:串行化

这种事务隔离级别是最高的事务隔离级别,保证数据的一致性和完整性,可以防止脏读、不可重复读,、幻读问题,但是并发性较差。

下面继续使用【2.7.2.1 并发访问问题】中的三个案例进行测试。

首先手动将事务隔离级别设置为TRANSACTION_SERIALIZABLE串行化,其余代码保持不变。

// 设置事务隔离级别为:串行化
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
  • 测试脏读问题:已解决

  • 测试不可重复读问题:已解决

  • 测试幻读问题:已解决

  1. 以Debug方式执行A事务中的首次查询记录集操作,停到断点处,控制台打印首次查询的用户数据;

  1. 直接执行B事务中的插入记录操作,发现报错,无法打开新的事务。

  1. 继续执行A事务,再次以相同的SQL查询用户数据集,发现查询的数据是一样的,这就解决了幻读的问题。

  1. A事务提交后,再次执行B事务,发现可以成功执行,说明串行化等级下一次只能打开一个事务。

2.7.3 事务中的保存点

保存点是指通过事务中标记的一个中间点来对事务进行更细粒度的控制,一旦设置保存点,事务就可以归滚到保存点,而不影响保存点之前的操作。

DatabaseMetaData接口提供了supportsSavepoints()方法用于判断JDBC驱动程序是否支持保存点。

Connection接口提供了``setSavepoint()```方法用于在当前事务中设置保存点。如果该方法在事务外中调用,则会在该方法调用处开启一个新的事务。

该方法的返回值是一个Savepoint对象,该对象可作为COnnection接口的rollback()方法的参数,用于回滚到对应的保存点。

示例代码如下:

// ......
// 关闭事务自动提交
connection.setAutoCommit(false);
// 读取一条记录
String sql = "select * from user where id = 1";
statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
resultSet = statement.executeQuery(sql);
resultSet.next();
System.out.println("第一次读取的用户信息:" + new User(resultSet).toString());
// 第一次修改这条记录
resultSet.updateString("name", "孙悟空-修改1");
resultSet.updateRow();
System.out.println("第一次修改后的用户信息:" + new User(resultSet).toString());
// 设置保存点
Savepoint savepoint = connection.setSavepoint();
// 第二次修改这条记录
resultSet.updateString("name", "孙悟空-修改2");
resultSet.updateRow();
System.out.println("第二次修改后的用户信息:" + new User(resultSet).toString());
// 回滚到保存点
connection.rollback(savepoint);
// 再次读取这条记录
// 读取一条记录
resultSet = statement.executeQuery(sql);
resultSet.next();
System.out.println("第二次读取的用户信息:" + new User(resultSet).toString());
// 提交事务
connection.commit();
// ......

控制台打印执行结果:

第一次读取的用户信息:User{id=1, name='黑风怪', age=18, phone='18705464523', birthday=2000-02-21}
第一次修改后的用户信息:User{id=1, name='孙悟空-修改1', age=18, phone='18705464523', birthday=2000-02-21}
第二次修改后的用户信息:User{id=1, name='孙悟空-修改2', age=18, phone='18705464523', birthday=2000-02-21}
第二次读取的用户信息:User{id=1, name='孙悟空-修改1', age=18, phone='18705464523', birthday=2000-02-21}

在示例代码中,依次进行读取记录→第一次修改→设置保存点→第二次修改→回滚到保存点→再次读取记录→提交事务,第二次读取的结果恰好就是第一次修改后的结果,说明确实回滚到了保存点的位置。

保存点创建后,可以被手动释放。Connection接口提供了releaseSavepoint()方法,接收一个Savepoint对象为参数,用于释放保存点。保存点被释放后,如果试图通过rollback()方法回滚到保存点,则会抛出SQLException异常。

事务中创建的保存点在事务提交或回滚之后会自动释放,事务回滚到某一保存点之后,该保存点之后的保存点将会自动释放。

2.8 小结

第2章到此就梳理完毕了,本章的主题是:JDBC规范。回顾一下本章的梳理的内容:

(二)JDBC API简介
(三)Connection
(四)Statement
(五)ResultSet
(六)DatabaseMetaData
(七)JDBC单连接事务

更多内容请查阅分类专栏:MyBatis3源码深度解析

第3章主要梳理:MyBatis常用工具类。主要内容包括:

  • 使用SQL类生成语句;
  • 使用ScriptRunner执行脚本;
  • 使用SqlRunner操作数据库;
  • MetaObject详解;
  • MetaClass详解;
  • ObjectFactory详解;
  • ProxyFactory详解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/526629.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

程序员常见的算法介绍

本文将为您详细讲解程序员常见的算法,以及它们的特点、区别和应用场景。算法是计算机科学的核心概念之一,它们是解决问题和执行任务的方法和步骤。在编程中,算法是实现特定功能的基础。 1. 排序算法 特点 - 用于将一组数据按照特定的…

活力up+

持续学习和接触新鲜事物能够有效激发大脑的神经可塑性,这一特性使得大脑能够在面对新信息、新挑战时灵活调整,构建新的神经连接,强化或弱化已有的神经通路,从而优化大脑的功能和结构。 1.神经可塑性 持续学习和不断接触新鲜事物对…

jupyter notebook 调整深色背景与单元格宽度与自动换行

# 安装jupyter主题 pip install jupyterthemes # 列举主题 jt -l # 设置主题 jt -t chesterish设置宽度 打开users 当前用户目录下的custom.css文件 写入.container { width:80% !important; } 即可 设置自动换行 查找创建这个目录以及文件notebook.json 写入配置 “li…

4 个最佳 Windows 文件恢复软件

如何从 Windows 硬盘恢复丢失的数据?您是否获得了最好的 Windows 文件恢复软件?在这里,我们测试并找到最好的 4 个 Windows文件恢复软件,让您轻松快速地从 Windows 硬盘驱动器或任何其他存储设备恢复丢失的数据。 4 个最佳 Windo…

ARM中多寄存内存访问概念和栈的种类和应用

多寄存器内存访问指令 多寄存器内存访问指令 MOV R1,#1 MOV R2,#2 MOV R3,#3 MOV R4,#4 MOV R11,#0x40000020 STM R11,{R1-R4} 将R1-R4寄存器中的数据存储到内存以R11为起始地址的内存中 LDM R11,{R6-R9} 将内存中以R11为起始地址的数据读取到R6-R9寄存器中 当寄存器…

基于决策树实现葡萄酒分类

基于决策树实现葡萄酒分类 将葡萄酒数据集拆分成训练集和测试集,搭建tree_1和tree_2两个决策树模型,tree_1使用信息增益作为特征选择指标,B树使用基尼指数作为特征选择指标,各自对训练集进行训练,然后分别对训练集和测…

linux驱动——中断

1.Cortex-A系列的中断的简介 中断的基本概念:(interrupt) 中断本质上是系统内部的异常机制,当中断产生之后,他会停下当前正在执行的任务,转而去做其他的事情,在停下当前正在执行的任务之前,要先入栈(保护现场,其他的事情做完之后…

基于GAN对抗网进行图像修复

一、简介 使用PyTorch实现的生成对抗网络(GAN)模型,包括编码器(Encoder)、解码器(Decoder)、生成器(ResnetGenerator)和判别器(Discriminator)。…

【vue.js】文档解读【day 4】 | 事件处理

如果阅读有疑问的话,欢迎评论或私信!! 文章目录 事件处理前言监听事件内联事件处理器方法事件处理器方法与内联事件判断在内联处理器中调用方法在内联事件处理器中访问事件参数修饰符事件修饰符按键修饰符常规按键别名系统按键别名组合按键ex…

Nomic Embed:能够复现的SOTA开源嵌入模型

Nomic-embed-text是2月份刚发布的,并且是一个完全开源的英文文本嵌入模型,上下文长度为8192。它在处理短文和长文本任务方面都超越了现有的模型,如OpenAI的Ada-002和text-embedding-3-small。该模型有137M个参数在现在可以算是非常小的模型了…

[LeetCode][LCR151]彩灯装饰记录 III——队列

题目 LCR 151. 彩灯装饰记录 III 一棵圣诞树记作根节点为 root 的二叉树,节点值为该位置装饰彩灯的颜色编号。请按照如下规则记录彩灯装饰结果: 第一层按照从左到右的顺序记录除第一层外每一层的记录顺序均与上一层相反。即第一层为从左到右&#xff0c…

鸿蒙报错:Hhvigor Update the SDKs by going to Tools > SDK Manager....

鸿蒙报错:Hhvigor Update the SDKs by going to Tools > SDK Manager… 打开setting里面的sdk,将API9工程下的全部勾上,应用下载 刚打开 js 和 Native 是没勾上的