事務
事務是應用程式中一系列嚴密的操作,所有操作必須成功完成,否則在每個操作中所作的所有更改都會被撤消。事務是邏輯上的一組操作,要麼都執行,要麼都不執行。
ACID簡介
原子性(Atomicity)、一緻性(Correspondence)、隔離性(Isolation)、持久性(Durability)。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iN0YmN3ADNjFmMxITMkljMkRmY4EmYwEmNkRjY2EzMx8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
(1)原子性:整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滞在中間某個環節。事務在執行過程中發生錯誤,會被復原(Rollback)到事務開始前的狀态,就像這個事務從來沒有執行過一樣。
(2)一緻性:在事務開始之前和事務結束以後,資料庫的完整性限制沒有被破壞。
(3)隔離性:隔離狀态執行事務,使它們好像是系統在給定時間内執行的唯一操作。如果有兩個事務,運作在相同的時間内,執行 相同的功能,事務的隔離性将確定每一事務在系統中認為隻有該事務在使用系統。這種屬性有時稱為串行化,為了防止事務操作間的混淆, 必須串行化或序列化請 求,使得在同一時間僅有一個請求用于同一資料。
(4)持久性:在事務完成以後,該事務所對資料庫所作的更改便持久的儲存在資料庫之中,并不會被復原。
ACID常見問題
問題一:Mysql怎麼保證一緻性的?
這個問題分為兩個層面來說。
從資料庫層面,資料庫通過原子性、隔離性、持久性來保證一緻性。也就是說ACID四大特性之中,C(一緻性)是目的,A(原子性)、I(隔離性)、D(持久性)是手段,是為了保證一緻性,資料庫提供的手段。資料庫必須要實作AID三大特性,才有可能實作一緻性。例如,原子性無法保證,顯然一緻性也無法保證。但是,如果你在事務裡故意寫出違反限制的代碼,一緻性還是無法保證的。例如,你在轉賬的例子中,你的代碼裡故意不給B賬戶加錢,那一緻性還是無法保證。是以,還必須從應用層角度考慮。
從應用層面,通過代碼判斷資料庫資料是否有效,然後決定復原還是送出資料!
一緻性實作--基于AID
問題二: Mysql怎麼保證原子性的?
利用Innodb的undo log。
undo log名為復原日志,是實作原子性的關鍵,當事務復原時能夠撤銷所有已經成功執行的sql語句,他需要記錄你要復原的相應日志資訊。
例如
(1)當你delete一條資料的時候,就需要記錄這條資料的資訊,復原的時候,insert這條舊資料
(2)當你update一條資料的時候,就需要記錄之前的舊值,復原的時候,根據舊值執行update操作
(3)當年insert一條資料的時候,就需要這條記錄的主鍵,復原的時候,根據主鍵執行delete操作
undo log記錄了這些復原需要的資訊,當事務執行失敗或調用了rollback,導緻事務需要復原,便可以利用undo log中的資訊将資料復原到修改之前的樣子。
原子性實作--基于undo log日志
問題三: Mysql怎麼保證持久性的?
利用Innodb的redo log。
正如之前說的,Mysql是先把磁盤上的資料加載到記憶體中,在記憶體中對資料進行修改,再刷回磁盤上。如果此時突然當機,記憶體中的資料就會丢失。
怎麼解決這個問題?
簡單啊,事務送出前直接把資料寫入磁盤就行啊。
這麼做有什麼問題?
隻修改一個頁面裡的一個位元組,就要将整個頁面刷入磁盤,太浪費資源了。畢竟一個頁面16kb大小,你隻改其中一點點東西,就要将16kb的内容刷入磁盤,聽着也不合理。
畢竟一個事務裡的SQL可能牽涉到多個資料頁的修改,而這些資料頁可能不是相鄰的,也就是屬于随機IO。顯然操作随機IO,速度會比較慢。
于是,決定采用redo log解決上面的問題。當做資料修改的時候,不僅在記憶體中操作,還會在redo log中記錄這次操作。當事務送出的時候,會将redo log日志進行刷盤(redo log一部分在記憶體中,一部分在磁盤上)。當資料庫當機重新開機的時候,會将redo log中的内容恢複到資料庫中,再根據undo log和binlog内容決定復原資料還是送出資料。
采用redo log的好處?
其實好處就是将redo log進行刷盤比對資料頁刷盤效率高,具體表現如下
redo log體積小,畢竟隻記錄了哪一頁修改了啥,是以體積小,刷盤快。
redo log是一直往末尾進行追加,屬于順序IO。效率顯然比随機IO來的快。
持久性實作--基于redo log日志
問題四: Mysql怎麼保證隔離性的?
利用的是鎖和MVCC機制。還是拿轉賬例子來說明,有一個賬戶表如下 表名t_balance
其中id是主鍵,user_id為賬戶名,balance為餘額。還是以轉賬兩次為例,如下圖所示
至于MVCC,即多版本并發控制(Multi Version Concurrency Control),一個行記錄資料有多個版本對快照資料,這些快照資料在undo log中。
如果一個事務讀取的行正在做DELELE或者UPDATE操作,讀取操作不會等行上的鎖釋放,而是讀取該行的快照版本。 由于MVCC機制在可重複讀(Repeateable Read)和讀已送出(Read Commited)的MVCC表現形式不同,就不贅述了。 但是有一點說明一下,在事務隔離級别為讀已送出(Read Commited)時,一個事務能夠讀到另一個事務已經送出的資料,是不滿足隔離性的。但是當事務隔離級别為可重複讀(Repeateable Read)中,是滿足隔離性的。
mysql 實作事務隔離的原理—— mvcc(多版本并發控制)
(1)快照讀和目前讀
快照讀就是一個 select 語句,形如:
select * from table
在 Repeatableread 事務隔離級别下,快照讀的特點是擷取目前資料庫的快照資料,對于所有未 commit 的資料都不可見,快照讀不會對資料上鎖。
目前讀是對所讀資料上悲觀鎖使其他目前讀無法操作資料。目前讀 sql 包括:
select ... lock in share mode
select ... for update
insert
update
delete
其中後面三個 sql 都是給資料庫上排他鎖(X鎖),而第一個 sql 是給資料庫上共享鎖(S鎖)。X 鎖是一旦某個目前讀到這個鎖,其他目前讀則沒有對這個事務讀寫的權利,其他目前讀會被阻塞住。而 S 鎖是當一個目前讀對某條資料上 S 鎖,其他目前讀可以對該資料也上 S 鎖但不能上 X 鎖,拿到 S 鎖的目前讀可以讀資料不能改資料。(關于資料庫悲觀鎖樂觀鎖并發章節會介紹)。
(2)mvcc 原理
innodb 實作快照讀和目前讀悲觀鎖的技術就是 mvcc 。innodb 在插入一條資料的時候會在後面跟上兩個隐藏的列,這兩個列,一個儲存了這個行的建立時系統版本号,一個儲存的是行的删除的系統版本号。每開始一個新的事務,系統版本号就會自動遞增,事務開始時刻的系統版本号會作為事務的 ID。innodb 更新一條資料是設定舊資料删除版本号,然後插入一條新的資料并設定建立版本号,然後删除舊的資料。那麼怎麼保證快照讀是讀取到未 commit 的資料呢,兩個條件:
InnoDB 隻查找建立版本早于目前事務版本的資料行,即,行的系統版本号小于或等于事務的系統版本号,這樣可以確定事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
行的删除版本,要麼未定義,要麼大于目前事務版本号。這樣可以確定事務讀取到的行,在事務開始之前未被删除。隻有符合上述兩個條件的紀錄,才能作為查詢結果傳回。
而資料庫鎖也是通過比對版本号來決定是否阻塞某個事務。
并發事務帶來的問題
在典型的應用程式中,多個事務并發運作,經常會操作相同的資料來完成各自的任務(多個使用者對統一資料進行操作)。并發雖然是必須的,但可能會導緻以下的問題。
髒讀(Dirty read): 當一個事務正在通路資料并且對資料進行了修改,而這種修改還沒有送出到資料庫中,這時另外一個事務也通路了這個資料,然後使用了這個資料。因為這個資料是還沒有送出的資料,那麼另外一個事務讀到的這個資料是“髒資料”,依據“髒資料”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在一個事務讀取一個資料時,另外一個事務也通路了該資料,那麼在第一個事務中修改了這個資料後,第二個事務也修改了這個資料。這樣第一個事務内的修改結果就被丢失,是以稱為丢失修改。例如:事務1讀取某表中的資料A=20,事務2也讀取A=20,事務1修改A=A-1,事務2也修改A=A-1,最終結果A=19,事務1的修改被丢失。
不可重複讀(Unrepeatableread): 指在一個事務内多次讀同一資料。在這個事務還沒有結束時,另一個事務也通路該資料。那麼,在第一個事務中的兩次讀資料之間,由于第二個事務的修改導緻第一個事務兩次讀取的資料可能不太一樣。這就發生了在一個事務内兩次讀到的資料是不一樣的情況,是以稱為不可重複讀。
幻讀(Phantom read): 幻讀與不可重複讀類似。它發生在一個事務(T1)讀取了幾行資料,接着另一個并發事務(T2)插入了一些資料時。在随後的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄,就好像發生了幻覺一樣,是以稱為幻讀。
不可重複度和幻讀差別:
不可重複讀的重點是修改比如多次讀取一條記錄發現其中某些列的值被修改,幻讀的重點在于新增或者删除比如多次讀取一條記錄發現記錄增多或減少了。
事務隔離級别有哪些?MySQL的預設隔離級别是?
SQL 标準定義了四個隔離級别:
READ-UNCOMMITTED(讀取未送出): 最低的隔離級别,允許讀取尚未送出的資料變更,可能會導緻髒讀、幻讀或不可重複讀。
READ-COMMITTED(讀取已送出): 允許讀取并發事務已經送出的資料,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。
REPEATABLE-READ(可重複讀): 對同一字段的多次讀取結果都是一緻的,除非資料是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
SERIALIZABLE(可串行化): 最高的隔離級别,完全服從ACID的隔離級别。所有的事務依次逐個執行,這樣事務之間就完全不可能産生幹擾,也就是說,該級别可以防止髒讀、不可重複讀以及幻讀。
隔離級别髒讀不可重複讀幻影讀
READ-UNCOMMITTED
√
√
√
READ-COMMITTED
×
√
√
REPEATABLE-READ
×
×
√
SERIALIZABLE
×
×
×
MySQL InnoDB 存儲引擎的預設支援的隔離級别是 REPEATABLE-READ(可重讀)。我們可以通過SELECT @@tx_isolation;指令來檢視
mysql> SELECT @@tx_isolation;+-----------------+| @@tx_isolation |+-----------------+| REPEATABLE-READ |+-----------------+
這裡需要注意的是:與 SQL 标準不同的地方在于 InnoDB 存儲引擎在 REPEATABLE-READ(可重讀)事務隔離級别下使用的是Next-Key Lock 鎖算法,是以可以避免幻讀的産生,這與其他資料庫系統(如 SQL Server)是不同的。是以說InnoDB 存儲引擎的預設支援的隔離級别是 REPEATABLE-READ(可重讀) 已經可以完全保證事務的隔離性要求,即達到了 SQL标準的SERIALIZABLE(可串行化)隔離級别。
因為隔離級别越低,事務請求的鎖越少,是以大部分資料庫系統的隔離級别都是READ-COMMITTED(讀取送出内容):,但是你要知道的是InnoDB 存儲引擎預設使用 REPEATABLE-READ(可重讀)并不會有任何性能損失。
InnoDB 存儲引擎在 分布式事務 的情況下一般會用到SERIALIZABLE(可串行化)隔離級别。
事務隔離級别的設定
修改事務隔離級别:set tx_isolation='READ-UNCOMMITTED';
檢視目前事務隔離級别:select @@tx_isolation;
鎖機制與InnoDB鎖算法
MyISAM和InnoDB存儲引擎使用的鎖:
MyISAM采用表級鎖(table-level locking)。
InnoDB支援行級鎖(row-level locking)和表級鎖,預設為行級鎖
表級鎖和行級鎖對比:
表級鎖: MySQL中鎖定 粒度最大 的一種鎖,對目前操作的整張表加鎖,實作簡單,資源消耗也比較少,加鎖快,不會出現死鎖。其鎖定粒度最大,觸發鎖沖突的機率最高,并發度最低,MyISAM和 InnoDB引擎都支援表級鎖。
行級鎖: MySQL中鎖定 粒度最小 的一種鎖,隻針對目前操作的行進行加鎖。行級鎖能大大減少資料庫操作的沖突。其加鎖粒度最小,并發度高,但加鎖的開銷也最大,加鎖慢,會出現死鎖。
InnoDB存儲引擎的鎖的算法有三種:
Record lock:單個行記錄上的鎖
Gap lock:間隙鎖,鎖定一個範圍,不包括記錄本身
Next-key lock:record+gap 鎖定一個範圍,包含記錄本身
相關知識點:
innodb對于行的查詢使用next-key lock
Next-locking keying為了解決Phantom Problem幻讀問題
當查詢的索引含有唯一屬性時,将next-key lock降級為record key
Gap鎖設計的目的是為了阻止多個事務将記錄插入到同一範圍内,而這會導緻幻讀問題的産生
有兩種方式顯式關閉gap鎖:(除了外鍵限制和唯一性檢查外,其餘情況僅使用record lock) A. 将事務隔離級别設定為RC B. 将參數innodb_locks_unsafe_for_binlog設定為1
大表優化
當MySQL單表記錄數過大時,資料庫的CRUD性能會明顯下降,一些常見的優化措施如下:
1. 限定資料的範圍
務必禁止不帶任何限制資料範圍條件的查詢語句。比如:我們當使用者在查詢訂單曆史的時候,我們可以控制在一個月的範圍内;
2. 讀/寫分離
經典的資料庫拆分方案,主庫負責寫,從庫負責讀;
3. 垂直分區
根據資料庫裡面資料表的相關性進行拆分。 例如,使用者表中既有使用者的登入資訊又有使用者的基本資訊,可以将使用者表拆分成兩個單獨的表,甚至放到單獨的庫做分庫。
簡單來說垂直拆分是指資料表列的拆分,把一張列比較多的表拆分為多張表。 如下圖所示,這樣來說大家應該就更容易了解了。
資料庫垂直分區
垂直拆分的優點: 可以使得列資料變小,在查詢時減少讀取的Block數,減少I/O次數。此外,垂直分區可以簡化表的結構,易于維護。
垂直拆分的缺點: 主鍵會出現備援,需要管理備援列,并會引起Join操作,可以通過在應用層進行Join來解決。此外,垂直分區會讓事務變得更加複雜;
4. 水準分區
保持資料表結構不變,通過某種政策存儲資料分片。這樣每一片資料分散到不同的表或者庫中,達到了分布式的目的。水準拆分可以支撐非常大的資料量。
水準拆分是指資料表行的拆分,表的行數超過200萬行時,就會變慢,這時可以把一張的表的資料拆成多張表來存放。舉個例子:我們可以将使用者資訊表拆分成多個使用者資訊表,這樣就可以避免單一表資料量過大對性能造成影響。
資料庫水準拆分
水準拆分可以支援非常大的資料量。需要注意的一點是:分表僅僅是解決了單一表資料過大的問題,但由于表的資料還是在同一台機器上,其實對于提升MySQL并發能力沒有什麼意義,是以 水準拆分最好分庫 。
水準拆分能夠 支援非常大的資料量存儲,應用端改造也少,但 分片事務難以解決 ,跨節點Join性能較差,邏輯複雜。《Java工程師修煉之道》的作者推薦 盡量不要對資料進行分片,因為拆分會帶來邏輯、部署、運維的各種複雜度 ,一般的資料表在優化得當的情況下支撐千萬以下的資料量是沒有太大問題的。如果實在要分片,盡量選擇用戶端分片架構,這樣可以減少一次和中間件的網絡I/O。
下面補充一下資料庫分片的兩種常見方案:
用戶端代理: 分片邏輯在應用端,封裝在jar包中,通過修改或者封裝JDBC層來實作。 當當網的 Sharding-JDBC 、阿裡的TDDL是兩種比較常用的實作。
中間件代理: 在應用和資料中間加了一個代理層。分片邏輯統一維護在中間件服務中。 我們現在談的 Mycat 、360的Atlas、網易的DDB等等都是這種架構的實作。