引言:什么是事务?
事务(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_UNCOMMITTED | 1 | 允许 | 允许 | 允许 |
| READ_COMMITTED | 2 | 禁止 | 允许 | 允许 |
| REPEATABLE_READ | 3 | 禁止 | 禁止 | 允许 |
| SERIALIZABLE | 4 | 禁止 | 禁止 | 禁止 |
并发问题解释
脏读:读取到另一个事务未提交的数据
不可重复读:同一事务中,两次读取同一数据结果不同
幻读:同一事务中,两次查询结果集数量不同(如插入或删除)
设置隔离级别
// 获取当前隔离级别
int level = conn.getTransactionIsolation();
System.out.println("当前隔离级别: " + level);
// 设置隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
各数据库默认隔离级别
| 数据库 | 默认隔离级别 |
|---|---|
| MySQL | REPEATABLE_READ |
| Oracle | READ_COMMITTED |
| SQL Server | READ_COMMITTED |
| PostgreSQL | READ_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
- 隔离级别越高,数据越安全,但性能越低
- 根据业务需求选择合适的隔离级别