數據庫查詢練習-事務

在這裏插入圖片描述

數據庫查詢練習-事務

在 MySQL 中,事務其實是一個最小的不可分割的工作單元。事務能夠保證一個業務的完整性

比如我們的銀行轉賬:

-- a用戶-> -100
UPDATE user set money = money - 100 WHERE name = 'a';

-- b用戶 -> +100
UPDATE user set money = money + 100 WHERE name = 'b';

在實際項目中,假設只有一條 SQL 語句執行成功,而另外一條執行失敗了,就會出現數據前後不一致。

因此,在執行多條有關聯 SQL 語句時,事務可能會要求這些 SQL 語句要麼同時執行成功,要麼就都執行失敗。

在 MySQL 中,事務的自動提交狀態默認是開啓的。

-- 查詢事務的自動提交狀態 結果等於1表示自動提交時開啓的;
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
|            1 |
+--------------+

自動提交的作用:當我們執行一條 SQL 語句的時候,其產生的效果就會立即體現出來,且不能回滾

什麼是回滾?舉個例子:

CREATE DATABASE bank;

USE bank;

CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(20),
    money INT
);

INSERT INTO user VALUES (1, 'a', 1000);

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
+----+------+-------+

可以看到,在執行插入語句後數據立刻生效,原因是 MySQL 中的事務自動將它提交到了數據庫中。那麼所謂回滾的意思就是,撤銷執行過的所有 SQL 語句,使其回滾到最後一次提交數據時的狀態。

在 MySQL 中使用 ROLLBACK 執行回滾:

-- 回滾到最後一次提交
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
+----+------+-------+

由於所有執行過的 SQL 語句都已經被提交過了,所以數據並沒有發生回滾。那如何讓數據可以發生回滾?

-- 關閉自動提交
SET AUTOCOMMIT = 0;

-- 查詢自動提交狀態
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
|            0 |
+--------------+

將自動提交關閉後,測試數據回滾:

INSERT INTO user VALUES (2, 'b', 1000);

-- 關閉 AUTOCOMMIT 後,數據的變化是在一張虛擬的臨時數據表中展示,
-- 發生變化的數據並沒有真正插入到數據表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
|  2 | b    |  1000 |
+----+------+-------+

-- 數據表中的真實數據其實還是:
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
+----+------+-------+

-- 由於數據還沒有真正提交,可以使用回滾
ROLLBACK;

-- 再次查詢
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
+----+------+-------+

那如何將虛擬的數據真正提交到數據庫中?使用 COMMIT :

INSERT INTO user VALUES (2, 'b', 1000);
-- 手動提交數據(持久性),
-- 將數據真正提交到數據庫中,執行後不能再回滾提交過的數據。
COMMIT;

-- 提交後測試回滾
ROLLBACK;

-- 再次查詢(回滾無效了)
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
|  2 | b    |  1000 |
+----+------+-------+

總結

  1. 自動提交

    • 查看自動提交狀態:SELECT @@AUTOCOMMIT
    • 設置自動提交狀態:SET AUTOCOMMIT = 0
  2. 手動提交

    @@AUTOCOMMIT = 0 時,使用 COMMIT 命令提交事務。

  3. 事務回滾

    @@AUTOCOMMIT = 0 時,使用 ROLLBACK 命令回滾事務。

事務的實際應用,讓我們再回到銀行轉賬項目:

-- 轉賬
UPDATE user set money = money - 100 WHERE name = 'a';

-- 到賬
UPDATE user set money = money + 100 WHERE name = 'b';

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |   900 |
|  2 | b    |  1100 |
+----+------+-------+

這時假設在轉賬時發生了意外,就可以使用 ROLLBACK 回滾到最後一次提交的狀態:

-- 假設轉賬發生了意外,需要回滾。
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
|  2 | b    |  1000 |
+----+------+-------+

這時我們又回到了發生意外之前的狀態,也就是說,事務給我們提供了一個可以返回的機會。假設數據沒有發生意外,這時可以手動將數據真正提交到數據表中:COMMIT

手動開啓事務 - BEGIN / START TRANSACTION

事務的默認提交被開啓 ( @@AUTOCOMMIT = 1 ) 後,此時就不能使用事務回滾了。但是我們還可以手動開啓一個事務處理事件,使其可以發生回滾:

-- 使用 BEGIN 或者 START TRANSACTION 手動開啓一個事務
-- START TRANSACTION;
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';

-- 由於手動開啓的事務沒有開啓自動提交,
-- 此時發生變化的數據仍然是被保存在一張臨時表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |   900 |
|  2 | b    |  1100 |
+----+------+-------+

-- 測試回滾
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |  1000 |
|  2 | b    |  1000 |
+----+------+-------+

仍然使用 COMMIT 提交數據,提交後無法再發生本次事務的回滾。

BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | a    |   900 |
|  2 | b    |  1100 |
+----+------+-------+

-- 提交數據
COMMIT;

-- 測試回滾(無效,因爲表的數據已經被提交)
ROLLBACK;

事務的 ACID 特徵與使用

事務的四大特徵:

  • A 原子性:事務是最小的單位,不可以再分割;

  • C 一致性:要求同一事務中的 SQL 語句,必須保證同時成功或者失敗;

  • I 隔離性:事務1 和 事務2 之間是具有隔離性的;

  • D 持久性:事務一旦結束 ( COMMIT ) ,就不可以再返回了 ( ROLLBACK ) 。

事務開啓

  • 修改默認提交 set autocommit=0;
  • begin;
  • start transaction;

事務手動提交

  • commit;

事務手動回滾

  • rollback;

事務的隔離性

事務的隔離性可分爲四種 ( 性能從高到低 ,隔離等級越高,性能越差)

  1. READ UNCOMMITTED ( 讀取未提交 )

    如果有多個事務,那麼任意事務都可以看見其他事務的未提交數據

  2. READ COMMITTED ( 讀取已提交 )

    只能讀取到其他事務已經提交的數據

  3. REPEATABLE READ ( 可被重複讀 )

    如果有多個連接都開啓了事務,那麼事務之間不能共享數據記錄,否則只能共享已提交的記錄。

  4. SERIALIZABLE ( 串行化 )

    所有的事務都會按照固定順序執行,執行完一個事務後再繼續執行下一個事務的寫入操作

查看當前數據庫的默認隔離級別:

-- MySQL 8.x, GLOBAL 表示系統級別,不加表示會話級別。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
SELECT @@TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ                | -- MySQL的默認隔離級別,可以重複讀。
+--------------------------------+

-- MySQL 5.x
SELECT @@GLOBAL.TX_ISOLATION;
SELECT @@TX_ISOLATION;

修改隔離級別:

-- 設置系統隔離級別,LEVEL 後面表示要設置的隔離級別 (READ UNCOMMITTED)。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 查詢系統隔離級別,發現已經被修改。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-UNCOMMITTED               |
+--------------------------------+

髒讀

測試 READ UNCOMMITTED ( 讀取未提交 ) 的隔離性:

INSERT INTO user VALUES (3, '小明', 1000);
INSERT INTO user VALUES (4, '淘寶店', 1000);

SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |  1000 |
|  4 | 淘寶店    |  1000 |
+----+-----------+-------+

-- 開啓一個事務操作數據
-- 假設小明在淘寶店買了一雙800塊錢的鞋子:
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘寶店';

-- 然後淘寶店在另一方查詢結果,發現錢已到賬。
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |   200 |
|  4 | 淘寶店    |  1800 |
+----+-----------+-------+

由於小明的轉賬是在新開啓的事務上進行操作的,而該操作的結果是可以被其他事務(另一方的淘寶店)看見的,因此淘寶店的查詢結果是正確的,淘寶店確認到賬。但就在這時,如果小明在它所處的事務上又執行了 ROLLBACK 命令,會發生什麼?

-- 小明所處的事務
ROLLBACK;

-- 此時無論對方是誰,如果再去查詢結果就會發現:
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |  1000 |
|  4 | 淘寶店    |  1000 |
+----+-----------+-------+

這就是所謂的髒讀,一個事務讀取到另外一個事務還未提交的數據。這在實際開發中是不允許出現的。

讀取已提交

把隔離級別設置爲 READ COMMITTED

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+

這樣,再有新的事務連接進來時,它們就只能查詢到已經提交過的事務數據了。但是對於當前事務來說,它們看到的還是未提交的數據,例如:

-- 正在操作數據事務(當前事務)
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘寶店';

-- 雖然隔離級別被設置爲了 READ COMMITTED,但在當前事務中,
-- 它看到的仍然是數據表中臨時改變數據,而不是真正提交過的數據。
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |   200 |
|  4 | 淘寶店    |  1800 |
+----+-----------+-------+


-- 假設此時在遠程開啓了一個新事務,連接到數據庫。
$ mysql -u root -p12345612

-- 此時遠程連接查詢到的數據只能是已經提交過的
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |  1000 |
|  4 | 淘寶店    |  1000 |
+----+-----------+-------+

但是這樣還有問題,那就是假設一個事務在操作數據時,其他事務干擾了這個事務的數據。例如:

-- 小張在查詢數據的時候發現:
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |   200 |
|  4 | 淘寶店    |  1800 |
+----+-----------+-------+

-- 在小張求表的 money 平均值之前,小王做了一個操作:
START TRANSACTION;
INSERT INTO user VALUES (5, 'c', 100);
COMMIT;

-- 此時表的真實數據是:
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |  1000 |
|  4 | 淘寶店    |  1000 |
|  5 | c         |   100 |
+----+-----------+-------+

-- 這時小張再求平均值的時候,就會出現計算不相符合的情況:
SELECT AVG(money) FROM user;
+------------+
| AVG(money) |
+------------+
|  820.0000  |
+------------+

雖然 READ COMMITTED 讓我們只能讀取到其他事務已經提交的數據,但還是會出現問題,就是在讀取同一個表的數據時,可能會發生前後不一致的情況。這被稱爲不可重複讀現象 ( READ COMMITTED )

幻讀

將隔離級別設置爲 REPEATABLE READ ( 可被重複讀取 ) :

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+

測試 REPEATABLE READ ,假設在兩個不同的連接上分別執行 START TRANSACTION :

-- 小張 - 成都
START TRANSACTION;
INSERT INTO user VALUES (6, 'd', 1000);

-- 小王 - 北京
START TRANSACTION;

-- 小張 - 成都
COMMIT;

當前事務開啓後,沒提交之前,查詢不到,提交後可以被查詢到。但是,在提交之前其他事務被開啓了,那麼在這條事務線上,就不會查詢到當前有操作事務的連接。相當於開闢出一條單獨的線程。

無論小張是否執行過 COMMIT ,在小王這邊,都不會查詢到小張的事務記錄,而是隻會查詢到自己所處事務的記錄:

SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |  1000 |
|  4 | 淘寶店    |  1000 |
|  5 | c         |   100 |
+----+-----------+-------+

這是因爲小王在此之前開啓了一個新的事務 ( START TRANSACTION ) ,那麼在他的這條新事務的線上,跟其他事務是沒有聯繫的,也就是說,此時如果其他事務正在操作數據,它是不知道的。

然而事實是,在真實的數據表中,小張已經插入了一條數據。但是小王此時並不知道,也插入了同一條數據,會發生什麼呢?

INSERT INTO user VALUES (6, 'd', 1000);
-- ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'

報錯了,操作被告知已存在主鍵爲 6 的字段。這種現象也被稱爲幻讀,一個事務提交的數據,不能被其他事務讀取到

串行化

顧名思義,就是所有事務的寫入操作全都是串行化的。什麼意思?把隔離級別修改成 SERIALIZABLE :

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| SERIALIZABLE                   |
+--------------------------------+

還是拿小張和小王來舉例:

-- 小張 - 成都
START TRANSACTION;

-- 小王 - 北京
START TRANSACTION;

-- 開啓事務之前先查詢表,準備操作數據。
SELECT * FROM user;
+----+-----------+-------+
| id | name      | money |
+----+-----------+-------+
|  1 | a         |   900 |
|  2 | b         |  1100 |
|  3 | 小明      |  1000 |
|  4 | 淘寶店    |  1000 |
|  5 | c         |   100 |
|  6 | d         |  1000 |
+----+-----------+-------+

-- 發現沒有 7 號王小花,於是插入一條數據:
INSERT INTO user VALUES (7, '王小花', 1000);

此時會發生什麼呢?由於現在的隔離級別是 SERIALIZABLE ( 串行化 ) ,串行化的意思就是:假設把所有的事務都放在一個串行的隊列中,那麼所有的事務都會按照固定順序執行,執行完一個事務後再繼續執行下一個事務的寫入操作 ( 這意味着隊列中同時只能執行一個事務的寫入操作 ) 。

根據這個解釋,小王在插入數據時,會出現等待狀態,直到小張執行 COMMIT 結束它所處的事務,或者出現等待超時。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章