1. 程式人生 > 其它 >MySQL 事務隔離級別和鎖

MySQL 事務隔離級別和鎖

技術標籤:mysqlinnodb

事務及其特性

資料庫事務(簡稱:事務)是資料庫管理系統執行過程中的一個邏輯單位,由一個有限的資料庫操作序列構成。事務的使用是資料庫管理系統區別檔案系統的重要特徵之一。

事務擁有四個重要的特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability),人們習慣稱之為 ACID 特性。下面我逐一對其進行解釋。

IBM Compose for MySQL

IBM Cloud 上提供的 Compose for MySQL 資料庫服務可以幫助您更好地管理 MySQL,其特性包含自擴充套件部署、高可用、以及自動化無停止備份。

  • 原子性 (Atomicity)

事務開始後所有操作,要麼全部做完,要麼全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。例如,如果一個事務需要新增 100 條記錄,但是在新增了 10 條記錄之後就失敗了,那麼資料庫將回滾對這 10 條新增的記錄。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。

  • 一致性 (Consistency)

指事務將資料庫從一種狀態轉變為另一種一致的的狀態。事務開始前和結束後,資料庫的完整性約束沒有被破壞。例如工號帶有唯一屬性,如果經過一個修改工號的事務後,工號變的非唯一了,則表明一致性遭到了破壞。

  • 隔離性 (Isolation)

要求每個讀寫事務的物件對其他事務的操作物件能互相分離,即該事務提交前對其他事務不可見。 也可以理解為多個事務併發訪問時,事務之間是隔離的,一個事務不應該影響其它事務執行效果。這指的是在併發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間。由併發事務所做的修改必須與任何其他併發事務所做的修改隔離。例如一個使用者在更新自己的個人資訊的同時,是不能看到系統管理員也在更新該使用者的個人資訊(此時更新事務還未提交)。

注:MySQL 通過鎖機制來保證事務的隔離性。

  • 永續性 (Durability)

事務一旦提交,則其結果就是永久性的。即使發生宕機的故障,資料庫也能將資料恢復,也就是說事務完成後,事務對資料庫的所有更新將被儲存到資料庫,不能回滾。這只是從事務本身的角度來保證,排除 RDBMS(關係型資料庫管理系統,例如 Oracle、MySQL 等)本身發生的故障。

注:MySQL 使用 redo log 來保證事務的永續性。

事務的隔離級別

SQL 標準定義的四種隔離級別被 ANSI(美國國家標準學會)和 ISO/IEC(國際標準)採用,每種級別對事務的處理能力會有不同程度的影響。

我們分別對四種隔離級別從併發程度由高到低進行描述,並用程式碼進行演示,資料庫環境為 MySQL 5.7。

READ UNCOMMITTED(讀未提交)

該隔離級別的事務會讀到其它未提交事務的資料,此現象也稱之為 髒讀 。

  1. 準備兩個終端,在此命名為 mysql 終端 1 和 mysql 終端 2,再準備一張測試表 test ,寫入一條測試資料並調整隔離級別為 READ UNCOMMITTED ,任意一個終端執行即可。
SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
  1. 登入 mysql 終端 1,開啟一個事務,將 ID 為 1 的記錄更新為 2 。
begin;
update test set id = 2 where id = 1;
select * from test; -- 此時看到一條ID為2的記錄
  1. 登入 mysql 終端 2,開啟一個事務後查看錶中的資料。
use test;
begin;
select * from test; -- 此時看到一條 ID 為 2 的記錄

最後一步讀取到了 mysql 終端 1 中未提交的事務(沒有 commit 提交動作),即產生了 髒讀 ,大部分業務場景都不允許髒讀出現,但是此隔離級別下資料庫的併發是最好的。

READ COMMITTED(讀提交)

一個事務可以讀取另一個已提交的事務,多次讀取會造成不一樣的結果,此現象稱為不可重複讀問題,Oracle 和 SQL Server 的預設隔離級別。

  1. 準備兩個終端,在此命名為 mysql 終端 1 和 mysql 終端 2,再準備一張測試表 test ,寫入一條測試資料並調整隔離級別為 READ COMMITTED ,任意一個終端執行即可。
SET @@session.transaction_isolation = 'READ-COMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
  1. 登入 mysql 終端 1,開啟一個事務,將 ID 為 1 的記錄更新為 2 ,並確認記錄數變更過來。
begin;
update test set id = 2 where id = 1;
select * from test; -- 此時看到一條記錄為 2
  1. 登入 mysql 終端 2,開啟一個事務後,查看錶中的資料。
use test;
begin;
select * from test; -- 此時看一條 ID 為 1 的記錄
  1. 登入 mysql 終端 1,提交事務。
commit;
  1. 切換到 mysql 終端 2。
select * from test; -- 此時看到一條 ID 為 2 的記錄

mysql 終端 2 在開啟了一個事務之後,在第一次讀取 test 表(此時 mysql 終端 1 的事務還未提交)時 ID 為 1 ,在第二次讀取 test 表(此時 mysql 終端 1 的事務已經提交)時 ID 已經變為 2 ,說明在此隔離級別下已經讀取到已提交的事務。

REPEATABLE READ(可重複讀)

該隔離級別是 MySQL 預設的隔離級別,在同一個事務裡, select 的結果是事務開始時時間點的狀態,因此,同樣的 select 操作讀到的結果會是一致的,但是,會有 幻讀 現象。MySQL 的 InnoDB 引擎可以通過 next-key locks 機制(參考下文 行鎖的演算法 一節)來避免幻讀。

  1. 準備兩個終端,在此命名為 mysql 終端 1 和 mysql 終端 2,準備一張測試表 test 並調整隔離級別為 REPEATABLE READ ,任意一個終端執行即可。
SET @@session.transaction_isolation = 'REPEATABLE-READ';
create database test;
use test;
create table test(id int primary key,name varchar(20));
  1. 登入 mysql 終端 1,開啟一個事務。
begin;
select * from test; -- 無記錄
  1. 登入 mysql 終端 2,開啟一個事務。
begin;
select * from test; -- 無記錄
  1. 切換到 mysql 終端 1,增加一條記錄並提交。
insert into test(id,name) values(1,'a');
commit;
  1. 切換到 msyql 終端 2。
select * from test; --此時查詢還是無記錄

通過這一步可以證明,在該隔離級別下已經讀取不到別的已提交的事務,如果想看到 mysql 終端 1 提交的事務,在 mysql 終端 2 將當前事務提交後再次查詢就可以讀取到 mysql 終端 1 提交的事務。我們接著實驗,看看在該隔離級別下是否會存在別的問題。

  1. 此時接著在 mysql 終端 2 插入一條資料。
insert into test(id,name) values(1,'b'); -- 此時報主鍵衝突的錯誤

也許到這裡您心裡可能會有疑問,明明在第 5 步沒有資料,為什麼在這裡會報錯呢?其實這就是該隔離級別下可能產生的問題,MySQL 稱之為 幻讀 。注意我在這裡強調的是 MySQL 資料庫,Oracle 資料庫對於幻讀的定義可能有所不同。

SERIALIZABLE(序列化)

在該隔離級別下事務都是序列順序執行的,MySQL 資料庫的 InnoDB 引擎會給讀操作隱式加一把讀共享鎖,從而避免了髒讀、不可重讀復讀和幻讀問題。

  1. 準備兩個終端,在此命名為 mysql 終端 1 和 mysql 終端 2,分別登入 mysql,準備一張測試表 test 並調整隔離級別為 SERIALIZABLE ,任意一個終端執行即可。
SET @@session.transaction_isolation = 'SERIALIZABLE';
create database test;
use test;
create table test(id int primary key);
  1. 登入 mysql 終端 1,開啟一個事務,並寫入一條資料。
begin;
insert into test(id) values(1);
  1. 登入 mysql 終端 2,開啟一個事務。
 begin;
select * from test; -- 此時會一直卡住
  1. 立馬切換到 mysql 終端 1,提交事務。
commit;

一旦事務提交,msyql 終端 2 會立馬返回 ID 為 1 的記錄,否則會一直卡住,直到超時,其中超時引數是由 innodb_lock_wait_timeout 控制。由於每條 select 語句都會加鎖,所以該隔離級別的資料庫併發能力最弱,但是有些資料表明該結論也不一定對,如果感興趣,您可以自行做個壓力測試。

表 1. 各個隔離級別下產生的一些問題

隔離級別髒讀不可重複讀幻讀
讀未提交可以出現可以出現可以出現
讀提交不允許出現可以出現可以出現
可重複讀不允許出現不允許出現可以出現
序列化不允許出現不允許出現不允許出現

MySQL 中的鎖

鎖也是資料庫管理系統區別檔案系統的重要特徵之一。鎖機制使得在對資料庫進行併發訪問時,可以保障資料的完整性和一致性。對於鎖的實現,各個資料庫廠商的實現方法都會有所不同。本文討論 MySQL 中的 InnoDB 引擎的鎖。

鎖的型別

InnoDB 實現了兩種型別的行級鎖

  • 共享鎖 (也稱為 S 鎖):允許事務讀取一行資料。

可以使用 SQL 語句 select * from tableName where… lock in share mode; 手動加 S 鎖。

  • 獨佔鎖 (也稱為 X 鎖):允許事務刪除或更新一行資料。

可以使用 SQL 語句 select * from tableName where… for update; 手動加 X 鎖。

S 鎖和 S 鎖是 相容 的,X 鎖和其它鎖都 不相容 ,舉個例子,事務 T1 獲取了一個行 r1 的 S 鎖,另外事務 T2 可以立即獲得行 r1 的 S 鎖,此時 T1 和 T2 共同獲得行 r1 的 S 鎖,此種情況稱為 鎖相容 ,但是另外一個事務 T2 此時如果想獲得行 r1 的 X 鎖,則必須等待 T1 對行 r 鎖的釋放,此種情況也成為 鎖衝突 。

為了實現多粒度的鎖機制,InnoDB 還有兩種內部使用的 意向鎖 ,由 InnoDB 自動新增,且都是表級別的鎖。

  • 意向共享鎖 (IS):事務即將給表中的各個行設定共享鎖,事務給資料行加 S 鎖前必須獲得該表的 IS 鎖。
  • 意向排他鎖 (IX):事務即將給表中的各個行設定排他鎖,事務給資料行加 X 鎖前必須獲得該表 IX 鎖。
    意向鎖的主要目的是為了使得 行鎖 和 表鎖 共存。表 2 列出了行級鎖和表級意向鎖的相容性。

表 2. 行級鎖和表級意向鎖的相容性

鎖型別XIXSIS
X衝突衝突衝突衝突
IX衝突相容衝突相容
S衝突衝突相容相容
IS衝突相容相容相容

行鎖的演算法

InnoDB 儲存引擎使用三種行鎖的演算法用來滿足相關事務隔離級別的要求。

  • Record Locks

該鎖為索引記錄上的鎖,如果表中沒有定義索引,InnoDB 會預設為該表建立一個隱藏的聚簇索引,並使用該索引鎖定記錄。

  • Gap Locks

該鎖會鎖定一個範圍,但是不括記錄本身。可以通過修改隔離級別為 READ COMMITTED 或者配置 innodb_locks_unsafe_for_binlog 引數為 ON 。

  • Next-key Locks

該鎖就是 Record Locks 和 Gap Locks 的組合,即鎖定一個範圍並且鎖定該記錄本身。InnoDB 使用 Next-key Locks 解決幻讀問題。需要注意的是,如果索引有唯一屬性,則 InnnoDB 會自動將 Next-key Locks 降級為 Record Locks。舉個例子,如果一個索引有 1, 3, 5 三個值,則該索引鎖定的區間為 (-∞,1], (1,3], (3,5], (5,+ ∞) 。

死鎖

死鎖 是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。

InnoDB 引擎採取的是 wait-for graph 等待圖的方法來自動檢測死鎖,如果發現死鎖會自動回滾一個事務。

下面我們通過一個示例來了解死鎖。

  1. 準備兩個終端,在此命名為 mysql 終端 1 和 mysql 終端 2,分別登入 mysql,再準備一張測試表 test 寫入兩條測試資料,並調整隔離級別為 SERIALIZABLE ,任意一個終端執行即可。
SET @@session.transaction_isolation = 'REPEATABLE-READ';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1),(2);
  1. 登入 mysql 終端 1,開啟一個事務,手動給 ID 為 1 的記錄加 X 鎖。
begin;
select * from test where id = 1 for update;
  1. 登入 mysql 終端 2,開啟一個事務,手動給 ID 為 2 的記錄加 X 鎖。
begin;
select * from test where id = 2 for update;
  1. 切換到 mysql 終端 1,手動給 ID 為 2 的記錄加 X 鎖,此時會一直卡住,因為此時在等待第 3 步中 X 鎖的釋放,直到超時,超時時間由 innodb_lock_wait_timeout 控制。
select * from test where id = 2 for update;
  1. 在鎖超時前立刻切換到 mysql 終端 2,手動給 ID 為 1 的記錄加 X 鎖,此時又會等待第 2 步中 X 所的釋放,兩個終端都在等待資源的釋放,所以 InnoDB 引擎會立馬檢測到死鎖產生,自動回滾一個事務,以防止死鎖一直佔用資源。
select * from test where id = 1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

此時,通過 show engine innodb status\G 命令可以看到 LATEST DETECTED DEADLOCK 相關資訊,即表明有死鎖發生;或者通過配置 innodb_print_all_deadlocks (MySQL 5.6.2 版本開始提供)引數為 ON 將死鎖相關資訊列印到 MySQL 的錯誤日誌。

鎖的優化建議

鎖如果利用不好,會給業務造成大量的卡頓現象,在瞭解了鎖相關的一些知識點後,我們可以有意識的去避免鎖帶來的一些問題。

  1. 合理設計索引,讓 InnoDB 在索引鍵上面加鎖的時候儘可能準確,儘可能的縮小鎖定範圍,避免造成不必要的鎖定而影響其他 Query 的執行。
  2. 儘可能減少基於範圍的資料檢索過濾條件,避免因為間隙鎖帶來的負面影響而鎖定了不該鎖定的記錄。
  3. 儘量控制事務的大小,減少鎖定的資源量和鎖定時間長度。
  4. 在業務環境允許的情況下,儘量使用較低級別的事務隔離,以減少 MySQL 因為實現事務隔離級別所帶來的附加成本。

結束語

通過閱讀本文,可以讓您對資料庫的事務還有事務的隔離級別有個基本的瞭解,同時也介紹了 MySQL 中 InnoDB 引擎中一些鎖相關的知識,從而可以讓您利用關係型資料庫系統設計一個更為健壯的業務模型。

轉載至: https://developer.ibm.com/zh/technologies/databases/articles/os-mysql-transaction-isolation-levels-and-locks/