庫存-Mysql中的事務、鎖與儲存引擎

徐家三少發表於2016-12-14

設計一個庫存系統。在庫存系統中,最重要的就是要防止超賣。模擬的SQL語句如下:
首先查詢是否有剩餘量,正常的操作為:

select  *  from t_goods where id=1 and  rest>0複製程式碼

然後發現有剩餘量,開始執行更新操作:

update t_goods   set rest=rest-1 where id=1;複製程式碼

假設有A,B,C 三個使用者進來,那麼同時執行select語句,假設剩餘量只剩下1個,那麼A,B,C同時操作會引起超賣。

那麼可以在update的時候嚴格一下,也就是在update的時候再去判斷一次庫存是否大於0:

update t_goods   set rest=rest-1 where id=1 and  rest > 0;複製程式碼

那麼,一個執行緒在update的時候,另外一個執行緒同時能訪問這條資料嗎?這看起來是一個簡單的問題,然而要清楚明白的瞭解發生了什麼,卻並不容易。
這裡就涉及到了資料庫的三大板塊:事務、,儲存引擎

先來說說什麼是事務,或者說把一個操作叫做事務,應該滿足什麼特徵。

一、事務的4個基本特徵

  當事務處理系統建立事務時,將確保事務具有某些特性。元件的開發者們假設事務的特性應該是一些不需要他們親自管理的特性。這些特性稱為ACID特性。 ACID就是:原子性(Atomicity )、一致性( Consistency )、隔離性或獨立性( Isolation)和永續性(Durabilily)。

1、原子性 (Atomicity )

   原子性屬性用於標識事務是否完全地完成,一個事務的任何更新要在系統上完全完成,如果由於某種原因出錯,事務不能完成它的全部任務,系統將返回到事務開始前的狀態。讓我們再看一下銀行轉帳的例子。如果在轉帳的過程中出現錯誤,整個事務將會回滾。只有當事務中的所有部分都成功執行了,才將事務寫入磁碟並使變化 永久化。為了提供回滾或者撤消未提交的變化的能力,許多資料來源採用日誌機制。例如,SQL Server使用一個預寫事務日誌,在將資料應用於(或提交到)實際資料頁面前,先寫在事務日誌上。但是,其他一些資料來源不是關係型資料庫管理系統 (RDBMS),它們管理未提交事務的方式完全不同。只要事務回滾時,資料來源可以撤消所有未提交的改變,那麼這種技術應該可用於管理事務。

2、一致性( Consistency )

  事務在系統完整性中實施一致性,這通過保證系統的任何事務最後都處於有效狀態來實現。如果事務成功地完成,那麼系統中所有變化將正確地應用,系統處於有效狀態。如果在事務中出現錯誤,那麼系統中的所有變化將自動地回滾,系統返回到原始狀態。因為事務開
始時系統處於一致狀態,所以現在系統仍然處於一致狀態。 再讓我們回頭看一下銀行轉帳的例子,在帳戶轉換和資金轉移前,帳戶處於有效狀態。如果事務成功地完成,並且提交事務,則帳戶處於新的有效的狀態。如果事務出錯,終止後,帳戶返回到原先的有效狀態。
記住,事務不負責實施資料完整性,而僅僅負責在事務提交或終止以後確保資料返回到一致狀態。理解資料完整性規則並寫程式碼實現完整性的重任通常落在 開發者肩上,他們根據業務要求進行設計。 當許多使用者同時使用和修改同樣的資料時,事務必須保持其資料的完整性和一致性。因此我們進一步研究ACID特性中的下一個特性:隔離性。

3、隔離性 ( Isolation)

  隔離性是當多個使用者併發訪問資料庫時,比如操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所干擾,多個併發事務之間要相互隔離。即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後才開始,這樣每個事務都感覺不到有其他事務在併發地執行。事務的隔離性通過使用鎖定來實現。

4、永續性 (Durabilily)

  永續性意味著一旦事務執行成功,在系統中產生的所有變化將是永久的。應該存在一些檢查點防止在系統失敗時丟失資訊。甚至硬體本身失敗,系統的狀態仍能通過在日誌中記錄事務完成的任務進行重建。永續性的概念允許開發者認為不管系統以後發生了什麼變化,完 成的事務是系統永久的部分。

  對於資料庫來說,1、2、4特性較容易滿足。資料庫是一個共享資源,可以同時供多個使用者使用。也就是需要對事務進行併發的控制,以滿足隔離性。如果一個個事務是序列的執行,也就是一次只能執行一個事務,只有一個事務等到另外一個事務完全提交修改以後再執行另外一個事務,那麼完全滿足了隔離性。但是此時資料庫系統大部分都是處於空閒狀態,其效率將會很低。因此,事務應該允許併發的執行,並且應該對事務進行併發控制。如果不對事務進行併發控制,會出現以下三種情況:

1、丟失更新(Lost update)

  兩個事務都同時更新一行資料,但是第二個事務卻覆蓋了第一個事務的修改。比如T1,T2事務都發現當前剩餘量為16,然後減1, T1修改以後為15,T2修改以後同樣為15,T2的修改覆蓋了T1。

2、非重複讀(Non-repeatable Reads)

  一個事務對同一行資料重複讀取兩次,但是卻得到了不同的結果。同一查詢在同一事務中多次進行,由於其他提交事務所做的修改或刪除,每次返回不同的結果集,此時發生非重複讀。 更為通俗的說法是:在一個事務內,多次讀同一個資料。在這個事務還沒有結束時,另一個事務也訪問該同一資料。那麼,在第一個事務的兩次讀資料之間。由於第二個事務的修改,那麼第一個事務讀到的資料可能不一樣,這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為不可重複讀,即原始讀取不可重複。如果同一個事務在第二次讀取的時候發現這條記錄消失了或者增加了,這種情況叫做幻讀。

3、髒讀(Dirty Reads)

   一個事務開始讀取了某行資料,但是另外一個事務已經更新了此資料但沒有能夠及時提交或者回滾,那麼這個事務有可能讀取的是修改前的值,導致了髒讀。髒讀和丟失更新的唯一區別就在於丟失更新所讀取的資料是正確的。

  出現這樣的原因是事務的隔離性遭到了破壞。但是在某些情況下,併發所造成的事務問題並不是完全不可接受的,比如有可能出現幻讀。因此,在效能與事務特性的平衡選擇下,SQL給出了4種隔離級別。SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低階別的隔離級一般支援更高的併發處理,並擁有更低的系統開銷。

1.Read Uncommitted(讀取未提交內容)

  在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用於實際應用,因為它的效能也不比其他級別好多少。讀取未提交的資料,也被稱之為髒讀(Dirty Read)。

2.Read Committed(讀取提交內容)

   這是大多數資料庫系統的預設隔離級別(但不是MySQL預設的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別 也支援所謂的不可重複讀(Nonrepeatable Read),因為同一事務的其他例項在該例項處理其間可能會有新的commit,所以同一select可能返回不同結果。

3.Repeatable Read(可重讀)

  這是MySQL的預設事務隔離級別,它確保同一事務的多個例項在併發讀取資料時,會看到同樣的資料行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當使用者讀取某一範圍的資料行時,另一個事務又在該範圍內插入了新行,當使用者再讀取該範圍的資料行時,會發現有新的“幻影” 行。InnoDB和Falcon儲存引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。

4.Serializable(可序列化)

  這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的資料行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。

使用select @@tx_isolation;可以檢視檢視Mysql的預設的事務隔離級別:


mysql>    select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 rows in set (0.02 sec)

mysql>複製程式碼

  Mysql使用鎖來完成併發控制。要特別說明的是,對於原子性,一致性和永續性來說,使用者無法介入,但是對於併發控制,使用者卻能手工的靈活操縱,也就是說,資料庫事務的隔離性有時候需要使用者手工控制。
基本的封鎖型別有三種種:排它鎖(X鎖)和共享鎖(S鎖)已經意向鎖。

X鎖

  所謂X鎖,是事務T對資料A加上X鎖時,只允許事務T讀取和修改資料A,其他事務不能讀取也不能修改資料庫A。

S鎖

  所謂S鎖,是事務T對資料A加上S鎖時,其他事務只能再對資料A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。若事務T對資料物件A加了S鎖,則T就可以對A進行讀取,但不能進行更新(S鎖因此又稱為讀鎖),在T釋放A上的S鎖以前,其他事務可以再對A加S鎖,但不能加X鎖,從而可以讀取A,但不能更新A。

意向鎖:

  多粒度封鎖協議允許多粒樹中的每個結點被獨立加鎖。對一個結點加鎖意味著這個結點的所有後裔結點也被加以同樣型別的鎖。顯示封鎖是資料庫直接對資料物件加鎖,隱式封鎖是該物件沒有被獨立封鎖,而是其上級物件被封鎖而導致該資料物件被加鎖。因此係統在對某一資料物件加鎖時不僅要檢查該資料物件上有無(顯式和隱式)封鎖與之衝突;還要檢查其所有上級結點和所有下級結點,看申請的封鎖是否與這些結點上的(顯式和隱式)封鎖衝突;顯然,這樣的檢查方法效率很低。為此引進了意向鎖。意向鎖的含義是:對任一結點加鎖時,必須先對它的上層結點加意向鎖。

   例如事務T要對某個元組加X鎖,則首先要對關係和資料庫加IX鎖。換言之,對關係和資料庫加IX鎖,表示它的後裔結點—某個元組擬(意向)加X鎖。

   引進意向鎖後,系統對某一資料物件加鎖時不必逐個檢查與下一級結點的封鎖衝突了。例如,事務T要對關係R加X鎖時,系統只要檢查根結點資料庫和R本身是否已加了不相容的鎖(如發現已經加了IX,則與X衝突),而不再需要搜尋和檢查R中的每一個元組是否加了X鎖或S鎖。

鎖定時間的長短

  鎖保持的時間長度為保護所請求級別上的資源所需的時間長度。用於保護讀取操作的共享鎖的保持時間取決於事務隔離級別。採用 READ COMMITTED 的事務隔離級別時,只在讀取頁的期間內控制共享鎖。在掃描中,直到在掃描內的下一頁上獲取鎖時才釋放鎖。如果指定 HOLDLOCK 提示或者將事務隔離級別設定為 REPEATABLE READSERIALIZABLE,則直到事務結束才釋放鎖。

  根據為遊標設定的併發選項,遊標可以獲取共享模式的滾動鎖以保護提取。當需要滾動鎖時,直到下一次提取或關閉遊標(以先發生者為準)時才釋放滾動鎖。但是,如果指定 HOLDLOCK,則直到事務結束才釋放滾動鎖。

  請注意用於保護更新的排它鎖(X鎖)將直到事務結束才釋放。

  如果一個連線試圖獲取一個鎖,而該鎖與另一個連線所控制的鎖衝突,則試圖獲取鎖的連線將一直阻塞到將衝突鎖釋放而且連線獲取了所請求的鎖或者連線的超時間隔已到期。預設情況下沒有超時間隔,但是一些應用程式設定超時間隔以防止無限期等待。

儲存引擎

  並不是所有的儲存引擎都支援事務和鎖,使用show engines檢視各個儲存引擎對事務和鎖的概況:


mysql> show engines;
+------------+---------+------------------------------------------------------------+--------------+------+------------+
| Engine     | Support | Comment                                                    | Transactions | XA   | Savepoints |
+------------+---------+------------------------------------------------------------+--------------+------+------------+
| MRG_MYISAM | YES     | Collection of identical MyISAM tables                      | NO           | NO   | NO         |
| CSV        | YES     | CSV storage engine                                         | NO           | NO   | NO         |
| MyISAM     | DEFAULT | Default engine as of MySQL 3.23 with great performance     | NO           | NO   | NO         |
| InnoDB     | YES     | Supports transactions, row-level locking, and foreign keys | YES          | YES  | YES        |
| MEMORY     | YES     | Hash based, stored in memory, useful for temporary tables  | NO           | NO   | NO         |
+------------+---------+------------------------------------------------------------+--------------+------+------------+
5 rows in set (0.02 sec)複製程式碼

  可以發現,只有InnoDB才完全支援事務,行級鎖和外來鍵。MyISAM引擎不支援事務。如果要使用事務,必須是innodb引擎:alter table table_name engine=innodb;

分析Mysql事務中鎖的變化

  假設有如下三條語句組成的事務


//1.查詢出商品資訊

select status from t_goods where id=1;

//2.根據商品資訊生成訂單

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status為2

update t_goods set status=2;複製程式碼

  事務採用預設的MySQL預設的Repeatable Read隔離級別。如果要有關各種sql語句到底加什麼鎖的資訊可以訪問[dev.mysql.com/doc/refman/…]

select status from t_goods where id=1;   沒有加鎖,當前事務讀,其他事務也可以讀
insert 加排它鎖,但不影響其他事務的插入其他記錄。(排它鎖,行級鎖)
update 加排它鎖,影響其他事務讀這條記錄(排它鎖,行級鎖)複製程式碼

這裡只列舉了Mysql手冊中關於SELECT ... FROM 中鎖的描述:
>
SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

可以看到,預設並沒有加鎖,那麼有可能出現不可重複讀的情況。只有在SERIALIZABLE的級別下,才會加shared next-key locks。

  使用者可以為select手工的加上一些鎖,以防止其他事務訪問。比如select for update


//1.查詢出商品資訊

select status from t_goods where id=1 for update;

//2.根據商品資訊生成訂單

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status為2

update t_goods set status=2;複製程式碼

  上面我們提到,使用select…for update會把資料給鎖住,加上一個排它鎖。不過我們需要注意一些鎖的級別,MySQL InnoDB預設Row-Level Lock,所以只有「明確」地指定主鍵,MySQL 才會執行Row lock (只鎖住被選取的資料) ,否則MySQL 將會執行Table Lock (將整個資料表單給鎖住)。select…for update 的排它鎖,只有等到事務結束才釋放。
有關select for update的更多資訊,可以訪問:dev.mysql.com/doc/refman/… 瞭解更多。

事務邊界

  Mysql預設自動提交事務,因此一條SQL語句就是一個事務。可以使用show variables like 'autocommit'檢視事務自動提交狀態:

show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 rows in set (0.03 sec)複製程式碼

  如果一個事務要包含多條事務,那麼必須顯示指定事務的邊界。比如使用 begincommit關鍵字。

Mysql中的鎖

排它鎖,讀鎖和意向鎖只是從鎖的作用來劃分。從鎖的粒度來劃分,Mysql鎖的型別可以從dev.mysql.com/doc/refman/… 瞭解到。請注意,行級鎖,表級鎖,頁級鎖只是一種級別劃分。並不存在這樣的鎖的名字叫做“行級鎖,表級鎖,頁級鎖”。

檢視錶的基本狀態(儲存引擎型別,索引數等):

SHOW TABLE STATUS FROM database_name;

JDBC中的事務與Mysql中的事務

從JDBC的角度來說,提交事務就是提交sql給資料庫來執行。由資料庫來保證最後的事務處理。JDBC的事務其實就是保證這些事務之間的資料要麼全部交給資料庫執行,要麼全部不交給資料庫。如果設定了自動提交,那麼sql一旦出現,就立即執行。 MYSQL預設是自動提交的,也就是你提交一個QUERY,它就直接執行!我們可以通過
set autocommit=0 禁止自動提交
set autocommit=1 開啟自動提交
所以,在JDBC中,一起提交sql語句才起作用。(JDBC會給資料庫增加事務處理語句,然後再發給資料庫)
因此,mysql是否開啟自動提交關係不大。
如果事務不是由jdbc來生成的,那麼提交一系列的sql語句到資料庫以後就被資料庫當做單條資料庫執行了,那麼就沒有意義了。

相關文章