JavaWeb 成长之路Android开发程序员

JDBC 基础(下)事务控制

2017-02-24  本文已影响645人  SawyerZh
事务控制.png

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

1.概述

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

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

3.MySQL 中的事务

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

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.并发事务问题

5.四大隔离级别

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

隔离级别 脏读 不可重复读 幻读
串行化(SERIALIZABLE) ✔️ ✔️ ✔️
可重复读(REPEATABLE READ) ✔️ ✔️ -
读已提交(READ COMMITTED) ✔️
读未提交(READ UNCOMMITTED)

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

id name balance
1 A 10000
2 B 10000

6.1 - 串行化测试

# 设置窗口 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 中对事务的控制。

7.1 - 开启事务

读读 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.

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

7.2 - 提交事务

读读 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 - 回滚事务

读读 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 - 设置保存点

读读 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 - 事务回滚

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 - 回滚到保存点

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);
    }
}

悄悄话 🌈


彩蛋 🐣

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


上一篇 下一篇

猜你喜欢

热点阅读