Mysql系列第十五講 事務詳解

qwer1030274531發表於2020-10-09

什麼是事務?

資料庫中的事務是指對資料庫執行一批操作,這些操作最終要麼全部執行成功,要麼全部失敗,不會存在部分成功的情況。

舉個例子

比如A使用者給B使用者轉賬100操作,過程如下:

1.從A賬戶扣1002.給B賬戶加10012

如果在事務的支援下,上面最終只有2種結果:

  1. 操作成功:A賬戶減少100;B賬戶增加100

  2. 操作失敗:A、B兩個賬戶都沒有發生變化

如果沒有事務的支援,可能出現錯:A賬戶減少了100,此時系統掛了,導致B賬戶沒有加上100,而A賬戶憑空少了100。

事務的幾個特性(ACID)

原子性(Atomicity)
事務的整個過程如原子操作一樣,最終要麼全部成功,或者全部失敗,這個原子性是從最終結果來看的,從最終結果來看這個過程是不可分割的。

一致性(Consistency)
事務開始之前、執行中、執行完畢,這些時間點,多個人去觀察事務操作的資料的時候,看到的資料都是一致的,比如在事務操作過程中,A連線看到的是100,那麼B此時也去看的時候也是100,不會說AB看到的資料不一樣,他們在某個時間點看到的資料是一致的。

隔離性(Isolation)
一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。

永續性(Durability)
一個事務一旦提交,他對資料庫中資料的改變就應該是永久性的。當事務提交之後,資料會持久化到硬碟,修改是永久性的。

Mysql中事務操作

mysql中事務預設是隱式事務,執行insert、update、delete操作的時候,資料庫自動開啟事務、提交或回滾事務。

是否開啟隱式事務是由變數autocommit控制的。

所以事務分為隱式事務和顯式事務。

隱式事務

事務自動開啟、提交或回滾,比如insert、update、delete語句,事務的開啟、提交或回滾由mysql內部自動控制的。

檢視變數autocommit是否開啟了自動提交

mysql> show variables like 'autocommit';+---------------+-------+| Variable_name | Value |+---------------+-------+| autocommit    | ON    |+---------------+-------+1 row in set, 1 warning (0.00 sec)1234567

autocommit為ON表示開啟了自動提交。

顯式事務

事務需要手動開啟、提交或回滾,由開發者自己控制。

2種方式手動控制事務:

方式1:

語法:

//設定不自動提交事務set autocommit=0;//執行事務操作commit|rollback;1234

示例1:提交事務操作,如下:

mysql> create table test1 (a int);Query OK, 0 rows affected (0.01 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> set autocommit=0;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values(1);Query OK, 1 row affected (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)12345678910111213141516171819202122

示例2:回滾事務操作,如下:

mysql> set autocommit=0;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values(2);Query OK, 1 row affected (0.00 sec)mysql> rollback;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)12345678910111213141516

可以看到上面資料回滾了。

我們把autocommit還原回去:

mysql> set autocommit=1;Query OK, 0 rows affected (0.00 sec)12

方式2:

語法:

start transaction;//開啟事務//執行事務操作commit|rollback;123

示例1:提交事務操作,如下:

mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values (2);Query OK, 1 row affected (0.00 sec)mysql> insert into test1 values (3);Query OK, 1 row affected (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 ||    2 ||    3 |+------+3 rows in set (0.00 sec)1234567891011121314151617181920212223242526272829

上面成功插入了2條資料。

示例2:回滾事務操作,如下:

mysql> select * from test1;+------+| a    |+------+|    1 ||    2 ||    3 |+------+3 rows in set (0.00 sec)mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> delete from test1;Query OK, 3 rows affected (0.00 sec)mysql> rollback;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 ||    2 ||    3 |+------+3 rows in set (0.00 sec)12345678910111213141516171819202122232425262728

上面事務中我們刪除了test1的資料,顯示刪除了3行,最後回滾了事務。

savepoint關鍵字

在事務中我們執行了一大批操作,可能我們只想回滾部分資料,怎麼做呢?

我們可以將一大批操作分為幾個部分,然後指定回滾某個部分。可以使用savepoin來實現,效果如下:

先清除test1表資料:

mysql> delete from test1;Query OK, 3 rows affected (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)12345

演示savepoint效果,認真看:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values (1);Query OK, 1 row affected (0.00 sec)mysql> savepoint part1;//設定一個儲存點Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values (2);Query OK, 1 row affected (0.00 sec)mysql> rollback to part1;//將savepint = part1的語句到當前語句之間所有的操作回滾Query OK, 0 rows affected (0.00 sec)mysql> commit;//提交事務Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)12345678910111213141516171819202122232425

從上面可以看出,執行了2次插入操作,最後只插入了1條資料。

savepoint需要結合rollback to sp1一起使用,可以將儲存點sp1到rollback to之間的操作回滾掉。

只讀事務

表示在事務中執行的是一些只讀操作,如查詢,但是不會做insert、update、delete操作,資料庫內部對只讀事務可能會有一些效能上的最佳化。

用法如下:

start transaction read only;1

示例:

mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> start transaction read only;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 ||    1 |+------+2 rows in set (0.00 sec)mysql> delete from test1;ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction.mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+| **加粗樣式**   1 ||    1 |+------+2 rows in set (0.00 sec)12345678910111213141516171819202122232425262728

只讀事務中執行delete會報錯。

事務中的一些問題

這些問題主要是基於資料在多個事務中的可見性來說的。

髒讀
一個事務在執行的過程中讀取到了其他事務還沒有提交的資料。
這個還是比較好理解的。

讀已提交
從字面上我們就可以理解,即一個事務操作過程中可以讀取到其他事務已經提交的資料。

事務中的每次讀取操作,讀取到的都是資料庫中其他事務已提交的最新的資料(相當於當前讀)

可重複讀
一個事務操作中對於一個讀取操作不管多少次,讀取到的結果都是一樣的。

幻讀
髒讀、不可重複讀、可重複讀、幻讀,其中最難理解的是幻讀

以mysql為例:

幻讀在可重複讀的模式下才會出現,其他隔離級別中不會出現

幻讀現象例子:

可重複讀模式下, 比如有個使用者表,手機號碼為主鍵,有兩個事物進行如下操作

事務A操作如下:
1、開啟事務
2、查詢號碼為X的記錄,不存在
3、插入號碼為X的資料,插入報錯(為什麼會報錯,先向下看)
4、查詢號碼為X的記錄,發現還是不存在(由於是可重複讀,所以讀取記錄X還是不存在的)

事物B操作:在事務A第2步操作時插入了一條X的記錄,所以會導致A中第3步插入報錯(違反了唯一約束)

上面操作對A來說就像發生了幻覺一樣,明明查詢X(A中第二步、第四步)不存在,但卻無法插入成功

幻讀可以這麼理解:事務中後面的操作(插入號碼X)需要上面的讀取操作(查詢號碼X的記錄)提供支援,但讀取操作卻不能支援下面的操作時產生的錯誤,就像發生了幻覺一樣。

如果還是理解不了的,繼續向下看,後面後詳細的演示。

事務的隔離級別

當多個事務同時進行的時候,如何確保當前事務中資料的正確性,比如A、B兩個事物同時進行的時候,A是否可以看到B已提交的資料或者B未提交的資料,這個需要依靠事務的隔離級別來保證,不同的隔離級別中所產生的效果是不一樣的。

事務隔離級別主要是解決了上面多個事務之間資料可見性及資料正確性的問題。

隔離級別分為4種:

讀未提交:READ-UNCOMMITTED

讀已提交:READ-COMMITTED

可重複讀:REPEATABLE-READ

序列:SERIALIZABLE

上面4中隔離級別越來越強,會導致資料庫的併發性也越來越低。

檢視隔離級別

mysql> show variables like 'transaction_isolation';+-----------------------+----------------+| Variable_name         | Value          |+-----------------------+----------------+| transaction_isolation | READ-COMMITTED |+-----------------------+----------------+1 row in set, 1 warning (0.00 sec)1234567

隔離級別的設定

分2步驟,修改檔案、重啟mysql,如下:

修改mysql中的my.init檔案,我們將隔離級別設定為:READ-UNCOMMITTED,如下:

# 隔離級別設定,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重複讀,SERIALIZABLE序列transaction-isolation=READ-UNCOMMITTED12

以管理員身份開啟cmd視窗,重啟mysql,如下:

C:\Windows\system32>net stop mysql
mysql 服務正在停止..mysql 服務已成功停止。
C:\Windows\system32>net start mysql
mysql 服務正在啟動 .mysql 服務已經啟動成功。1234567

各種隔離級別中會出現的問題

隔離級別 髒讀 不可重複讀 幻讀
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE

表格中和網上有些不一樣,主要是幻讀這塊,幻讀只會在可重複讀級別中才會出現,其他級別下不存在。

下面我們來演示一下,各種隔離級別中可見性的問題,開啟兩個視窗,叫做A、B視窗,兩個視窗中登入mysql。

READ-UNCOMMITTED:讀未提交
將隔離級別置為READ-UNCOMMITTED:

# 隔離級別設定,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重複讀,SERIALIZABLE序列transaction-isolation=READ-UNCOMMITTED12

重啟mysql:

C:\Windows\system32>net stop mysql
mysql 服務正在停止..mysql 服務已成功停止。
C:\Windows\system32>net start mysql
mysql 服務正在啟動 .mysql 服務已經啟動成功。1234567

檢視隔離級別:

mysql> show variables like 'transaction_isolation';+-----------------------+----------------+| Variable_name         | Value          |+-----------------------+----------------+| transaction_isolation | READ-UNCOMMITTED |+-----------------------+----------------+1 row in set, 1 warning (0.00 sec)1234567

先清空test1表資料:

delete from test1;select * from test1;12

按時間順序在2個視窗中執行下面操作:

時間 視窗A 視窗B
T1 start transaction;
T2 select * from test1;
T3
start transaction;
T4
insert into test1 values (1);
T5
select * from test1;
T6 select * from test1;
T7
commit;
T8 commit;

A視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)12345678910111213141516

B視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values (1);Query OK, 1 row affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)12345678910111213141516

看一下:

T2-A:無資料,T6-A:有資料,T6時刻B還未提交,此時A已經看到了B插入的資料,說明出現了髒讀。

T2-A:無資料,T6-A:有資料,查詢到的結果不一樣,說明不可重複讀。

結論:讀未提交情況下,可以讀取到其他事務還未提交的資料,多次讀取結果不一樣,出現了髒讀、不可重複讀

READ-COMMITTED:讀已提交
將隔離級別置為READ-COMMITTED

# 隔離級別設定,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重複讀,SERIALIZABLE序列transaction-isolation=READ-COMMITTED12

重啟mysql:

C:\Windows\system32>net stop mysql
mysql 服務正在停止..mysql 服務已成功停止。
C:\Windows\system32>net start mysql
mysql 服務正在啟動 .mysql 服務已經啟動成功。1234567

檢視隔離級別:

mysql> show variables like 'transaction_isolation';+-----------------------+----------------+| Variable_name         | Value          |+-----------------------+----------------+| transaction_isolation | READ-COMMITTED |+-----------------------+----------------+1 row in set, 1 warning (0.00 sec)1234567

先清空test1表資料:

delete from test1;select * from test1;12

按時間順序在2個視窗中執行下面操作:

時間 視窗A 視窗B
T1 start transaction;
T2 select * from test1;
T3
start transaction;
T4
insert into test1 values (1);
T5
select * from test1;
T6 select * from test1;
T7
commit;
T8 select * from test1;
T9 commit;

A視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)12345678910111213141516171819

B視窗如下

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values (1);Query OK, 1 row affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 |+------+1 row in set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)12345678910111213141516

看一下:

T5-B:有資料,T6-A視窗:無資料,A看不到B的資料,說明沒有髒讀。

T6-A視窗:無資料,T8-A:看到了B插入的資料,此時B已經提交了,A看到了B已提交的資料,說明可以讀取到已提交的資料。

T2-A、T6-A:無資料,T8-A:有資料,多次讀取結果不一樣,說明不可重複讀。

結論:讀已提交情況下,無法讀取到其他事務還未提交的資料,可以讀取到其他事務已經提交的資料,多次讀取結果不一樣,未出現髒讀,出現了讀已提交、不可重複讀。

REPEATABLE-READ:可重複讀
將隔離級別置為REPEATABLE-READ

# 隔離級別設定,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重複讀,SERIALIZABLE序列transaction-isolation=REPEATABLE-READ12

重啟mysql:

C:\Windows\system32>net stop mysql
mysql 服務正在停止..mysql 服務已成功停止。
C:\Windows\system32>net start mysql
mysql 服務正在啟動 .mysql 服務已經啟動成功。1234567

檢視隔離級別:

mysql> show variables like 'transaction_isolation';+-----------------------+----------------+| Variable_name         | Value          |+-----------------------+----------------+| transaction_isolation | REPEATABLE-READ |+-----------------------+----------------+1 row in set, 1 warning (0.00 sec)1234567

先清空test1表資料:

delete from test1;select * from test1;12

按時間順序在2個視窗中執行下面操作:

時間 視窗A 視窗B
T1 start transaction;
T2 select * from test1;
T3
start transaction;
T4
insert into test1 values (1);
T5
select * from test1;
T6 select * from test1;
T7
commit;
T8 select * from test1;
T9 commit;
T10 select * from test1;

A視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> select * from test1;Empty set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 ||    1 |+------+2 rows in set (0.00 sec)1234567891011121314151617181920212223

B視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> insert into test1 values (1);Query OK, 1 row affected (0.00 sec)mysql> select * from test1;+------+| a    |+------+|    1 ||    1 |+------+2 rows in set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)1234567891011121314151617

看一下:

T2-A、T6-A視窗:無資料,T5-B:有資料,A看不到B的資料,說明沒有髒讀。

T8-A:無資料,此時B已經提交了,A看不到B已提交的資料,A中3次讀的結果一樣都是沒有資料的,說明可重複讀。

結論:可重複讀情況下,未出現髒讀,未讀取到其他事務已提交的資料,多次讀取結果一致,即可重複讀。

幻讀演示
幻讀只會在REPEATABLE-READ(可重複讀)級別下出現,需要先把隔離級別改為可重複讀。

將隔離級別置為REPEATABLE-READ

# 隔離級別設定,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重複讀,SERIALIZABLE序列transaction-isolation=REPEATABLE-READ12

重啟mysql:

C:\Windows\system32>net stop mysql
mysql 服務正在停止..mysql 服務已成功停止。
C:\Windows\system32>net start mysql
mysql 服務正在啟動 .mysql 服務已經啟動成功。1234567

檢視隔離級別:

mysql> show variables like 'transaction_isolation';+-----------------------+----------------+| Variable_name         | Value          |+-----------------------+----------------+| transaction_isolation | REPEATABLE-READ |+-----------------------+----------------+1 row in set, 1 warning (0.00 sec)1234567

準備資料:

mysql> create table t_user(id int primary key,name varchar(16) unique key);Query OK, 0 rows affected (0.01 sec)mysql> insert into t_user values (1,'路人甲Java'),(2,'路人甲Java');ERROR 1062 (23000): Duplicate entry '路人甲Java' for key 'name'mysql> select * from t_user;Empty set (0.00 sec)12345678

上面我們建立t_user表,name新增了唯一約束,表示name不能重複,否則報錯。

按時間順序在2個視窗中執行下面操作: heilongjiang/

時間 視窗A 視窗B
T1 start transaction;
T2
start transaction;
T3 – 插入路人甲Java insert into t_user values (1,‘路人甲Java’);
T4
select * from t_user;
T5 – 檢視路人甲Java是否存在 select * from t_user where name=‘路人甲Java’;
T6
commit;
T7 – 插入路人甲Java insert into t_user values (2,‘路人甲Java’);
T8 – 檢視路人甲Java是否存在 select * from t_user where name=‘路人甲Java’;
T9 commit;

A視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> select * from t_user where name='路人甲Java';Empty set (0.00 sec)mysql> insert into t_user values (2,'路人甲Java');ERROR 1062 (23000): Duplicate entry '路人甲Java' for key 'name'mysql> select * from t_user where name='路人甲Java';Empty set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)12345678910111213

B視窗如下:

mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql> insert into t_user values (1,'路人甲Java');Query OK, 1 row affected (0.00 sec)mysql> select * from t_user;+----+---------------+| id | name          |+----+---------------+|  1 | 路人甲Java    |+----+---------------+1 row in set (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)12345678910111213141516

看一下:

A想插入資料路人甲Java,插入之前先查詢了一下(T5時刻)該使用者是否存在,發現不存在,然後在T7時刻執行插入,報錯了,報資料已經存在了,因為T6時刻B已經插入了路人甲Java。

然後A有點鬱悶,剛才查的時候不存在的,然後A不相信自己的眼睛,又去查一次(T8時刻),發現路人甲Java還是不存在的。

此時A心裡想:資料明明不存在啊,為什麼無法插入呢?這不是懵逼了麼,A覺得如同發生了幻覺一樣。

SERIALIZABLE:序列
SERIALIZABLE會讓併發的事務序列執行。

看效果:

將隔離級別置為SERIALIZABLE

# 隔離級別設定,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重複讀,SERIALIZABLE序列transaction-isolation=SERIALIZABLE12

重啟mysql:

C:\Windows\system32>net stop mysql
mysql 服務正在停止..mysql 服務已成功停止。
C:\Windows\system32>net start mysql
mysql 服務正在啟動 .mysql 服務已經啟動成功。
檢視隔離級別:12345678

檢視隔離級別:

mysql> show variables like 'transaction_isolation';+-----------------------+--------------+| Variable_name         | Value        |+-----------------------+--------------+| transaction_isolation | SERIALIZABLE |+-----------------------+--------------+1 row in set, 1 warning (0.00 sec)1234567

先清空test1表資料:

delete from test1;select * from test1;12

按時間順序在2個視窗中執行下面操作:

時間 視窗A 視窗B
T1 start transaction;
T2 select * from test1;
T3
start transaction;
T4
insert into test1 values (1);
T5 select * from test1;
T6 commit;
T7
commit;

按時間順序執行上面的命令,會發現T4-B這樣會被阻塞,直到T6-A執行完畢。

可以看出來,事務只能序列執行了。序列情況下不存在髒讀、不可重複讀、幻讀的問題了。

關於隔離級別的選擇 /henan/

  1. 需要對各種隔離級別產生的現象非常瞭解,然後選擇的時候才能遊刃有餘

  2. 隔離級別越高,併發性也低,比如最高階別SERIALIZABLE會讓事物序列執行,併發操作變成序列了,會導致系統效能直接降低。

  3. 具體選擇哪種需要結合具體的業務來選擇。

  4. 讀已提交(READ-COMMITTED)通常用的比較多。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2725706/,如需轉載,請註明出處,否則將追究法律責任。

相關文章