首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> MySQL 資料庫 >> MySQL 綜合教學

JDBC 事務管理與工具類封裝實戰:從 PreparedStatement 到連線池最佳實踐

     編輯:MySQL 綜合教學

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_UNCOMMITTED1可能可能可能
READ_COMMITTED2不可可能可能
REPEATABLE_READ4不可不可可能✅
SERIALIZABLE8不可不可不可

三、工具類封裝演進

版本 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 事務管理是基石,理解透徹才能駕馭上層框架。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved