事務(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)
-
原子性(Atomicity)
原子性是指事務(wù)不允許部分執(zhí)行。事務(wù)包含的所有操作要么全部成功,要么全部失敗并回滾。
-
一致性(Consistency)
如果事務(wù)執(zhí)行期間沒有出現(xiàn)系統(tǒng)錯誤或其他事務(wù)錯誤,并且數(shù)據(jù)庫在事務(wù)開始期間是數(shù)據(jù)一致的,那么在該事務(wù)結(jié)束時,我們認(rèn)為數(shù)據(jù)庫仍然保證了一致性。
-
隔離性(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ù)讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表。
持久性(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;
- 事務(wù)隔離級別為讀提交時,寫數(shù)據(jù)只會鎖住相應(yīng)的行。
- 事務(wù)隔離級別為可重復(fù)讀時,如果檢索條件有索引(包括主鍵索引)的時候,默認(rèn)加鎖方式是next-key 鎖;如果檢索條件沒有索引,更新數(shù)據(jù)時會鎖住整張表。一個間隙被事務(wù)加了鎖,其他事務(wù)是不能在這個間隙插入記錄的,這樣可以防止幻讀。
- 事務(wù)隔離級別為串行化時,讀寫數(shù)據(jù)都會鎖住整張表。
- 隔離級別越高,越能保證數(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ù)符合兩個條件:
-
InnoDB必須找到一個行的版本,它至少要和事務(wù)的版本一樣老(也即它的版本號不大于事務(wù)的版本號)。這保證了不管是事務(wù)開始之前,或者事務(wù)創(chuàng)建時,或者修改了這行數(shù)據(jù)的時候,這行數(shù)據(jù)是存在的。 - 這行數(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日志來重寫持久化操作。