Spring 事務介紹(一)之 數據庫的事務的基本特性

spring.png

Spring 事務介紹(一)之 數據庫的事務的基本特性

數據庫的事務的基本特性

事務是區分文件存儲系統和Nosql數據庫重要特性之一,其存在的意義是為了保證即時在并發的情況下,也能正確的執行crud操作,怎樣才能算是正確的?這時提出了事務需要保證的四個特性ACID:

  • A:原子性(atomicity)

事務中各項操作,要么全做要么不做,任何一項操作的失敗都會導致整個事務的失敗;

  • C:一致性(consistency)

事務結束后系統狀態是一致的;

  • I:隔離性(isolation)

并發執行的事務彼此無法看到對方的中間狀態;

  • D:持久性(durability)

事務完成后所做的改動都會被持久化,即使發生災難性的失敗;

在高并發的情況下,要完全保證其ACID是非常困難的,除非把所有的事務串行化執行,但是后果就是性能大打折扣。很多時候我們有些業務對事務的要求是不一樣的,所有數據庫中設計了四種隔離級別,供用戶基于業務進行選擇。

隔離級別 臟讀(Dirty Read) 不可重復讀(NonRepeatable Read) 幻讀(Phantom read)
讀未提交(Read Uncommitted) 可能 可能 可能
讀已提交(Read Committed) 不可能 可能 可能
可重復讀(Repeatable Read) 不可能 不可能 可能
可串行化(Serializable) 不可能 不可能 不可能
  • 臟讀:

一個事務讀取到另一個事務未提交的更新數據。

  • 不可重復讀:

在同一事務中,多次讀取同一數據返回的結果有所不同,換句話說,后面讀取可以讀到另一個事務已提交的更新數據,相反,“可重復讀”在同一事務中多次讀取數據時,能夠保證所讀數據一樣,也就是后續讀取不能讀取到另一事務所提交的更新數據。

  • 幻讀

查詢表中一條數據如果不存在就插入一條,并發的時候卻發現,里面居然有兩條相同的數據,導致插入失敗,這就是幻讀的問題。

幻讀在mysql中,在默認的可重復讀的隔離級別下,由mvcc(多版本并發控制)引起的,其中間隙鎖可以避免幻讀的問題,但是間隙鎖會引起鎖等待問題。

MVCC:

MVCC是通過保存數據在某個時間點的快照來實現的. 不同存儲引擎的MVCC. 不同存儲引擎的MVCC實現是不同的,典型的有樂觀并發控制和悲觀并發控制.

間隙鎖:

當我們用范圍條件而不是相等條件檢索數據,并請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對于鍵值在條件范圍內但并不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。

幾種隔離級別的代碼demo:

ReadUncommittedTest.java

package com.demo.spring;

import java.sql.*;

/**
 * com.demo.spring
 *
 * @author Zyy
 * @date 2019/2/13 22:55
 *
 * Connection.TRANSACTION_READ_UNCOMMITTED
 * 允許讀取未提交事務,會出現臟讀,不可重復讀,幻讀的問題
 */
public class ReadUncommittedTest {
    private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
    private static String userName = "root";
    private static String password = "root";
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
        Thread t1 = run(new Runnable() {
            public void run() {
                insert("001", "test", 100);
            }
        });

        Thread t2 = run(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(500);
                    Connection conn = openConnection();
                    // 將參數升級成 Connection.TRANSACTION_READ_COMMITTED 即可解決臟讀的問題
                    conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
                    select("test", conn);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.join();

    }

    public static Thread run(Runnable runnable) {
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }

    public static Connection openConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
        return conn;
    }

    static {
        try {
            Connection connection = openConnection();
            deleteAccount(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void insert(String accountName, String name, int money) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
            prepare.setString(1, accountName);
            prepare.setString(2, name);
            prepare.setInt(3, money);
            prepare.executeUpdate();
            System.out.println("執行插入成功");
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void select(String name, Connection conn) {
        try {
            PreparedStatement prepare = conn.
                    prepareStatement("select * from account where user = ?");
            prepare.setString(1, name);
            ResultSet resultSet = prepare.executeQuery();
            System.out.println("執行查詢");
            while (resultSet.next()) {
                for (int i = 1; i <= 4; i++) {
                    System.out.print(resultSet.getString(i) + " ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void deleteAccount(Connection conn) {
        try {
            PreparedStatement prepare = conn.prepareStatement("delete from account");
            prepare.executeUpdate();
            System.out.println("執行刪除");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

執行結果:

執行插入成功
執行查詢
141 001 test 100 

出現臟讀問題,讀取到未提交的插入數據。

ReadCommittedTest.java

package com.demo.spring;

import java.sql.*;

/**
 * com.demo.spring
 *
 * @author Zyy
 * @date 2019/2/13 22:32
 *
 * Connection.TRANSACTION_READ_COMMITTED
 * 允許讀取已提交事務,會出現不可重復讀,幻讀的問題
 */
public class ReadCommittedTest {
    private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
    private static String userName = "root";
    private static String password = "root";
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = run(new Runnable() {
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    insert("001", "test", 100);
                }
            }
        });

        Thread t2 = run(new Runnable() {
            public void run() {
                try {
                    Connection connection = openConnection();
                    connection.setAutoCommit(false);
                    // 將參數升級成 Connection.TRANSACTION_REPEATABLE_READ 即可解決不可重復讀問題
                    connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                    // 第一次讀取不到
                    select("test", connection);
                    // 釋放鎖
                    synchronized (lock) {
                        lock.notify();
                    }
                    // 第二次讀取到(數據不一至)
                    Thread.sleep(500);
                    select("test", connection);
                    connection.close();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.join();
        t2.join();

    }

    public static Thread run(Runnable runnable) {
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }

    public static Connection openConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
        return conn;
    }

    static {
        try {
            Connection connection = openConnection();
            //deleteAccount(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void insert(String accountName, String name, int money) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
            prepare.setString(1, accountName);
            prepare.setString(2, name);
            prepare.setInt(3, money);
            prepare.executeUpdate();
            System.out.println("執行插入成功");
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void select(String name, Connection conn) {
        try {
            PreparedStatement prepare = conn.
                    prepareStatement("select * from account where user = ?");
            prepare.setString(1, name);
            ResultSet resultSet = prepare.executeQuery();
            System.out.println("執行查詢");
            while (resultSet.next()) {
                for (int i = 1; i <= 4; i++) {
                    System.out.print(resultSet.getString(i) + " ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void deleteAccount(Connection conn) {
        try {
            PreparedStatement prepare = conn.prepareStatement("delete from account");
            prepare.executeUpdate();
            System.out.println("執行刪除");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

執行結果

執行查詢
141 001 test 100 
142 001 test 100 
143 001 test 100 
執行插入成功
執行查詢
141 001 test 100 
142 001 test 100 
143 001 test 100 
144 001 test 100 

出現不可重復讀的問題,兩次讀取結果不一致。

ReadRepeatableTest.java

package com.demo.spring;

import java.sql.*;

/**
 * com.demo.spring
 *
 * @author Zyy
 * @date 2019/2/13 23:15
 *
 * Connection.TRANSACTION_REPEATABLE_READ
 * 可重復讀 ,在一個事務中同一SQL語句 無論執行多少次都會得到相同的結果
 * 會出現幻讀的問題
 */
public class ReadRepeatableTest {

    private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
    private static String userName = "root";
    private static String password = "root";
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
        Thread t1 = run(new Runnable() {
            public void run() {
                try {
                    synchronized (lock) {
                        lock.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                update("test");
            }
        });

        Thread t2 = run(new Runnable() {
            public void run() {
                try {
                    Connection conn = openConnection();
                    conn.setAutoCommit(false);
                    conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
                    // 第一次讀取 讀取到的數據為未修改前的數據
                    select("test", conn);
                    // 釋放鎖
                    synchronized (lock) {
                        lock.notify();
                    }
                    // 第二次讀取 TRANSACTION_REPEATABLE_READ級別,讀取到的數據也為未修改前的數據 兩次讀取數據一至
                    // 設置id為主鍵 如果此時t1做插入(id=1),t2按主鍵查詢(id=1)
                    // 因為此時為TRANSACTION_REPEATABLE_READ級別 ,所以查詢為空,然后進行插入(id=1)
                    // 此時會出現主鍵沖突的異常,這種情況為幻讀,有興趣的可以嘗試一下
                    Thread.sleep(500);
                    select("test", conn);
                    conn.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.join();

    }

    public static Thread run(Runnable runnable) {
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }

    public static Connection openConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
        return conn;
    }

    static {
        try {
            Connection connection = openConnection();
            //deleteAccount(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void insert(String accountName, String name, int money) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
            prepare.setString(1, accountName);
            prepare.setString(2, name);
            prepare.setInt(3, money);
            prepare.executeUpdate();
            System.out.println("執行插入成功");
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void deleteAccount(Connection conn) {
        try {
            PreparedStatement prepare = conn.prepareStatement("delete from account");
            prepare.executeUpdate();
            System.out.println("執行刪除成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void update(String user) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("update account set money = money + 1 where user = ?");
            prepare.setString(1, user);
            prepare.executeUpdate();
            conn.close();
            System.out.println("執行修改成功");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void select(String name, Connection conn) {
        try {
            PreparedStatement prepare = conn.
                    prepareStatement("select * from account where user = ?");
            prepare.setString(1, name);
            ResultSet resultSet = prepare.executeQuery();
            System.out.println("執行查詢");
            while (resultSet.next()) {
                for (int i = 1; i <= 4; i++) {
                    System.out.print(resultSet.getString(i) + " ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

執行結果:

執行查詢
141 001 test 100 
142 001 test 100 
143 001 test 100 
144 001 test 100 
執行修改成功
執行查詢
141 001 test 100 
142 001 test 100 
143 001 test 100 
144 001 test 100 

兩次查詢結果一致,已解決了不可重復讀的問題,可是會出現幻讀的問題。

幻讀場景描述:

設置id為主鍵,在兩個同時進行的事務中,如果此時事務t1做插入(id=1),事務t2按主鍵查詢(id=1)因為此時為TRANSACTION_REPEATABLE_READ級別 ,所以查詢為空,然后進行插入(id=1)
此時會出現主鍵沖突的異常,這種情況主要是由MVCC導致的,t2查詢的數據因為沒有改動所以是之前保留的查詢數據,為快照版本,但實際上數據庫已經新增了一條,此時進行插入,就拋出主鍵沖突異常了,明明查詢沒有數據然后進行插入,可是會出現插入失敗的情況,這種場景就是幻讀。

數據庫默認隔離級別:

Oracle:讀已提交(Read Committed)

Mysql:可重復讀(Repeatable Read)

另外,mysql執行一條查詢語句默認是一個獨立的事務,所以看上去效果與讀已提交一樣。

Mysql

查看當前會話隔離級別

select @@tx_isolation;

查看系統當前隔離級別

select @@global.tx_isolation;

設置當前會話隔離級別

set session transaction isolatin level repeatable read;

設置系統當前隔離級別

set global transaction isolation level repeatable read;

Oracle

查看系統默認事務隔離級別,也是當前會話隔離級別

#首先創建一個事務

declare
     trans_id Varchar2(100);
  begin
     trans_id := dbms_transaction.local_transaction_id( TRUE );
  end; 
  
#查看事務隔離級別

SELECT s.sid, s.serial#,

  CASE BITAND(t.flag, POWER(2, 28))
    WHEN 0 THEN 'READ COMMITTED'
    ELSE 'SERIALIZABLE'
  END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');

github : https://github.com/zhaoyybalabala/spring-test


歡迎留言交流:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容