JDBC 基础(下)事务控制

事务控制.png

本篇文章主要介绍的是 MySQL / JDBC 中的事务,为了方便读者浏览,这里默认需要读者已经掌握 SQL基础 以及 JDBC 数据库连接基础。这部分的基础也可以参考下面的链接进行简单的快速入门。

1.概述

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在银行转账系统中,A -> B 转账 1000 元,这时就需要将 A 账户余额 -1000,对应的 B 账户余额 +1000。这两个操作过程必须同时执行成功才能完成此操作,这样,这些数据库操作语句就构成了一个事务。

  • 在上面的举例中如果 A 的账户余额 -1000 执行完毕后,程序被中断了(抛出异常、服务器宕机等),而 B 账户没有 +1000 元,这肯定是有问题的。
  • 现在对事务应该有一个了解了吧🤔事务中的多个操作,或者全部执行完毕,或者全不执行,不存在只执行了一部分的情况。

2.事务的四大特性(ACID)

  • 原子性(Atomicity):事务中的所有操作是不可再分割的原子单位,事务中的所有操作是一个整体,或者整体执行成功,亦或者整体执行失败。
  • 一致性(Consistency):事务执行后,数据库状态与其他业务规则保持一致。如转账业务,无论执行成功与否,参与转账的两个帐号余额值和应该是不变的。
  • 隔离性(Isolation):在并发操作中,不同事务之间应该隔离开来,每个并发中的事务的执行不会相互干扰。
  • 持久性(Durability):一旦事务提交成功,事务中的所有数据更新必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重新启动时,也必须能保证通过某种机制恢复数据。

3.MySQL 中的事务

在默认情况下,MySQL 每执行一条 SQL 语句,都是一个单独的事务。如果需要在每一个事务中包含多条 SQL 语句的执行,那么就需要开启事务和结束事务。

  • 开启事务:START TRANSACTION
  • 结束事务:COMMITROLLBACK
  • 在执行 SQL 语句之前,先执行 START TRANSACTION ,则代表开启了一个事务,然后执行多条 SQL 语句,最后需要结束事务,COMMIT 表示提交,即事务中的多条 SQL 语句所更改的数据会持久化到数据库中。或者 ROLLBACK 表示回滚,即回滚到事务的起点,将之前所做的所有操作撤销。

Reiminder 💁‍♂️
ROLLBACK 可以结束事务,但不代表会将数据持久化到数据库中,而只有 COMMIT 提交才可以将数据持久化到数据库中。

  • 测试表:
# 创建 Account 表
CREATE TABLE `Account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `balance` decimal(10,0) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `Account_id_uindex` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

# 插入数据
INSERT INTO Account (name, balance) VALUES ('A', 10000);
INSERT INTO Account (name, balance) VALUES ('B', 10000);
INSERT INTO Account (name, balance) VALUES ('C', 10000);

3.1 - COMMIT 测试

# 开启事务
START TRANSACTION;

# 执行事务 SQL 语句
# SQL1
UPDATE Account
SET balance = balance - 1000
WHERE id = 1;
# SQL2
UPDATE Account
SET balance = balance + 1000
WHERE id = 2;

# 提交事务
COMMIT;

# 结果分析
1   A   9000
2   B   11000
3   C   10000

分析:提交事务后,更新的数据将被持久化到数据库中。

3.2 - ROOLBACK 测试

# 开启事务
START TRANSACTION;

# 执行事务 SQL 语句
# SQL1
UPDATE Account
SET balance = balance - 1000
WHERE id = 1;
# SQL2
UPDATE Account
SET balance = balance + 1000
WHERE id = 2;

# 回滚事务
ROLLBACK;
# 提交事务
COMMIT;

# 结果分析
1   A   10000
2   B   10000
3   C   10000

分析:事务提交前执行 ROLLBACK 回滚事务至 START TRANSACTION 时的状态,所以持久化后数据库中数据没有被改变。

3.3 - 事务不提交测试

# 开启事务
START TRANSACTION;

# 执行事务 SQL 语句
# SQL1
UPDATE Account
SET balance = balance - 1000
WHERE id = 1;
# SQL2
UPDATE Account
SET balance = balance + 1000
WHERE id = 2;

# 输出结果
SELECT *
FROM Account;

# 控制台打印数据
1   A   9000
2   B   11000
3   C   10000

# 结果分析(数据库数据)
1   A   10000
2   B   10000
3   C   10000

分析:在执行了 SQL 语句后,在内存中的数据表数据已经被修改了,但是由于没有提交事务,所以数据没有被持久化到数据库中。

4.并发事务问题

  • 脏读(Dirty Read):在事务的执行过程中,读取到了其他事务的 未提交 的数据,即读到了脏数据。
  • 不可重复读(Unrepeatable Read):在事务的执行过程中,读到了其他事务 修改后 的数据,换句话说在该事务中的不同时间点读取到了不一致的数据,即不可重复读。
  • 幻读/虚读(Phantom Read):在事务的执行过程中,读取到了其他事务对 记录数 修改后的数据,对同一张表的两次查询的 COUNT(*) 不一致。
  • 不可重复读与幻读的区别:
    • 不可重复读:强调的是数据 内容 的不一致,主要针对 UPDATE 的修改。
    • 幻读:强调的是 记录数 的不一致,主要针对 INSERT/DELETE 的修改。

5.四大隔离级别

刚刚我们介绍了事务并发时可能出现的各种问题,其实可以发现是违背了事务的 隔离性 的要求所引起的,所以我们需要通过事务的隔离来解决这个问题,下面我们就来介绍一下事务的四大隔离级别。

  • 串行化(SERIALIZABLE):
    • 概述:对数据串行的访问,非并发访问。
    • 特点:不会出现任何并发问题,性能最差。
    • 理解:在当前串行化事务中,如果有其他事务对数据进行了增删改操作,当前事务读取数据会被阻塞,需要等到其他事务结束后(ROLLBACK/COMMIT)才能执行数据读取。
  • 可重复读(REPEATABLE READ):
    • 概述:在一个事务的执行过程中,能保证读取到数据的一致性,是 MySQL 中使用的 InnoDB 存储引擎默认的隔离级别。
    • 特点:可避免脏读和不可重复读,不能避免幻读问题,并发式读取访问,性能比串行化好。
    • 理解:在一个事务内,锁定读取,通过保存第一次读取的快照(snapshot),保证每次读取的数据一致。
  • 读已提交(READ COMMITTED):
    • 概述:在一个事务内,可以读取到其他事务已经提交的数据,性能优于可重复读(REPEATABLE READ),Oracle 数据库中的默认隔离级别。
    • 特点:可避免脏读,不能避免不可重复读和幻读问题,并发式访问读取,性能比可重复读好。
    • 理解:MySQL中与 Oracle 的该隔离级别,通过读取新鲜的快照(fresh snapshot)来读取其他事务已提交的更新内容。
  • 读未提交(READ UNCOMMITTED):
    • 概述:在一个事务内,可以读取到其他事务没有提交的修改内容,即脏读(Dirty Read)。
    • 特点:不能避免任何并发事务的问题,性能最好。
    • 理解:在 SERIALIZBLE 的事务隔离级别,InnoDB 存储引擎会对每个 SELECT 语句后自动加上 LOCK IN SHARE MODE,即给每个读取操作加一个共享锁,因此在这个事务隔离级别下,读占用锁了,一致性的非锁定读不再予以支持,一般不会在本地事务中使用 SERIALIZBLE 的隔离级别,SERIALIZABLE 的事务隔离级别主要用于 InnoDB 存储引擎的分布式事务。
隔离级别 脏读 不可重复读 幻读
串行化(SERIALIZABLE) ✔️ ✔️ ✔️
可重复读(REPEATABLE READ) ✔️ ✔️ -
读已提交(READ COMMITTED) ✔️
读未提交(READ UNCOMMITTED)

6.MySQL 各隔离级别的并发事务测试

  • 查看隔离级别:MySQL 默认隔离级别是 REPEATABLE-READ,可以通过 SELECT @@TX_ISOLATION; 查看隔离级别。

  • 设置隔离级别:SET SESSION TRANSACTION ISOLATION LEVEL xxx;

  • 测试表及数据

id name balance
1 A 10000
2 B 10000

6.1 - 串行化测试

  • 测试版本:MySQL Server 5.7
  • 测试环境:
# 设置窗口 2 隔离级别为 串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 特别的 -> 窗口 1 的隔离级别不需要特别设置。
# 我们演示是通过窗口 1 进行修改数据值,在窗口 2 来观察结果的。
串行化测试

值得注意💡在第 3 步,窗口 1 执行 INSERT 插入了一条数据,而后第 4 步窗口 2 执行 SELECT 操作会被阻塞(避免幻读),直到窗口 1 事务结束(COLLBACK/COMMIT)后才会被执行。
特别的💡当窗口 2 一旦执行过 SELECT 操作后,如果有其他事务对数据进行增删改操作都将被阻塞(可重复读的保证),直到该串行化事务结束后才会被执行。

6.2 - 可重复读测试

可重复读测试

与串行化类似的是,当窗口 2 执行步骤 3 读操作后,查询的结果将被锁定。当其他事务要对该锁定数据执行更改操作时都将会被阻塞,所以当窗口 1 执行步骤 4 时将会被阻塞,从而保证了可重复读。

6.3 - 读已提交测试

读已提交测试

当步骤 4 修改了 balance 值时,此时还未提交,所以步骤 5 查询到的结果并没有改变(读已提交),而在步骤 7 查询到了窗口 1 改变的结果,因为此时窗口 1 的事务已经提交。
特别的💡在窗口 2 事务的执行过程中,步骤 3 与步骤 7 查询到了不同的结果,由此可以看出这是与可重复读的重要区别。

6.4 - 读未提交测试

读未提交测试

步骤 4 中窗口 1 事务修改了数据,步骤 5 中窗口 2 事务读取到了修改后的数据,此时窗口 1 事务还未提交,因此读取到的是 脏数据,该隔离级别不能避免任何的并发事务问题。

7.JDBC 事务

刚刚我们介绍了在 MySQL 中对事务进行的操作,而 JDBC 中 也必然有与对应的方式进行事务控制,下面我们介绍一下 JDBC 中对事务的控制。

  • 在 JDBC 中处理事务都是通过 Connection 完成的。
  • 同一个事务中的所有的操作,都是使用同一个 Connection 对象。

7.1 - 开启事务

  • 方法:void setAutoCommit(boolean autoCommit)

读读 API 📖
If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either the method commit or the method rollback. By default, new connections are in auto-commit mode.

  • 如果 connection 处于自动提交模式,会将每一条 SQL 语句作为一个单独的事务提交(commit);否则,其 SQL 语句可以通过调用 commit() 方法或 rollback() 方法终止事务。默认是自动提交模式。

Reminder 💁‍♂️
Java 还特别指出:对于 DML 语句,例如插入、 更新或删除和 DDL 语句,该语句是完整的尽快它执行完。
Select 语句,该语句完成时关闭关联的 ResultSet。

7.2 - 提交事务

  • 方法:commit()

读读 API 📖
Makes all changes made since the previous commit/rollback permanent and releases any database locks currently held by this Connection object. This method should be used only when auto-commit mode has been disabled.

  • 提交自上次提交后的所有更改,并释放目前此连接对象的任何 数据库锁
  • 只有当禁用了自动提交时此方法有效。

7.3 - 回滚事务

  • 方法:rollback()

读读 API 📖
Undoes all changes made in the current transaction and releases any database locks currently held by this Connection object. This method should be used only when auto-commit mode has been disabled.

  • 撤销对当前事务中所做的所有更改,并释放目前此连接对象持有的任何 数据库锁
  • 只有当禁用了自动提交时此方法有效。

7.4 - 设置保存点

  • 方法:Savepoint setSavepoint(String name)

读读 API 📖
Creates a savepoint with the given name in the current transaction and returns the new Savepoint object that represents it.
if setSavepoint is invoked outside of an active transaction, a transaction will be started at this newly created savepoint.

  • 在当前事务中创建一个指定名称的保存点,并返回一个用来表示它的新的保存点对象。
  • 如果该方法在一个事务外被调用时,将在这个新创建的保存点启动事务。

7.5 - 事务回滚

  • 不带保存点的 JDBC 事务的基本格式:
try {
    connection.setAutoCommit(false);    // 禁用自动提交
    ...
    ...
    connection.commit();    // 在 try 的末尾提交
} catch() {
    connection.rollback();  // 事务执行中断则回滚
}
  • 代码示例:
public static void transfer(boolean b) throws Throwable {
    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = JdbcUtils.getConnection();
        // 禁用自动提交
        connection.setAutoCommit(false);

        String sql = "UPDATE Account SET balance = balance + ? WHERE id = ?";
        preparedStatement = connection.prepareStatement(sql);

        // 操作 1
        preparedStatement.setDouble(1, -10000);
        preparedStatement.setInt(2, 1);
        preparedStatement.executeUpdate();

        // 在事务的两个操作中抛出异常,中断事务内务的执行
        if (b) {
            throw new Exception();
        }

        // 操作 2
        preparedStatement.setDouble(1, 10000);
        preparedStatement.setInt(2, 2);
        preparedStatement.executeUpdate();

        // 提交事务
        connection.commit();
    } catch (Exception e) {
        try {
            if (connection != null) {
                connection.rollback();
            }
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
        throw new RuntimeException();
    } finally {
        JdbcUtils.release(connection, preparedStatement);
    }
}

7.6 - 回滚到保存点

  • 概述:保存点(savePoint) 是 JDBC 3.0 的 API,其要求数据库支持以保存点方式的的回滚。
  • 检查方法:boolean b = connection.getMetaData().supportsSavepoints();
  • 回滚到保存点方法:void rollback(Savepoint savepoint)
  • 作用:保存点的作用是将事务回滚到指定的保存点。需要在事务中先设置好保存点,然后回滚时通过 Savepoint 回滚到指定的保存点,而不是回滚整个事务。

Reminder 💁‍♂️
回滚到指定的保存点并没有结束事务,只有回滚了整个事务才会结束事务。

  • 代码示例:
    /*
     * 李四对张三说,如果你给我转1W,我就给你转100W。
     * ==========================================
     * 
     * 张三给李四转1W(张三减去1W,李四加上1W)
     * 设置保存点!
     * 李四给张三转100W(李四减去100W,张三加上100W)
     * 查看李四余额为负数,那么回滚到保存点。
     * 提交事务
     */
private static void savepoint() throws RuntimeException {
    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = JdbcUtils.getConnection();
        // 禁用自动提交
        connection.setAutoCommit(false);

        String sql = "UPDATE Account SET balance = balance + ? WHERE name = ?";
        preparedStatement = connection.prepareStatement(sql);

        // 操作1(张三减去1W)
        preparedStatement.setDouble(1, -10000);
        preparedStatement.setString(2, "zs");
        preparedStatement.executeUpdate();

        // 操作2(李四加上1W)
        preparedStatement.setDouble(1, 10000);
        preparedStatement.setString(2, "ls");
        preparedStatement.executeUpdate();

        // 设置表存点
        Savepoint savepoint = connection.setSavepoint();

        // 操作3(李四减去100W)
        preparedStatement.setDouble(1, -1000000);
        preparedStatement.setString(2, "ls");
        preparedStatement.executeUpdate();

        // 操作4(张三加上100W)
        preparedStatement.setDouble(1, 1000000);
        preparedStatement.setString(2, "zs");
        preparedStatement.executeUpdate();

        // 操作5(查看李四余额)
        sql = "SELECT balance FROM Account WHERE name = ?";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, "ls");
        ResultSet resultSet = preparedStatement.executeQuery();
        double balance = 0;
        if (resultSet.next()) {
            balance = resultSet.getDouble("balance");
        }

        // 如果李四的余额为负数,那么回滚到指定保存点
        if (balance < 0) {
            connection.rollback(savepoint);
            System.out.println("张三你上当了");
        }

        // 提交事务
        connection.commit();

    } catch (SQLException e) {
        // 回滚事务
        if (connection != null) {
            try {
                connection.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        throw new RuntimeException();
    } finally {
        JdbcUtils.release(connection, preparedStatement);
    }
}

悄悄话 🌈

  • 年后的学习节奏变得非常之快,导致最近也很久没有与大家分享技术笔记了,最近学习了 JavaWeb 的 HTML、CSS、JS、MySQL、Tomcat、Servlet、JSP 等等内容,哪一个技术拿出来都应该可以让我来研究一阵子了,怎奈进度太快也只能是抓大放小。最近在数据库阶段的事务控制的部分我比较感兴趣并做了一些小实验,觉得有一些意义,所以来与大家分享一下。
  • 后面的 Cookie、Session 技术也是我觉得理解的比较深入的一个技术点,我会留在下次的更新中进行分享。

彩蛋 🐣

  • 最近在整理一些 JavaWeb 成长之路 的一些学习笔记,本篇是 Database 系列中的一篇,今后还会与大家分享 JavaWeb 中的一系列的技术,有兴趣的朋友可以关注我的专题,一同学习。

如果你觉得我的分享对你有帮助的话,请在下面👇随手点个喜欢 💖,你的肯定才是我最大的动力,感谢。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容