教程中心 / Java教程 / 事务处理

JDBC事务处理

18分钟 数据库编程

引言:什么是事务?

事务(Transaction)是数据库操作的基本单位,一个事务包含一个或多个SQL操作,这些操作要么全部成功,要么全部失败

事务类比:银行转账

银行转账场景:
张三转100元给李四

操作1:从张三账户扣除100元
操作2:向李四账户增加100元

如果没有事务:
- 操作1成功(张三-100)
- 操作2失败(李四没收到)
- 张三丢了100元!

有事务:
- 开启事务
- 操作1成功
- 操作2成功
- 提交事务
- 如果任一失败,全部回滚

事务的ACID特性

特性英文说明
原子性Atomicity事务是最小执行单位,不可分割
一致性Consistency事务执行前后,数据库状态保持一致
隔离性Isolation并发执行的事务相互隔离,不互相干扰
持久性Durability事务提交后,结果永久保存

JDBC事务基本操作

开启事务

// 默认情况下,JDBC自动提交每条SQL
// 开启事务需要关闭自动提交
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);  // 关闭自动提交,开启事务

提交事务

try {
    // 执行多个SQL操作
    PreparedStatement pstmt1 = conn.prepareStatement("UPDATE account SET balance = balance - ? WHERE id = ?");
    pstmt1.setDouble(1, 100);
    pstmt1.setInt(2, 1);
    pstmt1.executeUpdate();

    PreparedStatement pstmt2 = conn.prepareStatement("UPDATE account SET balance = balance + ? WHERE id = ?");
    pstmt2.setDouble(1, 100);
    pstmt2.setInt(2, 2);
    pstmt2.executeUpdate();

    // 所有操作成功,提交事务
    conn.commit();
    System.out.println("转账成功!");
} catch (Exception e) {
    // 发生异常,回滚事务
    conn.rollback();
    System.out.println("转账失败,已回滚");
} finally {
    // 恢复自动提交
    conn.setAutoCommit(true);
    conn.close();
}

回滚事务

// 当发生异常时,回滚到事务开始前的状态
try {
    conn.setAutoCommit(false);

    // 执行SQL操作
    // ...

    conn.commit();
} catch (Exception e) {
    System.out.println("发生异常,回滚事务");
    conn.rollback();  // 撤销所有操作
} finally {
    conn.setAutoCommit(true);
}

完整转账示例

public void transferMoney(Connection conn, int fromId, int toId, double amount) {
    PreparedStatement pstmt1 = null;
    PreparedStatement pstmt2 = null;

    try {
        conn.setAutoCommit(false);  // 开启事务

        // 扣除转出账户金额
        String sql1 = "UPDATE accounts SET balance = balance - ? WHERE id = ?";
        pstmt1 = conn.prepareStatement(sql1);
        pstmt1.setDouble(1, amount);
        pstmt1.setInt(2, fromId);
        int rows1 = pstmt1.executeUpdate();

        if (rows1 == 0) {
            throw new RuntimeException("转出账户不存在");
        }

        // 增加转入账户金额
        String sql2 = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
        pstmt2 = conn.prepareStatement(sql2);
        pstmt2.setDouble(1, amount);
        pstmt2.setInt(2, toId);
        int rows2 = pstmt2.executeUpdate();

        if (rows2 == 0) {
            throw new RuntimeException("转入账户不存在");
        }

        // 所有操作成功,提交事务
        conn.commit();
        System.out.println("转账成功!");

    } catch (Exception e) {
        // 发生任何异常,回滚事务
        System.out.println("转账失败: " + e.getMessage());
        conn.rollback();
        throw e;
    } finally {
        closeResources(pstmt1, pstmt2, null);
        try {
            conn.setAutoCommit(true);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Savepoint保存点

Savepoint允许在事务中设置保存点,可以只回滚到保存点,而不是回滚整个事务。

保存点示例

conn.setAutoCommit(false);

try {
    // 操作1:插入订单
    stmt.executeUpdate("INSERT INTO orders VALUES (...)");

    // 设置保存点1
    Savepoint sp1 = conn.setSavepoint("savepoint1");

    // 操作2:插入订单明细
    stmt.executeUpdate("INSERT INTO order_items VALUES (...)");

    // 操作3:更新库存
    stmt.executeUpdate("UPDATE inventory SET stock = stock - 1 WHERE product_id = ?");

    // 所有操作成功
    conn.commit();

} catch (Exception e) {
    // 判断是什么类型的异常
    if (e.getMessage().contains("库存不足")) {
        // 只想回滚操作3,保留操作1和2
        conn.rollback(sp1);  // 回滚到保存点1
        conn.commit();  // 提交保存点前的操作
        System.out.println("库存不足,但订单已创建");
    } else {
        // 其他异常,回滚整个事务
        conn.rollback();
        System.out.println("发生错误,整个事务回滚");
    }
} finally {
    conn.setAutoCommit(true);
}

保存点相关方法

Savepoint savepoint = conn.setSavepoint();  // 创建未命名保存点
Savepoint savepoint = conn.setSavepoint("point_name");  // 创建命名保存点

conn.releaseSavepoint(savepoint);  // 释放保存点(不能引用已释放的保存点)
conn.rollback(savepoint);  // 回滚到指定保存点

事务隔离级别

隔离级别决定事务之间的可见性和相互影响程度。隔离级别越高,数据越安全,但性能越低。

四种隔离级别

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED1允许允许允许
READ_COMMITTED2禁止允许允许
REPEATABLE_READ3禁止禁止允许
SERIALIZABLE4禁止禁止禁止

并发问题解释

脏读:读取到另一个事务未提交的数据
不可重复读:同一事务中,两次读取同一数据结果不同
幻读:同一事务中,两次查询结果集数量不同(如插入或删除)

设置隔离级别

// 获取当前隔离级别
int level = conn.getTransactionIsolation();
System.out.println("当前隔离级别: " + level);

// 设置隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

各数据库默认隔离级别

数据库默认隔离级别
MySQLREPEATABLE_READ
OracleREAD_COMMITTED
SQL ServerREAD_COMMITTED
PostgreSQLREAD_COMMITTED

实战场景

场景1:订单创建(扣库存)

public void createOrder(Connection conn, Order order) {
    try {
        conn.setAutoCommit(false);

        // 1. 检查库存是否足够
        String checkSql = "SELECT stock FROM products WHERE id = ?";
        PreparedStatement checkStmt = conn.prepareStatement(checkSql);
        checkStmt.setInt(1, order.getProductId());
        ResultSet rs = checkStmt.executeQuery();

        if (!rs.next() || rs.getInt("stock") < order.getQuantity()) {
            throw new RuntimeException("库存不足");
        }

        // 2. 扣减库存
        String updateSql = "UPDATE products SET stock = stock - ? WHERE id = ?";
        PreparedStatement updateStmt = conn.prepareStatement(updateSql);
        updateStmt.setInt(1, order.getQuantity());
        updateStmt.setInt(2, order.getProductId());
        updateStmt.executeUpdate();

        // 3. 创建订单
        String insertSql = "INSERT INTO orders (user_id, product_id, quantity) VALUES (?, ?, ?)";
        PreparedStatement insertStmt = conn.prepareStatement(insertSql);
        insertStmt.setInt(1, order.getUserId());
        insertStmt.setInt(2, order.getProductId());
        insertStmt.setInt(3, order.getQuantity());
        insertStmt.executeUpdate();

        // 4. 提交事务
        conn.commit();
        System.out.println("订单创建成功");

    } catch (Exception e) {
        conn.rollback();
        System.out.println("订单创建失败: " + e.getMessage());
        throw e;
    } finally {
        conn.setAutoCommit(true);
    }
}

场景2:批量转账(多个账户)

public void batchTransfer(Connection conn, List transfers) {
    try {
        conn.setAutoCommit(false);

        for (Transfer transfer : transfers) {
            // 扣除转出账户
            String deductSql = "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?";
            PreparedStatement deductStmt = conn.prepareStatement(deductSql);
            deductStmt.setDouble(1, transfer.getAmount());
            deductStmt.setInt(2, transfer.getFromId());
            deductStmt.setDouble(3, transfer.getAmount());
            int rows = deductStmt.executeUpdate();

            if (rows == 0) {
                throw new RuntimeException("账户 " + transfer.getFromId() + " 余额不足或不存在");
            }

            // 增加转入账户
            String addSql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
            PreparedStatement addStmt = conn.prepareStatement(addSql);
            addStmt.setDouble(1, transfer.getAmount());
            addStmt.setInt(2, transfer.getToId());
            int rows2 = addStmt.executeUpdate();

            if (rows2 == 0) {
                throw new RuntimeException("账户 " + transfer.getToId() + " 不存在");
            }
        }

        conn.commit();
        System.out.println("批量转账成功,共 " + transfers.size() + " 笔");

    } catch (Exception e) {
        conn.rollback();
        System.out.println("批量转账失败: " + e.getMessage());
        throw e;
    } finally {
        conn.setAutoCommit(true);
    }
}

最佳实践

尽量缩短事务:事务持有锁的时间越短越好
避免嵌套事务:大多数数据库不支持真正的嵌套事务
选择合适的隔离级别:根据业务需求选择,不是越高越好
在finally中恢复自动提交:确保连接池复用时状态正确

小结

  • 事务具有ACID特性:原子性、一致性、隔离性、持久性
  • setAutoCommit(false)开启事务,commit()提交,rollback()回滚
  • Savepoint可以部分回滚事务
  • 隔离级别从低到高:READ_UNCOMMITTED → READ_COMMITTED → REPEATABLE_READ → SERIALIZABLE
  • 隔离级别越高,数据越安全,但性能越低
  • 根据业务需求选择合适的隔离级别
☕ Java 在线代码编辑器
📝 运行结果