天天看點

JDBC學習:事務

什麼是事務

資料庫中一些操作的集合是一個獨立的單元,事務就是構成單一邏輯工作機關的集合。

為什麼需要事務

事務是為解決資料安全操作提出的,事務控制實際上就是控制資料的安全通路。

比如:銀行轉帳業務,賬戶A給賬戶B轉帳100元,需要賬戶A餘額減100元,賬戶B餘額加100元,兩個需要同時發生。完成這種操作需要保證要麼全部成功,要麼全部失敗。

什麼是復原

未能成功完成的事務成為中止事務,對中止事務造成的變更需要進行撤銷處理,稱為事務復原。

事務的特性(ACID 原則)

  • 原子性(atomicity):對事務中的全部操作是不可分割的,要麼全部完成,要麼都不執行。
  • 一緻性(consistency):事務執行之前和執行之後,資料庫都必須處于一緻性狀态。
  • 隔離性(isolation):事務的執行不受其他事務的幹擾,事務執行的中間結果對其他事務必須是透明的。
  • 持久性(durability):對于任意已送出的事務,系統必須保證該對資料庫的改變不丢失,即使資料庫出現故障。

Java JDBC 事務機制

比如有個業務:當我們修改一個資訊後再去查詢這個資訊。這是一個簡單的業務,實作起來也非常容易,但是當這個業務放在多線程高并發的平台下,問題自然就出現了。

比如當執行了一個修改後,在查詢之前有一個線程也執行了修改語句,這時再執行查詢,看到的資訊就有可能和我們修改的不同。為了解決這一問題,就引入了引入JDBC事務機制。

如何操作

把事務操作設定為不自動送出,通過手動送出就能實作事務的處理。

搭建實驗環境實驗,在目前資料庫中建立一個測試表:

CREATE TABLE tb (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT KEY COMMENT '編号',
    name VARCHAR(20) NOT NULL COMMENT '姓名',
    sex VARCHAR(4) NOT NULL COMMENT '性别',
    phone VARCHAR(11) NOT NULL COMMENT '手機号碼'
);           

插入幾條測試資料:

INSERT INTO tb
(name, sex, phone)
VALUES
("張三", "男", "139****2234"),
("李四", "女", "130****3239"),
("王二", "男", "136****1234"),
("小王", "女", "137****1735"),
("趙雲", "男", "131****1255"),
("關羽", "男", "139****1930");           
實驗代碼

相關延伸

事務并發處理可能引起的問題

  • 髒讀(dirty read):一個事務讀取了另一個事務尚未送出的資料。
  • 不可重複讀(non-repeatable read):一個事務的操作導緻另一個事務前後兩次讀取到不同的資料。
  • 幻讀(phantom read):一個事務的操作導緻另一個事務前後兩次查詢到的結果資料量不同。

舉例:

  • 事務A、B并發執行,當A事務update後,B事務select讀取到A尚未送出的資料,此時A事務rollback,則B讀取到的資料是無效的“髒”資料。
  • 當B事務select讀取資料後,A事務update操作更改B事務select到的資料,此時B事務再次讀取該資料,發現前後兩次的資料不一樣。
  • 當B事務select讀取資料後,A事務insert或delete了一條滿足A事務的select條件的記錄,此時B事務再次select,發現查詢到前不存在的資料(“幻影”),或者前面的某個記錄不見了。

JDBC的事務支援

JDBC對事務的支援展現在三個方面:

1.自動送出模式(Auto-commit mode)

Connection提供了一個

auto-commit

的屬性來指定事務何時結束。

  • a.當auto-commit為

    true

    時,當每個獨立的SQL操作執行完畢,事務立即自動送出,也就是說每個SQL操作都是一個事務的。auto-commit預設為

    true

一個獨立SQL操作什麼時候什麼執行完畢呢?

JDBC規範這樣規定

對資料操作語言(DML如insert,update,delete)和資料定義語言(DDL如create,drop),語句一執行完就視為執行完畢。

對select語句,當與它關聯的ResultSet對象關閉時,視為執行完畢。

對存儲過程或者其他傳回多個結果的語句,當與它關聯的所有ResultSet對象全部關閉,所有update count(update,delete等語句操作影響的行數)和output parameter(存儲過程的輸出參數)都已經擷取之後,視為執行完畢。

  • b.當auto-commit為

    false

    時,每個事務都必須顯式調用

    commit

    方法進行送出,或者顯式調用commit方法進行復原。

2.事務隔離級别(Transaction Isolation Levels)

JDBC提供了5種不同的事務隔離級别。在Connection中進行了定義。

  • TRANSACTION_NONE

    :JDBC不支援事務
  • TRANSACTION_READ_UNCOMMITTED

    :允許髒讀、不可重複讀和幻讀
  • TRANSACTION_READ_COMMITTED

    :禁止髒讀,但允許不可重複讀和幻讀
  • TRANSACTION_REPEATABLE_READ

    :禁止髒讀和不可重複讀,單可以幻讀
  • TRANSACTION_SERIALIZABLE

    :禁止髒讀、不可重複讀和幻讀

3.儲存點(SavePoint)

JDBC定義了SavePoint接口,提供在一個更細粒度的事務控制機制。當設定了一個儲存點後,可以rollback到該儲存點處的狀态,而不是rollback整個事務。

連接配接對象擷取和關閉的時機

現狀:

  • 連接配接對象的擷取和銷毀比較浪費時間
  • 一個事務中多個操作,若每個操作都生成一個連接配接對象,多使用者同時連接配接資料庫時,通路效率會非常的低。

需求:

  • 一個事務中的多個操作應該用同一個連接配接對象控制,不然無法實作送出和復原。一個事務開始時獲得連接配接對象,一個事務結束時關閉連接配接。

解決:

  1. 将連接配接對象的申請交給專門的連接配接管理類

    事務申請連接配接對象時,通過連接配接管理類申請,而不再通過DriverManager申請

  2. 一個事務一定是由一個線程完成的,使用線程局部變量獲得同一個連接配接對象

接口

DataSource

該工廠用于提供到此 DataSource 對象所表示的實體資料源的連接配接。

作為 DriverManager 工具的替代項,DataSource 對象是擷取連接配接的首選方法。

DataSource

接口由驅動程式供應商實作。共有三種類型的實作:

  • 基本實作:生成标準的 Connection 對象
  • 連接配接池實作:生成自動參與連接配接池的 Connection 對象。此實作與中間層連接配接池管理器一起使用。
  • 分布式事務實作:生成一個 Connection 對象,該對象可用于分布式事務,大多數情況下總是參與連接配接池。此實作與中間層事務管理器一起使用,大多數情況下總是與連接配接池管理器一起使用。

方法:

  • getConnection()

    :嘗試建立與此 DataSource 對象所表示的資料源的連接配接。
  • getConnection(String username, String password)

執行個體:

先在src目錄下建立配置檔案

config.properties

:

driver=com.mysql.jdbc.Driver
url=jdbc\:mysql\://127.0.0.1\:3306/empmgs?useUnicode\=true&characterEncoding\=utf-8
username=root
password=root           

執行個體代碼:

import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;

public class demo4 {
    static Properties prop = new Properties();
    static DataSource ds = null;
    static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
    private static Connection conn = null;
    private static Statement sm = null;
    /**
     * 靜态初始化塊加載注冊驅動
     */
    static {
        try {
            prop.load(new FileInputStream("src/config.properties"));
            ds = BasicDataSourceFactory.createDataSource(prop);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    static public Connection getConnection(){
        try {
            //先看線程局部變量
            conn = threadLocal.get();
            if(conn == null){//線程局部變量中沒有儲存連接配接對象
                conn = ds.getConnection();
                threadLocal.set(conn);//設定連接配接對象到線程局部變量
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
    public static void main(String[] args) {
        try{
            conn = getConnection ();
            sm = conn.createStatement();
            // 關閉自動送出
            conn.setAutoCommit(false);
            sm.executeUpdate("UPDATE tb SET name = '老王' WHERE id = 3 ");
            sm.executeUpdate("INSERT INTO tb (name, sex, phone) VALUES ('美麗','女','132****5555')");
            conn.commit();
            System.out.println("運作結束");
        } catch (Exception e) {
            try {
                conn.rollback();
            } catch (Exception ex) {
                e.printStackTrace();
            }
        } finally {
            if (sm != null) {
                try {
                    sm.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }        
            if (conn != null) {
                try {
                    conn.close();
                    conn = null;
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}