數(shù)據(jù)庫事務(wù)詳解

事務(wù)的產(chǎn)生是為了簡化我們的編程模型,使我們在開發(fā)的過程中不用考慮各種潛在的錯誤和并發(fā)問題,而不是伴隨著數(shù)據(jù)庫系統(tǒng)天生就存在的。

事務(wù)支持是在引擎層實現(xiàn)的,InnoDB支持事務(wù)而MyISAM不支持。

假設(shè)數(shù)據(jù)庫不支持事務(wù),看下面的示例:

a賬戶要轉(zhuǎn)給b賬戶100元:

update acount set amount =amount -100 where user=a // 1

update acount set amount =amount +100 where user=b // 2

假設(shè)程序第一步執(zhí)行完成了執(zhí)行到第2步的時候出了問題,那么將導(dǎo)致a賬戶白白少了100塊。此時就能看到事務(wù)的重要性了。

數(shù)據(jù)庫事務(wù)需要具備四大特性(ACID)

  1. 原子性(Atomicity)

    原子性是指事務(wù)不允許部分執(zhí)行。事務(wù)包含的所有操作要么全部成功,要么全部失敗并回滾。

  2. 一致性(Consistency)

    如果事務(wù)執(zhí)行期間沒有出現(xiàn)系統(tǒng)錯誤或其他事務(wù)錯誤,并且數(shù)據(jù)庫在事務(wù)開始期間是數(shù)據(jù)一致的,那么在該事務(wù)結(jié)束時,我們認(rèn)為數(shù)據(jù)庫仍然保證了一致性。

  3. 隔離性(Isolation)

    如果事務(wù)之間不是隔離的,可能會出現(xiàn)以下問題:

    (1) 臟讀(dirty read):一個事務(wù)在處理過程中讀取了另外一個事務(wù)未提交的數(shù)據(jù)。例如事務(wù)A中a給b轉(zhuǎn)100塊錢,在該事務(wù)中首先a的賬戶減100塊,而在此時事務(wù)B中查詢a的賬戶發(fā)現(xiàn)a少了100塊,然后事務(wù)A中b賬戶加錢時發(fā)生了意外導(dǎo)致事務(wù)A回滾。這個時候事務(wù)B拿到的a賬戶就是臟數(shù)據(jù)了。

    (2) 不可重復(fù)讀(none-repeatable read):在一個事務(wù)范圍內(nèi)多次查詢某個數(shù)據(jù)卻得到不同的結(jié)果。例如事務(wù)C中b要提現(xiàn)100塊,首先查詢b賬戶余額發(fā)現(xiàn)有100塊滿足提現(xiàn)要求,此時事務(wù)D中b轉(zhuǎn)賬100給a并且提交事務(wù)成功,在事務(wù)C中再次查詢b的賬戶余額發(fā)現(xiàn)已經(jīng)沒有錢了。

    臟讀和不可重復(fù)讀的區(qū)別在于臟讀是讀取到了另一個事務(wù)未提交的數(shù)據(jù),不可重復(fù)讀是讀取到了其他事務(wù)提交的數(shù)據(jù)。

    (3) 幻讀(phantom read):事務(wù)E中對一個表中所有數(shù)據(jù)做了從0修改為1的操作,這時事務(wù)F又向這個表插入了一行數(shù)據(jù),而這個數(shù)據(jù)項中值為0并提交事務(wù)。此時事務(wù)E查詢會發(fā)現(xiàn)還有一行數(shù)據(jù)沒有修改,這就是幻讀。

    不可重復(fù)讀側(cè)重于修改,幻讀側(cè)重于新增或刪除。解決不可重復(fù)讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表。

  4. 持久性(Durability)

在事務(wù)完成以后,該事務(wù)對數(shù)據(jù)庫所作的更改便持久的保存在數(shù)據(jù)庫之中,并不會被回滾。

數(shù)據(jù)庫為我們提供了四種隔離級別

事務(wù)隔離級別 臟讀 不可重復(fù)讀 幻讀
讀未提交(read-uncommitted)
讀已提交(read-committed)
可重復(fù)讀(repeatable-read)
串行化(serializable)

MySQL查詢當(dāng)前窗口的事務(wù)隔離級別

8以前:select @@tx_isolation;

8:select @@transaction_isolation;

  1. 事務(wù)隔離級別為讀提交時,寫數(shù)據(jù)只會鎖住相應(yīng)的行。
  2. 事務(wù)隔離級別為可重復(fù)讀時,如果檢索條件有索引(包括主鍵索引)的時候,默認(rèn)加鎖方式是next-key 鎖;如果檢索條件沒有索引,更新數(shù)據(jù)時會鎖住整張表。一個間隙被事務(wù)加了鎖,其他事務(wù)是不能在這個間隙插入記錄的,這樣可以防止幻讀。
  3. 事務(wù)隔離級別為串行化時,讀寫數(shù)據(jù)都會鎖住整張表。
  4. 隔離級別越高,越能保證數(shù)據(jù)的完整性和一致性,但是對并發(fā)性能的影響也越大。

MySQL中,默認(rèn)的隔離級別是REPEATABLE-READ(可重復(fù)讀),并且解決了幻讀問題。簡單的來說,mysql的默認(rèn)隔離級別解決了臟讀、幻讀、不可重復(fù)讀問題。

MySQL是如何實現(xiàn)事務(wù)的

首先需要了解MVCC(Multi-Version Concurrency Control),它在許多情況下避免了使用鎖,同時可以提供更小的開銷。根據(jù)實現(xiàn)的不同,它可以允許非阻塞式讀,在寫操作進行時只鎖定必要的記錄。

InnoDB

通過為每一行記錄添加兩個額外的隱藏的值來實現(xiàn)MVCC,這兩個值一個記錄這行數(shù)據(jù)何時被創(chuàng)建,另外一個記錄這行數(shù)據(jù)何時過期(或者被刪除)。但是InnoDB并不存儲這些事件發(fā)生時的實際時間,相反它只存儲這些事件發(fā)生時的系統(tǒng)版本號。這是一個隨著事務(wù)的創(chuàng)建而不斷增長的數(shù)字。每個事務(wù)在事務(wù)開始時會記錄它自己的系統(tǒng)版本號。每個查詢必須去檢查每行數(shù)據(jù)的版本號與事務(wù)的版本號是否相同。讓我們來看看當(dāng)隔離級別是REPEATABLE READ時這種策略是如何應(yīng)用到特定的操作的:

SELECT InnoDB必須保證每行數(shù)據(jù)符合兩個條件:

  1. InnoDB必須找到一個行的版本,它至少要和事務(wù)的版本一樣老(也即它的版本號不大于事務(wù)的版本號)。這保證了不管是事務(wù)開始之前,或者事務(wù)創(chuàng)建時,或者修改了這行數(shù)據(jù)的時候,這行數(shù)據(jù)是存在的。
  2. 這行數(shù)據(jù)的刪除版本必須是未定義的或者比事務(wù)版本要大。這可以保證在事務(wù)開始之前這行數(shù)據(jù)沒有被刪除。這里的不是真正的刪除數(shù)據(jù),而是標(biāo)志出來的刪除。真正意義的刪除是在commit的時候。

符合這兩個條件的行可能會被當(dāng)作查詢結(jié)果而返回。

INSERT:InnoDB為這個新行記錄當(dāng)前的系統(tǒng)版本號。

DELETE:InnoDB將當(dāng)前的系統(tǒng)版本號設(shè)置為這一行的刪除ID。

UPDATE:InnoDB會寫一個這行數(shù)據(jù)的新拷貝,這個拷貝的版本為當(dāng)前的系統(tǒng)版本號。它同時也會將這個版本號寫到舊行的刪除版本里。

快照讀和當(dāng)前讀

快照讀:讀取的是快照版本,也就是歷史版本

當(dāng)前讀:讀取的是最新版本

普通的SELECT就是快照讀,而UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是當(dāng)前讀。

有這樣三種鎖我們需要了解:

  • Record Locks(記錄鎖):在索引記錄上加鎖。
  • Gap Locks(間隙鎖):在索引記錄之間加鎖,或者在第一個索引記錄之前加鎖,或者在最后一個索引記錄之后加鎖。
  • Next-Key Locks:在索引記錄上加鎖,并且在索引記錄之前的間隙加鎖。它相當(dāng)于是Record Locks與Gap Locks的一個結(jié)合。

普通的SELECT用的是一致性讀不加鎖。而對于鎖定讀、UPDATE和DELETE,則需要加鎖,至于加什么鎖視情況而定。如果你對一個唯一索引使用了唯一的檢索條件,那么只需鎖定索引記錄即可;如果你沒有使用唯一索引作為檢索條件,或者用到了索引范圍掃描,那么將會使用間隙鎖或者next-key鎖以此來阻塞其它會話向這個范圍內(nèi)的間隙插入數(shù)據(jù)。

利用MVCC實現(xiàn)一致性非鎖定讀,這就保證在同一個事務(wù)中多次讀取相同的數(shù)據(jù)返回的結(jié)果是一樣的,解決了不可重復(fù)讀的問題

利用Gap Locks和Next-Key可以阻止其它事務(wù)在鎖定區(qū)間內(nèi)插入數(shù)據(jù),因此解決了幻讀問題

MySQL中事務(wù)操作

show variables like '%autocommit%';// 事務(wù)是否自動提交

START TRANSACTION;// 開啟事務(wù)

COMMIT;// 提交事務(wù)
ROLLBACK;// 回滾事務(wù)

使用java操作事務(wù)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * @Author: csj
 * @Description:
 * @Date: 2019/6/14 15:06
 */
public class LocalJDBCTransApplication {

    private static final Logger LOG = LoggerFactory.getLogger(LocalJDBCTransApplication.class);

    public static void main(String[] args) throws SQLException {
        Connection connection = getConnection();
        connection.setAutoCommit(false);

        String sql1 = "update user set gold = gold+1 where uid = ?";
        PreparedStatement ps1 = connection.prepareStatement(sql1);
        ps1.setLong(1,143000164498542592L);
        ps1.executeUpdate();

        String sql2 = "update user set gold = gold-1 where uid = ?";
        PreparedStatement ps2 = connection.prepareStatement(sql2);
        ps1.setLong(1,143000164498542592L);
        ps1.executeUpdate();
        ps1.close();
        ps2.close();
        connection.close();
    }

    private static Connection getConnection() throws SQLException {
        String driver = "com.mysql.cj.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/test";
        String username = "test";
        String password = "test";
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            LOG.error(e.getLocalizedMessage());
        }
        return DriverManager.getConnection(url,username,password);
    }
}

日志

undo用于解決事務(wù)未完成和事務(wù)回滾的情況,redo則是為了保證已經(jīng)提交的事務(wù)所做的修改持久化到輔助存儲。事務(wù)日志會在commit或commit之前寫入持久化存儲中,然后事務(wù)對數(shù)據(jù)本身的修改才能生效。因此就能夠保證在系統(tǒng)故障時可以通過讀取redo日志來重寫持久化操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容