首頁 > 軟體

MySQL學習之事務詳解

2022-12-09 14:04:04

一. 事務的業務場景

在資料庫中 事務(transaction) 可以把多個SQL給打包到一起, 即將多個SQL語句變成一個整體, 也就是說一個事務中的所有操作要麼全部成功執行, 要麼完全不執行.

通過實際場景來理解事務:

實際生活中我們經常涉及轉帳操作, 張三給李四轉賬2000元, 涉及到兩個操作

  • 給張三的賬戶餘額減去2000元
  • 給李四的賬戶餘額增加2000元

這裡就要考慮到這兩個操作的完整性, 也就是不能出現張三的賬戶餘額減少了2000元, 但李四的賬戶餘額未發生變化, 這就要求上面的兩個操作要麼全部執行完成功轉賬, 要麼一個都不執行雙方都沒有損失, 不會出現中途發生一些問題導致資料不一致的情況.

這樣的一次完整操作叫做 事務(transaction), 一個事務中的所有操作要麼全部成功執行, 要麼完全不執行.

二. 事務的使用

事務是如何保證操作的完整性的呢?

其實事務執行中間出錯了, 只需要讓事務中的這些操作恢復成之前的樣子即可, 這裡涉及到的一個操作, 回滾(rollback).

事務處理是一種對必須整批執行的 MySQL 操作的管理機制, 在事務過程中, 除非整批操作全部正確執行, 否則中間的任何一個操作出錯, 都會回滾 (rollback)到最初的安全狀態以確保不會對系統資料造成錯誤的改動.

相關語法:

-- 開啟事務
start transaction;

-- 若干條執行sql

-- 提交/回滾事務
commit/rollback;

注意:

在開啟事務之後, 執行sql不會立即去執行, 只有等到commit操作後才會統一執行(保證原子性).

範例:

首先建立一個賬戶表並初始化資料

-- 建立一個賬戶表
create table account(
     id int primary key auto_increment,
     name varchar(20),
     money double(10,2)
);
-- 初始化賬戶資訊
insert into account(name, money) values ('張三', 10000), ('李四', 10000);

首先看正常情況下的轉賬操作

-- 張三賬戶 -2000
mysql> update account set money = money - 2000 where name = '張三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
-- 李四賬戶 +2000
mysql> update account set money = money + 2000 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
-- 轉賬成功
mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 張三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

如果操作中出現異常情況, 比如sql語句中所寫的註釋格式錯誤導致sql執行中斷.

-- 先將張三和李四的賬戶餘額恢復為10000元
update account set money = 10000 where name = '張三';
update account set money = 10000 where name = '李四';
-- 張三 -2000
mysql> update account set money = money - 2000 where name = '張三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
-- 李四 +2000
mysql> 沒加--的註釋
    -> update account set money = money + 2000 where name = '李四';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '沒加--的註釋
update account set money = money + 2000 where name = '李四'' at line 1
-- 出現異常
mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 張三   |  8000.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

觀察結果發現了張三的賬戶少了2000元, 但李四的賬戶餘額並沒有增加, 在實際操作中這種涉及錢的操作發生這種失誤可能會造成很大的損失.

為了防止這種失誤的出現我們就可以使用事務來打包這些操作.

-- 先將張的賬戶餘額恢復為10000元
update account set money = 10000 where name = '張三';
-- 開啟事務
start transaction;
-- 張三 -2000
mysql> update account set money = money - 2000 where name = '張三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
-- 李四 -2000
mysql> 沒加--的註釋
    -> update account set money = money + 2000 where name = '李四';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '沒加--的註釋
update account set money = money + 2000 where name = '李四'' at line 1
-- 預期結果
mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 張三   |  8000.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

觀察這裡的結果發現在當前的資料庫使用者查詢到的account表中的賬戶餘額發生了變化,但開啟了事務之後在commit之前只是臨時的預操作並不會真的去修改表中的資料;

可以退出資料庫再開啟重新查詢表中資料或者切換使用者去查詢去驗證表中資料是否發生改變, 這裡就不作演示了.

發現操作結果異常之後, 當前使用者需要恢復到事務之前的狀態, 即進行回滾操作.

-- 回滾事務
mysql> rollback;
Query OK, 0 rows affected (0.03 sec)
-- 驗證回滾後的狀態
mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 張三   | 10000.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

如果開啟事務之後發現預操作的結果是預期的效果, 此時我們就可以提交事務, 當我們提交完事務之後, 資料就是真的修改了, 也就是硬碟中儲存的資料真的改變了.

-- 開啟事務
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
-- 張三 -2000
mysql> update account set money = money - 2000 where name = '張三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
-- 李四 +2000
mysql> update account set money = money + 2000 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
-- 提交事務
mysql> commit;
Query OK, 0 rows affected (0.03 sec)

mysql> select * from account;
+----+--------+----------+
| id | name   | money    |
+----+--------+----------+
|  1 | 張三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

要注意事務也不是萬能的, 不能保證你刪表刪庫之後可以完全恢復, 只是在適量的資料和操作下使用事務可以避免一些問題.

回滾(rollback)操作, 實際上是我們把事務中的操作再進行逆操作, 前面是插入, 回滾就是刪除…

這些操作是有很大開銷的, 可以儲存, 但不能夠無限儲存, 最多是將正再執行的事務儲存下來, 額外的內容就不好再儲存了; 資料庫要是有幾十億條資料, 佔據了幾百G硬碟空間, 不可能去花費幾個T甚至更多的空間用來記錄這些資料是如何來的.

三. 事務的特性(ACID)

1. 原子性(Atomicity)

一個事務是一個不可分割的最小單位, 事務中的所有操作要麼全部成功, 要麼全部失敗, 沒有中間狀態.

原子性主要是通過事務紀錄檔中的回滾紀錄檔(undo log)來實現的, 當事務對資料庫進行修改時, InnoDB 會根據操作生成相反操作的 undo log, 比如說對 insert 操作, 會生成 delete 記錄, 如果事務執行失敗或者呼叫了 rollback,就會根據 undo log 的內容恢復到執行之前的狀態.

事務的原子性, 也是事務的核心特性, 是事務的初心.

2. 一致性(Consistency)

事務執行之前和執行之後資料都是合法的一致性狀態, 即使發生了異常, 也不會因為異常引而破壞資料庫的完整性約束, 比如唯一性約束等.

事務執行前/執行後, 都得是資料合法的狀態; 比如像上面的轉賬, 不能說轉的過程出錯了, 導致出現錢轉丟了的情況.

3. 永續性(Durability)

事務提交之後對資料的修改是永續性的, 即使資料庫宕機也不會丟失, 通過事務紀錄檔中的重做紀錄檔(redo log)來保證; 事務修改之前, 會先把變更資訊預寫到 redo log 中, 如果資料庫宕機, 恢復後會讀取 redo log 中的記錄來恢復資料(回滾).

事務產生的修改, 都是會寫入硬碟的, 程式重啟/主機重啟/掉電, 事務都可以正常工作, 保證修改是生效的.

4. 隔離性(Isolation)

這裡的隔離性是指一個資料庫伺服器, 同時執行多個事務的時候, 事務之間的相互影響程度.

一個伺服器, 可以同時給多個使用者端提供服務, 這多個使用者端是並行執行的關係, 多個使用者端就會有多個事務, 多個事務同時去操作一個表的時候, 特別容易出現互相影響的問題.

如果隔離性越高, 就意味著事務之間的並行程度越低, 執行效率越慢, 但是資料準確性越高.

如果隔離性越低, 就意味著事務之間的並行程度越高, 執行效率越快, 但是資料準確性越低.

隔離性通過事務的隔離級別來定義, 並用鎖機制來保證寫操作的隔離性, 用 MVCC 來保證讀操作的隔離性.

四. 事務並行異常

在實際生產環境下, 可能會出現大規模並行請求的情況, 如果沒有妥善的設定事務的隔離級別, 就可能導致一些異常情況的出現,最常見的幾種異常為髒讀(Dirty Read), 幻讀(Phantom Read)和不可重複讀(Unrepeatable Read).

1. 髒讀

一個事務讀取到了另外一個事務沒有提交的資料(讀寫的是同一份資料).

說詳細點就是當一個事務正在存取資料並且對資料進行了修改, 而這種修改還沒有提交到資料庫中,與此同時時另外一個事務也存取這個資料, 然後使用了這個資料; 因為這個資料是還沒有提交的資料, 那麼另外一個事務讀到的這個資料就是髒資料, 依據髒資料所做的操作可能是不正確的.

用一個場景例子來理解, 張三正在寫程式碼, 李四趴在螢幕前看張三寫程式碼, 等張三走掉之後, 李四就把他剛剛寫的這段程式碼刪掉了, 此時李四看到的這段程式碼就可能是一個錯誤的程式碼.

在這個場景下, 張三和李四就可以理解為兩個事務, 這兩個事務是完全並行沒有任何限制的, 此時就會出現髒讀問題.

解決髒讀問題的辦法, 就是降低並行性, 提高隔離性, 具體來說就是給這裡的 “寫操作” 加鎖, 張三在寫程式碼的時候, 李四不能看, 張三和李四約定張三程式碼寫完後會提交到githup上, 李四去githup上去看.

當進行了寫加鎖的時候, 張三寫的時候, 李四就不能同時去讀了; 相當於降低了並行程度, 提高了隔離性. 降低了一定的效率, 但是提高了準確性.

2. 不可重複讀

在同一事務中, 連續兩次讀取同一資料, 得到的結果不一致.

還是基於上面的場景進行理解, 上面已經約定了寫加鎖(張三寫程式碼過程中, 李四不要讀, 等到張三提交之後, 李四再去讀).

此時張三在寫程式碼, 張三和李四有約定, 所以此時李四在等張三把程式碼提交到githup上再去看程式碼.

過了一會兒, 張三寫完了, 並將程式碼提交到了githup上, 李四開始讀程式碼.

當李四正在讀這個程式碼的時候, 張三覺得自己的程式碼還有不足, 於是張三動手修改, 重新提交了個版本; 導致李四讀程式碼讀了一半, 突然程式碼自動就變了.

這種情況就是不可重複讀問題了, 解決辦法是給讀操作也加鎖, 張三在讀程式碼的時候, 李四不能修改.

此時這兩個事務之間的並行程度進一步降低了, 隔離性又進一步提高了, 執行速度又進一步變慢了, 資料的準確性又進—步提高了.

3. 幻讀

同一事務中, 用同樣的操作讀取兩次, 得到的記錄數不相同.

幻讀是指當事務不是獨立執行時發生的一種現象, 例如第一個事務對一個表中的資料進行了修改, 這種修改涉及到表中的全部資料行; 同時, 第二個事務也修改這個表中的資料, 這種修改是向表中插入一行新資料; 那麼, 以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行, 就好象發生了幻覺一樣.

再基於2中的場景進行理解, 當前已經約定了寫加鎖和讀加鎖, 即張三寫程式碼的時候, 李四不能讀; 李四讀程式碼的時候, 張三也不能寫.

此時李四在讀程式碼, 張三雖然不能去修改李四現在正在讀的這個檔案, 但是李四又去新增/刪除一個其他的檔案, 此時, 雖然李四讀的程式碼內容沒變, 但他發現, 檔案的數量變了; 這就是幻讀問題了.

解決幻讀問題的辦法是 序列化, 也就是徹底的捨棄並行, 此時只要李四在讀程式碼, 張三就不能進行任何操作.

五. MySQL的四個隔離級別

MySQL中有 4 種事務隔離級別, 由低到高依次為 讀未提交 Read Uncommitted, 讀已提交 Read Committed , 可重複讀 Repeatable Read , 序列化 Serializable.

序列化的事務處理方式是最安全的, 但不能說用這個就一定好, 應該是根據實際需求去選擇合適的隔離級別, 比如銀行等涉及錢的場景, 就需要確保準確性, 速度慢一點也沒什麼; 而比如抖音,B站,快手等上面的點贊數, 收藏數就沒必要那麼精確了, 這個場景下速度提高一點體驗會更好一些.

髒讀不可重複讀幻讀
讀未提交 read uncommited
讀已提交 read commited×
可重複讀 repeatable read××
序列化 serializable×××

read uncommited

不做任何限制, 事務之間都是隨意並行執行的; 並行程度最高,隔離性最差.

會產生髒讀 + 不可重複讀 + 幻讀問題.

read commited

對寫操作加鎖, 並行程度降低, 隔離性提高.

解決了髒讀問題, 仍然存在不可重複讀 + 幻讀問題.

repeatable read

寫加鎖, 讀加鎖, 隔離性再次提高, 並行程度再次降低.

解決了髒讀 + 不可重複讀問題, 仍然存在幻讀問題.

這個隔離級別也是MySQL的預設隔離級別, 如果需要改的話, 可以通過MySQL的組態檔來進行調整.

serializable

嚴格執行序列化, 並行程度最低, 隔離性最高, 執行速度最慢.

解決了 髒讀 + 不可重複讀 + 幻讀問題.=

到此這篇關於MySQL學習之事務詳解的文章就介紹到這了,更多相關MySQL事務內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com