JDBC 事務管理與工具類封裝實戰:從 PreparedStatement 到連線池最佳實踐
JDBC 事務控制與資源管理是 Java 後端開發基本功。本文從事務 ACID 特性、JDBC 事務 API、工具類封裝演進、連線池整合四個層面系統梳理。
一、JDBC 事務核心 API
Connection conn = DriverManager.getConnection(url, user, pass);
// 1. 關閉自動提交(開啟事務)
conn.setAutoCommit(false);
try {
// 2. 執行多條 SQL
PreparedStatement ps1 = conn.prepareStatement("INSERT INTO orders ... ");
ps1.executeUpdate();
PreparedStatement ps2 = conn.prepareStatement("UPDATE inventory SET qty=qty-? WHERE sku=?");
ps2.executeUpdate();
// 3. 全部成功 → 提交
conn.commit();
} catch (SQLException e) {
// 4. 任一失敗 → 回滾
conn.rollback();
throw e;
} finally {
// 5. 釋放資源
close(ps1, ps2, conn);
}
二、事務隔離級別設定
// 讀取當前隔離級別
int level = conn.getTransactionIsolation();
// 設定隔離級別(需在事務開始前)
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
| 隔離級別 | 常量 | 髒讀 | 不可重複讀 | 幻讀 | MySQL 預設 |
| READ_UNCOMMITTED | 1 | 可能 | 可能 | 可能 | |
| READ_COMMITTED | 2 | 不可 | 可能 | 可能 | |
| REPEATABLE_READ | 4 | 不可 | 不可 | 可能 | ✅ |
| SERIALIZABLE | 8 | 不可 | 不可 | 不可 | |
三、工具類封裝演進
版本 1:基礎 DBUtils(手動管理連線)
public class DBUtils {
private static final String URL = "jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=Asia/Shanghai";
private static final String USER = "root";
private static final String PASS = "password";
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
static {
try { Class.forName(DRIVER); }
catch (ClassNotFoundException e) { throw new RuntimeException(e); }
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASS);
}
public static void close(ResultSet rs, Statement st, Connection conn) {
try { if (rs != null) rs.close(); } catch (SQLException ignore) {}
try { if (st != null) st.close(); } catch (SQLException ignore) {}
try { if (conn != null) conn.close(); } catch (SQLException ignore) {}
}
}
版本 2:屬性檔外部化配置(db-config.properties)
jdbc.url=jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=password
jdbc.driver=com.mysql.cj.jdbc.Driver
private static ResourceBundle rb = ResourceBundle.getBundle("db-config");
// 靜態塊讀取屬性檔,單例模式避免重複載入驅動
版本 3:整合連線池 — HikariCP(生產標準)
com.zaxxer
HikariCP
5.0.1
// HikariConfig 配置
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=Asia/Shanghai");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.setPoolName("MyApp-Pool");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
DataSource ds = new HikariDataSource(config);
// 使用
try (Connection conn = ds.getConnection()) {
conn.setAutoCommit(false);
// ... 事務邏輯
conn.commit();
} catch (SQLException e) {
// 連線自動歸還池中,rollback 由 try-with-resources 隱式處理或顯式調用
}
四、完整 DAO 範例:事務 + 連線池 + PreparedStatement
public class OrderDao {
private final DataSource ds;
public OrderDao(DataSource ds) { this.ds = ds; }
public void createOrder(Order order, List items) throws SQLException {
String sqlOrder = "INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)";
String sqlItem = "INSERT INTO order_items (order_id, sku_id, qty, price) VALUES (?, ?, ?, ?)";
String sqlStock = "UPDATE products SET stock = stock - ? WHERE sku_id = ? AND stock >= ?";
try (Connection conn = ds.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement psOrder = conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS)) {
psOrder.setLong(1, order.getUserId());
psOrder.setBigDecimal(2, order.getTotal());
psOrder.setString(3, "CREATED");
psOrder.executeUpdate();
try (ResultSet rs = psOrder.getGeneratedKeys()) {
if (rs.next()) order.setId(rs.getLong(1));
}
}
for (OrderItem item : items) {
// 扣庫存(樂觀鎖防超賣)
try (PreparedStatement psStock = conn.prepareStatement(sqlStock)) {
psStock.setInt(1, item.getQty());
psStock.setLong(2, item.getSkuId());
psStock.setInt(3, item.getQty());
int affected = psStock.executeUpdate();
if (affected == 0) throw new RuntimeException("庫存不足: " + item.getSkuId());
}
// 寫明細
try (PreparedStatement psItem = conn.prepareStatement(sqlItem)) {
psItem.setLong(1, order.getId());
psItem.setLong(2, item.getSkuId());
psItem.setInt(3, item.getQty());
psItem.setBigDecimal(4, item.getPrice());
psItem.executeUpdate();
}
}
conn.commit();
} catch (SQLException e) {
// 連線池自動處理 rollback(連線關閉時若未 commit 會自動 rollback)
throw e;
}
}
}
五、避坑指南與最佳實踐
| 坑點 | 後果 | 正確做法 |
| 忘記 setAutoCommit(false) | 每條 SQL 自動提交,非原子性 | 事務開始第一行強制關閉自動提交 |
| catch 區塊未 rollback | 髒資料殘留、鎖未釋放 | catch 中顯式 conn.rollback() |
| finally 未 close 資源 | 連線洩漏、池耗盡、應用掛起 | try-with-resources (Java 7+) 自動關閉 |
| 長事務持有連線 | 併發度下降、鎖爭用、主從延遲 | 事務儘量短、業務邏輯前置、大事務拆分 |
| 自動提交模式下手動 commit | 異常/無效果 | 僅在 setAutoCommit(false) 後調用 commit |
| 連線池大小設定不當 | 太小:等待超時;太大:DB 連線數耗盡、CPU 爭用 | 公式:CPU核心數 × 2 + 有效磁碟數;監控 pool usage 調整 |
六、進階:聲明式事務
Spring @Transactional、Seata 分散式事務、MyBatis 搭配事務管理器 — 生產專案強烈建議使用成熟框架管理事務,而非手工 JDBC。手工 JDBC 僅適合:輕量微服務、無框架依賴、教學驗證、特殊效能調優場景。
核心心法:事務邊界清晰、資源確定性釋放、連線池科學配置、框架優於手工。JDBC 事務管理是基石,理解透徹才能駕馭上層框架。