實戰分析:事務的隔離級別和傳播屬性

什麼是事務?java

要麼所有都要執行,要麼就都不執行。mysql

事務所具備的四種特性git

原子性,一致性,隔離性,持久性sql

原子性 數據庫

我的理解,就是事務執行不可分割,要麼所有完成,要麼所有拉倒不幹。併發

一致性 異步

關於一致性這個概念咱們來舉個例子說明吧,假設張三給李四轉了100元,那麼須要先從張三那邊扣除100,而後李四那邊增長100,這個轉帳的過程對於其餘事務而言是沒法看到的,這種狀態始終都在保持一致,這個過程咱們稱之爲一致性。ide

隔離性 函數

併發訪問數據庫時,一個用戶的事務不被其餘事務所幹擾,各併發事務之間數據是獨立的;工具

持久性 

一個事務被提交以後。它對數據庫中數據的改變是持久的,即便數據庫發生故障也不該該對其有任何影響。

爲何會出現事務的隔離級別?

咱們都知道,數據庫都是有相應的事物隔離級別。之因此須要分紅不一樣級別的事務,這個是由於在併發的場景下,讀取數據可能會有出現髒讀,不可重複讀以及幻讀的狀況,所以須要設置相應的事物隔離級別。

實戰分析:事務的隔離級別和傳播屬性

爲了方便理解,咱們將使用java程序代碼來演示併發讀取數據時候會產生的相應場景:

環境準備:

  • jdk8

  • mysql數據

創建測試使用表:

CREATE TABLE `money` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `money` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

 

一個方便於操做mysql的簡單JdbcUtil工具類:

import java.io.IOException;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;

/**
 * Jdbc操做數據庫工具類
 *
 * @author idea
 * @version 1.0
 */
public class JdbcUtil {

    public static final String DRIVER;
    public static final String URL;
    public static final String USERNAME;
    public static final String PASSWORD;

    private static Properties prop = null;

    private static PreparedStatement ps = null;

    /**
     * 加載配置文件中的信息
     */
    static {
        prop = new Properties();
        try {
            prop.load(JdbcUtil.class.getClassLoader().getResourceAsStream("db.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        DRIVER = prop.getProperty("driver");
        URL = prop.getProperty("url");
        USERNAME = prop.getProperty("username");
        PASSWORD = prop.getProperty("password");
    }

    /**
     * 獲取鏈接
     *
     * @return void
     * @author blindeagle
     */
    public static Connection getConnection() {
        try {
            Class.forName(DRIVER);
            Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            return conn;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 數據轉換爲list類型
     *
     * @param rs
     * @return
     * @throws SQLException
     */
    public static List convertList(ResultSet rs) throws SQLException {
        List list = new ArrayList();
        //獲取鍵名
        ResultSetMetaData md = rs.getMetaData();
        //獲取行的數量
        int columnCount = md.getColumnCount();
        while (rs.next()) {
            //聲明Map
            HashMap<String,Object> rowData = new HashMap();
            for (int i = 1; i <= columnCount; i++) {
                //獲取鍵名及值
                rowData.put(md.getColumnName(i), rs.getObject(i));
            }
            list.add(rowData);
        }
        return list;
    }
}

 

髒讀

所謂的髒讀是指讀取到沒有提交的數據信息。

模擬場景:兩個線程a,b同時訪問數據庫進行操做,a線程須要插入數據到庫裏面,可是沒有提交事務,這個時候b線程須要讀取數據庫的信息,將a裏面所要插入的數據(可是沒有提交)給讀取了進來,形成了髒讀現象。

代碼以下所示:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * @author idea
 * @date 2019/7/2
 * @Version V1.0
 */
public class DirtyReadDemo {

    public static final String READ_SQL = "SELECT * FROM money";
    public static final String WRITE_SQL = "INSERT INTO `money` (`id`, `money`) VALUES ('3', '350')";

    public Object lock = new Object();

    /**
     * 髒讀模擬(注意:須要設置表的存儲引擎爲innodb類型)
     */
    public static void dirtyRead() {
        try {
            Connection conn = JdbcUtil.getConnection();
            conn.setAutoCommit(false);
            PreparedStatement writePs = conn.prepareStatement(WRITE_SQL);
            writePs.executeUpdate();
            System.out.println("執行寫取數據操做----");

            Thread.sleep(500);

            //須要保證鏈接不一樣
            Connection readConn = JdbcUtil.getConnection();
            //注意這裏面須要保證提交的事物等級爲:未提交讀
            readConn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
            PreparedStatement readPs = readConn.prepareStatement(READ_SQL);
            ResultSet rs = readPs.executeQuery();
            System.out.println("執行讀取數據操做----");
            List list = JdbcUtil.convertList(rs);
            for (Object o : list) {
                System.out.println(o);
            }
            readConn.close();

        } catch (SQLException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        dirtyRead();
    }
}

 

因爲這個案例裏面的事物隔離級別知識設置在了TRANSACTION_READ_UNCOMMITTED層級,所以對於沒有提交事務的數據也會被讀取進來。形成了髒數據讀取的狀況。

所以程序運行以後的結果以下:

實戰分析:事務的隔離級別和傳播屬性


爲了預防髒讀的狀況發生,咱們一般須要提高事務的隔離級別,從原先的TRANSACTION_READ_UNCOMMITTED提高到TRANSACTION_READ_COMMITTED,這個時候咱們再來運行一下程序,會發現原先有的髒數據讀取消失了:

實戰分析:事務的隔離級別和傳播屬性

不可重複讀

所謂的不可重複讀,個人理解是,多個線程a,b同時讀取數據庫裏面的數據,a線程負責插入數據,b線程負責寫入數據,b線程裏面有兩次讀取數據庫的操做,分別是select1和select2,因爲事務的隔離級別設置在了TRANSACTION_READ_COMMITTED,因此當select1執行了以後,a線程插入了新的數據,再去執行select2操做的時候會讀取出新的數據信息,致使出現了不可重複讀問題。

演示代碼:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * 不可重複讀案例
 * @author idea
 * @date 2019/7/2
 * @Version V1.0
 */
public class NotRepeatReadDemo {

    public static final String READ_SQL = "SELECT * FROM money";
    public static final String WRITE_SQL = "INSERT INTO `money` (`id`, `money`) VALUES ('3', '350')";

    public Object lock = new Object();


    /**
     * 不可重複讀模擬
     */
    public  void notRepeatRead() {
        Thread writeThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try (Connection conn = JdbcUtil.getConnection();) {
                    //堵塞等待喚醒
                    synchronized (lock) {
                        lock.wait();
                    }
                    conn.setAutoCommit(true);
                    PreparedStatement ps = conn.prepareStatement(WRITE_SQL);
                    ps.executeUpdate();
                    System.out.println("執行寫取數據操做----");
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread readThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Connection readConn = JdbcUtil.getConnection();
                    readConn.setAutoCommit(false);
                    readConn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                    PreparedStatement readPs = readConn.prepareStatement(READ_SQL);
                    ResultSet rs = readPs.executeQuery();
                    System.out.println("執行讀取數據操做1----");
                    List list = JdbcUtil.convertList(rs);
                    for (Object obj : list) {
                        System.out.println(obj);
                    }

                    synchronized (lock){
                        lock.notify();
                    }

                    Thread.sleep(1000);
                    ResultSet rs2 = readPs.executeQuery();
                    System.out.println("執行讀取數據操做2----");
                    List list2 = JdbcUtil.convertList(rs2);
                    for (Object obj : list2) {
                        System.out.println(obj);
                    }
                    readConn.commit();
                    readConn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        writeThread.start();
        readThread.start();
    }

    public static void main(String[] args) {
        NotRepeatReadDemo notRepeatReadDemo=new NotRepeatReadDemo();
        notRepeatReadDemo.notRepeatRead();
    }

}

 

在設置了TRANSACTION_READ_COMMITTED隔離級別的狀況下,上述程序的運行結果爲:

實戰分析:事務的隔離級別和傳播屬性


爲了不這種狀況的發生,須要保證在同一個事務裏面,屢次重複讀取的數據都是一致的,所以須要將事務的隔離級別從TRANSACTION_READ_COMMITTED提高到TRANSACTION_REPEATABLE_READ級別,這種狀況下,上述程序的運行結果爲:

實戰分析:事務的隔離級別和傳播屬性

幻讀

官方文檔對於幻讀的定義以下:

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a 「phantom」 row.

讀到上一次沒有返回的記錄,看起來是幻影通常。

幻讀與不可重複讀相似。它發生在一個事務(T1)讀取了幾行數據,接着另外一個併發事務(T2)插入了一些數據時。在隨後的查詢中,第一個事務(T1)就會發現多了一些本來不存在的記錄,就好像發生了幻覺同樣,因此稱爲幻讀。爲了解決這種狀況,能夠選擇將事務的隔離級別提高到TRANSACTION_SERIALIZABLE。

什麼是TRANSACTION_SERIALIZABLE?

TRANSACTION_SERIALIZABLE是當前事務隔離級別中最高等級的設置,能夠徹底服從ACID的規則,經過加入行鎖的方式(innodb存儲引擎中)來防止出現數據併發致使的數據不一致性問題。爲了方便理解,能夠看看下方的程序:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.concurrent.CountDownLatch;

/**
 * @author idea
 * @date 2019/7/2
 * @Version V1.0
 */
public class FantasyReadDemo {

    public static final String READ_SQL = "SELECT * FROM money";
    public static final String UPDATE_SQL = "UPDATE `money` SET `money` = ? WHERE `id` = 3;n";


    public CountDownLatch countDownLatch=new CountDownLatch(2);

    public void readAndUpdate1() {
        try (Connection conn = JdbcUtil.getConnection();) {
            conn.setAutoCommit(false);
            PreparedStatement ps = conn.prepareStatement(READ_SQL);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            ResultSet rs = ps.executeQuery();
            rs.next();
            int currentMoney = (int) rs.getObject(2);
            System.out.println("執行寫取數據操做----" + currentMoney);
            //堵塞等待喚醒
            countDownLatch.countDown();
            PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL);
            writePs.setInt(1, currentMoney - 1);
            writePs.execute();
            conn.commit();
            writePs.close();
            ps.close();
            System.out.println("執行寫操做結束---1");
        } catch (Exception e) {
            e.printStackTrace();
            readAndUpdate1();
        }
    }

    public void readAndUpdate2() {
        try (Connection conn = JdbcUtil.getConnection();) {
            conn.setAutoCommit(false);
            PreparedStatement ps = conn.prepareStatement(READ_SQL);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            ResultSet rs = ps.executeQuery();
            rs.next();
            int currentMoney = (int) rs.getObject(2);
            System.out.println("執行寫取數據操做----" + currentMoney);
            //堵塞喚醒
            countDownLatch.countDown();
            PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL);
            writePs.setInt(1, currentMoney - 1);
            writePs.execute();
            conn.commit();
            writePs.close();
            ps.close();
            System.out.println("執行寫操做結束---2");
        } catch (Exception e) {
            //使用串行化事務級別可以較好的保證數據的一致性,可串行化事務 serializable 是事務的最高級別,在每一個讀數據上加上鎖
            //innodb裏面是加入了行鎖,所以出現了異常的時候,只須要從新執行一遍事務便可。
            e.printStackTrace();
            readAndUpdate2();
        }
    }

    public void fantasyRead() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                readAndUpdate1();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                readAndUpdate2();
            }
        });
        try {
            thread1.start();
//            Thread.sleep(500);
            thread2.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        FantasyReadDemo fantasyReadDemo = new FantasyReadDemo();
        fantasyReadDemo.fantasyRead();
    }

}

 

這裏面將事務的隔離級別設置到了TRANSACTION_SERIALIZABLE,可是在運行過程當中爲了保證數據的一致性,串行化級別的事物會給相應的行數據加入行鎖,所以在執行的過程當中會拋出下面的相關異常:

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:377)
    .......

 

這裏爲了方便演示,在拋出異常的時候從新再次執行了一遍事務的方法,從而完成屢次事務併發執行。

可是實際應用場景中,咱們對於這種併發狀態形成的問題都會交給業務層面加入鎖來解決衝突,所以TRANSACTION_SERIALIZABLE隔離級別通常在應用場景中比較少見。

七種事務的傳播機制

事務的七種傳播機制分別爲:

REQUIRED(默認) 默認的事務傳播機制,若是當前不支持事務,那麼就建立一個新的事務。

SUPPORTS 表示支持當前的事務,若是當前沒有事務,則不會單首創建事務

以上的這兩種事務傳播機制比較好理解,接下來的幾種事務傳播機制就比上邊的這幾類稍微複雜一些了。

REQUIRES_NEW

定義: 建立一個新事務,若是當前事務已經存在,把當前事務掛起。
爲了更好的理解REQUIRES_NEW的含義,咱們經過下邊的這個實例來進一步理解:

有這麼一個業務場景,須要往數據插入一個account帳戶信息,而後同時再插入一條userAccount的流水信息。(只是模擬場景,因此對象的命名有點簡陋)
直接來看代碼實現,內容以下所示:

/**
 * @author idea
 * @data 2019/7/6
 */
@Service
public class AccountService {

    @Autowired
    private AccountDao accountDao;
    @Autowired
    private UserAccountService userAccountService;

    /**
     * 外層定義事務, userAccountService.saveOne單獨定義事務
     *
     * @param accountId
     * @param money
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void saveOne(Integer accountId, Double money) {
        accountDao.insert(new Account(accountId, money));
        userAccountService.saveOne("idea", 1001);
        //這裏模擬拋出異常
        int j=1/0;
    }
}

 

再來看userAccountService.saveOne函數:

/**
 * @author idea
 * @data 2019/7/6
 */
@Service
public class UserAccountService {

    @Autowired
    private UserAccountDao userAccountDao;


    /**
     * @param username
     * @param accountId
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOne(String username,Integer accountId){
        userAccountDao.insert(new UserAccount(username,accountId));
    }
}

 

執行程序的時候,AccountService.saveOne裏面的 userAccountService.saveOne函數爲單獨定義的一個事務,並且傳播屬性爲REQUIRES_NEW。所以在執行外層函數的時候,即便後邊拋出了異常,也並不會影響到內部 userAccountService.saveOne的函數執行。

REQUIRES_NEW 老是新啓一個事務,這個傳播機制適用於不受父方法事物影響的操做,好比某些業務場景下須要記錄業務日誌,用於異步反查,那麼無論主體業務邏輯是否完成,日誌都須要記錄下來,不能由於主體業務邏輯報錯而丟失日誌;可是自己是一個單獨的事物,會受到回滾的影響,也就是說 userAccountService.saveOne裏面要是拋了異常,子事務內容一塊兒回滾。

NOT_SUPPORTED

定義:無事務執行,若是當前事務不存在,把已存在的當前事務掛起。

仍是接上邊的代碼來進行試驗:

帳戶的轉帳操做:

實戰分析:事務的隔離級別和傳播屬性


userAccountService內部的saveOne操做:

 

實戰分析:事務的隔離級別和傳播屬性


在執行的過程當中,userAccountService.saveOne拋出了異常,可是因爲該方法申明的事物傳播屬性爲NOT_SUPPORTED級別,所以當子事務內部拋出異常的時候,子事務自己不會回滾,並且也不會影響父類事務的執行。 

 

NOT_SUPPORTED能夠用於發送提示消息,站內信、短信、郵件提示等。不屬於而且不該當影響主體業務邏輯,即便發送失敗也不該該對主體業務邏輯回滾,而且執行過程當中,若是父事務出現了異常,進行回滾,也不會影響子類的事務

NESTED

定義:嵌套事務,若是當前事務存在,那麼在嵌套的事務中執行。若是當前事務不存在,則表現跟REQUIRED同樣。

關於Nested的定義,我我的感受網上寫的比較含糊,因此本身經過搭建Demo來強化理解,仍是原來的例子,假設說父類事務執行的過程當中拋出了異常以下,那麼子類也要跟着回滾:

實戰分析:事務的隔離級別和傳播屬性
實戰分析:事務的隔離級別和傳播屬性


當父事務出現了異常以後,進行回滾,子事務也會被牽扯進來一塊兒回滾。

MANDATORY

定義:MANDATORY單詞中文翻譯爲強制,支持使用當前事務,若是當前事務不存在,則拋出Exception。

這個比較好理解

實戰分析:事務的隔離級別和傳播屬性

 

實戰分析:事務的隔離級別和傳播屬性


當子方法定義了事務,且事務的傳播屬性爲MANDATORY級別的時候,若是父方法沒有定義事務操做的話,就會拋出異常。(此時的子方法會將數據記錄到數據庫裏面)

NEVER

定義:當前若是存在事務則拋出異常

實戰分析:事務的隔離級別和傳播屬性

 

實戰分析:事務的隔離級別和傳播屬性


在執行userAccountService.saveOne函數的時候,發現父類的方法定義了事務,所以會拋出異常信息,而且userAccountService.saveOne會回滾。

傳播屬性小結:

PROPAGATION_NOT_SUPPORTED
不會受到父類事務影響而回滾,本身也不會影響父類函數,出現異常後會自動回滾。

PROPAGATION_REQUIRES_NEW 
不會受到父類事務影響而回滾,本身也不會影響父類函數,出現異常後會自動回滾。

NESTED
會受到父類事務影響而回滾,出現異常後自身也回滾。若是不但願影響父類函數,那麼能夠經過使用try catch來控制操做。

MANDATORY
強制使用當期的事物,若是當前的父類方法沒有事務,那麼在處理數據的時候就會拋出異常

NEVER
當前若是存在事務則拋出異常

REQUIRED(默認) 默認的事務傳播機制,若是當前不支持事務,那麼就建立一個新的事務。

SUPPORTS 表示支持當前的事務,若是當前沒有事務,則不會單首創建事務

本文的所有相關代碼都已經上傳到gitee上邊了,歡迎感興趣的朋友前往進行代碼下載:

https://gitee.com/IdeaHome_admin/wfw
相關文章
相關標籤/搜索