MySQL 事务的基础知识
事務的基礎知識
1. 數據庫事務概述
事務是數據庫區別于文件系統的重要特性之一,當我們有了事務就會讓數據庫中的數據始終保持 一致性,同時我們還能通過事務的機制 恢復到某個時間地點的數據,這樣可以保證已提交到數據庫的修改不會因為系統崩潰而丟失。
1.1 存儲引擎的支持情況
查詢當前 MySQL 支持的存儲引擎
show engines;
| Engine | Support | Comment | Transactions | XA | Savepoints |
|---|---|---|---|---|---|
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| FEDERATED | NO | Federated MySQL storage engine | |||
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys(支持事務、行級鎖定和外鍵) | YES | YES | YES |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
只有 InnoDB 存儲引擎是支持事務的。
1.2 基本概念
事務:一組邏輯操作單元,使數據從一種狀態變換到另一種狀態。
事務處理的原則:保證所有事務都作為 一個工作單元 來執行,即使出現了故障,都不能改變這種執行方式。當在一個事務中執行多個操作時,要么所有的事務都被提交(commit),那么這些修改就 永遠 地保持下來;要么 放棄 所做的所有 修改,整個事務回滾(rollback)到最初狀態。
例如:賬戶轉賬(aa 向 bb 轉賬 100 元)
# aa 減 100
update account set money = money - 100 where name = 'AA';
# bb 加 100
update account set money = money + 100 where name = 'BB';
以上的操作就是 "一組邏輯操作單元" ,在邏輯上(業務中)是不可分割的。
注意:如果在為 bb 加錢時,出現故障或者錯誤,那么將放棄 aa 減錢的操作,將錢退回給 aa。
1.3 事務的 ACIC 特性
1.3.1 原子性(atomicity)
原子性是指事務是 一個不可分割的工作單位,要么全部提交,要么全部失敗回滾。
即要么轉賬成功,要么轉賬失敗,是不存在中間的狀態。如果無法保證原子性會怎么樣?就會出現數據不一致的情形,a賬戶減去100元,而b賬戶增加100元操作失敗,系統將無故丟失100元。
1.3.2 一致性(consistency)
(建議參考 Wikipedia 對 一致性(consistency)的闡述)
根據定義,一致性是事務執行前后,數據從一個 合法性狀態 變換另外一個 合法性狀態 。這種狀態是 語義上 的而不是語法上的,根據具體的業務有關。
那什么是合法的數據狀態呢?
滿足 預定的約束 的狀態就叫做合法的狀態。通俗一點,這狀態是由我們自己來定義的(比如滿足現實世界中的約束)滿足這個狀態,數據就是一致性的,不滿足這個狀態,數據就是不一致的!,如果事務中的某個操作失敗了,系統就會自動撤銷當前正在執行的事務,返回到事務操作之前的狀態。
舉例1:賬戶的余額必須 >=0
a賬戶有200元,轉賬300元出去,此時賬戶余額為-100元。此時數據就是不一致的。因為定義了余額必須 >=0 狀態(規則)。
舉例2:不管怎么操作,兩個賬戶的總余額必須不變
a賬戶200元,轉賬50元給b賬戶,a賬戶的錢扣了,但是b賬戶因為各種意外,余額并沒有增加,此時數據就是不一致的、因為定義了a和b的總余額必須不變的狀態(規則)
舉例3:唯一性約束
在數據表中我們將 姓名 字段設置為 唯一性約束,這時當事務進行提交或者事務發生回滾的時候,如果數據表中的姓名不唯一,就破壞了事務的一致性要求。
1.3.3 隔離性(isolation)
事務的隔離性是指 一個事務的執行不能被其他事務干擾,即一個事務內部的操作及使用的數據 對并發的其他事務是隔離的,并發執行的各個事務之間不能相互干擾。
如果無法保證隔離性會怎么樣?
假設a賬戶有200元,b賬戶0元。a賬戶往b賬戶轉賬兩次,每次金額為50元,分別在兩個事務中執行。如果無法保證隔離性,就可能會出現 數據不一致 的情形:
update accounts set money = money - 50 where name = 'aa';
update accounts set money = money + 50 where name = 'bb';
該行為就造成了 "臟寫"。
1.3.4 持久性(durability)
持久性是指一個事務一旦被提交,它對數據庫中的數據的改變就是 永久性的(寫入磁盤了),接下來的其他操作和數據庫故障不應該對其有任何影響。
持久性是通過 事務日志 來保證的。日志包括了 重做日志 和 回滾日志。
當我們通過事務對數據進行修改的時候,首先會將數據庫的變化信息記錄到 重做日志 中,然后再對數據庫中對應的行進行修改。這樣做的好處是,即使數據庫系統崩潰,數據庫重啟后也能找到沒有更新到數據庫系統中的 重做日志,重新執行,從而使事務具有持久性。
1.3.5 總結 - 重要概念
ACID 是事務的四大特性,在這四個特性中 原子性是 "基礎",隔離性是 "手段",一致性是 "約束條件",而持久性是 "目的"。
數據庫事務,其實就是數據庫設計者為了方便起見,把需要保證 原子性,隔離性,一致性 和 持久性? 的一個或多個數據庫操作?稱為一個事務。
1.4 事務的狀態
我們現在知道 事務 是一個抽象的概念,它其實對應著 一個或多個數據庫操作(dml),MySQL 根據這些操作所執行的不同階段把 事務 大致劃分成幾個狀態:
- 活動的(active)
事務對應的 數據庫操作正在執行過程中時,我們就說該事務處在 活動的 狀態。
- 部分提交的(partially committed)
當事務中的 最后一個操作執行完成,但由于操作都在內存中執行,所造成的影響并 沒有刷新到磁盤 時,我們就說該事務處在 部分提交的 狀態。(在刷盤之前)
- 失敗的(failed)
當事務處在 活動的 或者 部分提交的 狀態時,可能遇到了某些錯誤(數據庫自身的錯誤,操作系統錯誤或者直接斷電等)而無法繼續執行,或者人為的停止當前事務的執行,那個該事務處在 失敗的 狀態。
- 中止的(aborted)
如果事務執行了一部分而變為 失敗的 狀態,那么就需要把已經修改的事務中的操作還原到事務執行前的狀態(回滾)。這時該事務處在 中止的 狀態。
- 提交的(committed)
當一個處在 部分提交的 狀態的事務將修改過的數據都 同步到磁盤 上之后,我們就可以說該事務處在了 提交的 狀態。
一個基本的狀態轉換圖如下所示:
如圖所示,只有當事務處于 提交的 或者 中止的 狀態時,一個事務的生命周期才算結束了。
- 對于 已經提交的事務 來說,該事務對數據庫所做的修改將 永久生效。
- 對于 處于中止狀態的事務,該事務對數據庫所做的所有修改都 會被回滾到沒執行該事務之前的狀態。
2.如何使用事務
一個事務的完整過程:
步驟1:開啟事務
步驟2:一系列的 dml 操作 ...
步驟3:事務結束的狀態(提交 commit,中止 rollback)
使用事務有兩種方式,分別為 顯示事務 和 隱式事務。
2.1 顯式事務
start transaction 或者 begin,作用是 顯式開啟一個事務。
步驟1:開啟事務
BEGIN;
# 或者
START TRANSACTION;
注意:使用以上的方式開啟事務,是不受 autocommit (自動提交)變量影響的。
start transaction 語句相較于 begin?特別之處在于,后面能跟隨幾個 修飾符:
- read only:只讀事務。屬于該事務的數據庫操作 只能讀取數據,不能修改數據。
補充:只讀事務中只是不允許修改哪些其他事務也能訪問到表中的數據(事務共享表 - 數據),對于臨時表來說(使用 create tmeporary table 創建的表),由于它們只能在當前會話中可見(事務獨享表 - 數據),所以只讀事務其實也是可以對臨時表進行增,刪,改操作的。
- read write:可讀寫事務(默認)。屬于該事務的數據庫操作 可以讀取數據,也可以修改數據。
- with consistent snapshot:開啟一致性讀。
比如:
START TRANSACTION READ only; # 開啟一個只讀事務 START TRANSACTION READ only, WITH CONSISTENT SNAPSHOT; # 開啟只讀事務和一致性讀 START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT; # 開啟讀寫事務和一致性讀
步驟2:一系列事務中的操作(主要是dml操作,不含ddl)
步驟3:事務結束的狀態(提交 commit,中止 rollback)
# 提交事務,當提交事務后,對數據庫的修改時永久性的
commit;
# 回滾事務,即撤銷正在進行的所有沒有提交的修改
rollback;
# 將事務回滾到某個保存點
rollback to [savepoint];
其中關于 savepoint 相關操作有:
- 創建保存點:
# 在事務中創建保存點,方便后續針對保存點進行回滾,一個事務中可以存在多個保存點
savepoint 保存點名稱;
- 刪除保存點:
# 刪除某個保存點
release savepoint 保存點名稱;
2.2 隱式事務
MySQL 中有一個系統變量 autocommit(默認:開啟):
- 查看 autocommit
SHOW VARIABLES LIKE 'autocommit';
# 或者
SELECT @@autocommit;
# 或者
SELECT @@global.autocommit;
- 設置 autocommit
SET autocommit = TRUE; # 會話級別,只在當前會話生效
# 或者
SET GLOBAL autocommit = TRUE; # 注意:全局設置,MySQL服務器重啟后失效
默認情況下,如果我們不顯式的使用 start transaction 或者 begin 語句開啟一個事務,那么每一條語句都算是一個獨立的事務,這種特性稱之為事務的 自動提交。也就是說,不以 start transaction 或者 begin 語句顯式的開啟一個事務,那么執行的 dml 操作就相當于放到獨立的事務中執行。
關閉 自動提交 的功能兩中方法:
- 顯式使用 start transaction 或者 begin 語句開啟一個事務。這樣在本次事務提交或者回滾前會暫時關閉 自動提交 的功能。
- 把系統變量 autocommit 的值設置為 OFF。
SET autocommit = false; # 會話級別,只在當前會話生效
#或者
SET autocommit = OFF; # 會話級別,只在當前會話生效
#或者
SET autocommit = 0; # 會話級別,只在當前會話生效
這樣的話,寫入的多條語句就算是屬于同一個事務了,直到我們顯式的寫出 commit 或者 rollback?(提交或回滾)。
補充:Oracle 默認不自動提交事務,需要手動 commit 或者 rollback,而 MySQL 默認自動提交。
2.3 隱式提交數據的情況(重要)
- 使用數據庫定義語言(DDL)
當我們使用 create,alter,drop 等語句去 修改數據庫對象 時(數據庫,表,視圖,觸發器,存儲過程,視圖),就會隱式的提交前面語句所屬于的事務:
begin; # 開啟事務
select .... # 事務中的語句
update .... # 事務中的語句
.... # 事務中的語句
create table .... # 此時會隱式的提交前邊語句所屬于的事務
- 隱式使用或者修改 "mysql" 庫中的表(這里的mysql指的系統數據庫中名字為 "mysql" 數據庫)。
當我們使用 alter user,create user,drop user,grant,rename user,revoke,set password 等語句 修改用戶,權限 時也會隱式的提交前邊語句所屬于的事務。
-
使用 "事務控制" 或關于 "鎖" 定的語句
- 當我們在一個事務中還沒提交或者回滾時就又使用 start transaction 或者 begin 語句開啟了另一個事務時,會 隱式的提交 上一個事務:
begin; # 開啟事務 select .... # 事務中的語句 update .... # 事務中的語句 .... # 事務中的語句 begin; # 又開啟了一個事務 此時會隱式的提交上一個事務- 當前的 autocommit 系統變量的值為 OFF,我們手動修改為 ON 時,也會 隱式的提交 前面語句所屬的事務。
- 使用 lock tables,unlock tables 等關于 "鎖" 定的語句時也會 隱式的提交 前邊語句所屬的事務。
-
關于 MySQL 復制的一些語句(主從復制)
使用 start slave,stop slave,reset slave,change master to 等語句時會 隱式的提交 前邊語句所屬的事務。
- 其他的一些語句
使用 analyze table(分析表),cache index,check table(檢查表),flush(刷新),load index into cache,optimize table(優化表),repair table,reset 等語句也會 隱式的提交 前邊語句所屬的事務。
2.4 舉例:提交與回滾 - 顯式與隱式
# 創建測試表
create TABLE IF NOT EXISTS xld_begin(
id INT UNSIGNED PRIMARY KEY auto_increment COMMENT '主鍵id',
name VARCHAR(15) NOT NULL COMMENT '名稱',
age TINYINT UNSIGNED COMMENT '年齡',
INDEX idx_age(age)
)ENGINE = INNODB DEFAULT CHARSET = utf8;
- 回滾(rollback) - 事務
# 查詢數據
SELECT * FROM xld_begin;
# 查看自動提交是否開啟
SHOW VARIABLES LIKE '%autocommit%'; # ON 開啟
# 給表添加一條記錄
INSERT INTO xld_begin(name,age)VALUE ('張三',10);
# 查詢數據
SELECT * FROM xld_begin;
# 開啟一個事務
BEGIN;
# 在事務中添加一條記錄
INSERT INTO xld_begin(name,age) VALUE ('李四',10);
# 查詢數據
SELECT * FROM xld_begin;
# 程序錯誤
INSERT INTO xld_begin(name,age)VALUE (NULL,10);
# 回滾事務 - 事務結束
ROLLBACK;
# 查詢數據
SELECT * FROM xld_begin;
- 提交(commit) - 事務
# 清空表 (DDL)不受事務控制
TRUNCATE TABLE xld_begin;
# 查詢數據
SELECT * FROM xld_begin;
# 查看自動提交是否開啟
SHOW VARIABLES LIKE '%autocommit%'; # ON 開啟
# 給表添加一條記錄
INSERT INTO xld_begin(name,age)VALUE ('張三',10);
# 查詢數據
SELECT * FROM xld_begin;
# 開啟一個事務
BEGIN;
# 在事務中給表添加一條記錄
insert into xld_begin(name,age) VALUE ('李四',12);
# 修改張三的年齡
update xld_begin set age = 12 WHERE name = '張三';
# 查詢數據
SELECT * FROM xld_begin;
# 提交事務 - 事務結束
COMMIT;
# 查詢數據
SELECT * FROM xld_begin;
- 鏈事務
# 清空表 (DDL)不受事務控制
TRUNCATE TABLE xld_begin;
# 查詢數據
SELECT * FROM xld_begin;
# 查看自動提交是否開啟
SHOW VARIABLES LIKE '%autocommit%'; # ON 開啟
# 開啟鏈事務
SET @@SESSION.completion_type = 1;
# 查看鏈事務是否開啟成功
SELECT @@session.completion_type;
# 給表添加一條記錄
INSERT INTO xld_begin(name,age)VALUE ('張三',10);
# 查詢數據
SELECT * FROM xld_begin;
# 開啟事務
BEGIN;
# 在事務中給表添加一條記錄
insert into xld_begin(name,age) VALUE ('李四',12);
# 查詢數據
SELECT * FROM xld_begin;
# 提交事務 - 事務結束
COMMIT;
# 查詢數據
SELECT * FROM xld_begin;
# 在事務中給表添加一條記錄
insert into xld_begin(name,age) VALUE ('王五',14);
# 程序錯誤
INSERT INTO xld_begin(name,age)VALUE (NULL,10);
# 查詢數據
SELECT * FROM xld_begin;
ROLLBACK;
# 查詢數據
SELECT * FROM xld_begin;
completion_type 系統變量(全局/會話)的使用:
- 查看 completion_type
SHOW VARIABLES LIKE '%completion%';
# 或者
SELECT @@completion_type;
- 設置 completion_type
SET @@completion_type = 1; # 開啟鏈式事務
-
completion_type 的參數配置說明:
- completion_type = 0(默認),當我們執行 commit 時會提交事務,在執行下一個事務時,需要使用 start transaction 或者 begin 來開啟。
- completion_type = 1,這種情況下,當我們執行 commit 提交事務后,相當于執行了 commit and chain,也就是開啟一個 鏈式事務,即當我們提交事務之后會開啟一個相同隔離級別的事務。(注意:此時的"自動提交"是失效的,必須手動結束事務)
- completion_type = 2,這種情況下 commit = commit and release,當我們執行 commit 提交事務后,會自動與服務器斷開連接。
2.5 測試 innodb 與 myisam 事務的支持情況
# 創建 innodb 事務表
create TABLE IF NOT EXISTS xld_begin_innodb(
id int UNSIGNED PRIMARY KEY auto_increment COMMENT '主鍵id',
name VARCHAR(15) NOT NULL COMMENT '名稱',
age TINYINT UNSIGNED COMMENT '年齡',
INDEX idx_age(age)
) ENGINE = INNODB DEFAULT charset = utf8;
# 創建 myisam 事務表
create TABLE IF not EXISTS xld_begin_myisam(
id INT UNSIGNED PRIMARY KEY auto_increment COMMENT '主鍵id',
name VARCHAR(15) NOT NULL COMMENT '名稱',
age TINYINT UNSIGNED COMMENT '年齡',
index idx_age(age)
)ENGINE = myisam DEFAULT charset = utf8;
- Innodb 存儲引擎支持事務
# 查詢數據
SELECT * FROM xld_begin_innodb;
# 開啟事務
BEGIN;
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_innodb(name,age) value ('張三',10);
# 程序錯誤
INSERT INTO xld_begin_innodb(name,age) value (NULL,10);
# 回滾事務 - 事務結束
ROLLBACK;
# 查詢數據
SELECT * FROM xld_begin_innodb;
- Myisam 存儲引擎不支持事務
# 查詢數據
SELECT * FROM xld_begin_myisam;
# 開啟事務
BEGIN;
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_myisam(name,age) value ('張三',12);
# 程序錯誤
INSERT INTO xld_begin_myisam(name,age) value (NULL,12);
# 回滾事務 - 事務結束
ROLLBACK;
# 查詢數據
SELECT * FROM xld_begin_myisam;
2.6 舉例 :事務保存點(savepoint)
# 創建表
create TABLE IF not EXISTS xld_begin_savepoint(
id int UNSIGNED PRIMARY KEY auto_increment COMMENT '主鍵id',
name VARCHAR(15) not null COMMENT '名稱',
age TINYINT UNSIGNED COMMENT '年齡',
index idx_age(age)
)ENGINE = INNODB DEFAULT charset = utf8;
- 保存點的使用
# 查詢數據
SELECT * FROM xld_begin_savepoint;
# 開啟事務
BEGIN;
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_savepoint(name,age) value ('張三',10);
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_savepoint(name,age) value ('李四',10);
# 查詢數據
SELECT * FROM xld_begin_savepoint;
# 創建保存點
savepoint xld_savepoint1;
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_savepoint(name,age) value ('王五',10);
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_savepoint(name,age) value ('劉六',10);
# 查詢數據
SELECT * FROM xld_begin_savepoint;
# 創建保存點
savepoint xld_savepoint2;
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_savepoint(name,age) value ('王八',10);
# 在事務中給表添加一條記錄
INSERT INTO xld_begin_savepoint(name,age) value ('林九',10);
# 查詢數據
SELECT * FROM xld_begin_savepoint;
# 回滾到保存點 xld_savepoint2
ROLLBACK TO xld_savepoint2; # 注意:此時,事務還未結束
# 查詢數據
SELECT * FROM xld_begin_savepoint;
# 回滾到保存點 xld_savepoint1
ROLLBACK TO xld_savepoint1; # 注意:此時,事務還未結束
# 查詢數據
SELECT * FROM xld_begin_savepoint;
# 提交事務 - 事務結束
COMMIT;
# 查詢數據
SELECT * FROM xld_begin_savepoint;
3. 事務的隔離級別
MySQL 是一個 客戶端(C)/ 服務器(S) 架構的軟件,對于同一個服務器來說,可以有若干個客戶端與之連接,每個客戶端與服務器連接上之后,就可以稱為一個會話(Session)。
每個客戶端可以在自己的會話中向服務器發出請求語句(DML),一個請求語句(DML)可能是某個事務的一部分,也就是說 MySQL 服務器可能會同時處理多個事務。事務是有 隔離 的特性的,理論上在某個事務 對某個數據進行訪問 時,其他事務應該進行 排隊,當該事務提交之后,其他事務才可以繼續訪問這個數據。但是這樣對 性能影響太大,我們既想保持事務的 隔離性,又想讓服務器在處理多個事務時(同一個數據) 性能盡量高些,那就看二者如何權衡取舍了。
3.1 數據準備
# 自己想辦法吧!!!
3.2 數據并發問題
針對事務的隔離性和并發性,我們怎么做取舍呢?先看一下訪問相同數據的事務在 不保證串行執行(也就是執行完一個再執行另一個)的情況下可能會出現哪些問題:
1. 臟寫(Dirty Write)
有兩個事務:事務A,事務B。如果 事務A 修改了 另一個 事務B 修改過且未提交 的數據,那就意味著發生了 臟寫,示意圖如下:
有兩個事務:事務A,事務B ,事務B 先將 id 為1的name更新為 '李四',然后 事務A 接著又把這條 id 為1的name更新為 ’張三‘ 且提交(commit)了。如果之后 事務B 進行了回滾,那么 事務A 中的更新也將不復存在,這種現象就稱之為 臟寫。這時 事務A 就沒有效果了,明明把數據更新了,最后也提交事務了,最后看到的數據什么變化也沒有。
在 MySQL 默認的事務隔離級別下,在 事務A 中執行的更新語句會處于等待狀態(加鎖了)。
2. 臟讀(Dirty Read)
有兩個事務:事務A,事務B。事務A 讀取 了已經被 事務B 更新但還沒有提交 的數據。之后若 事務 B 回滾,事務A 讀取 的內容就是 臨時且無效 的。(一個事務讀到了,另一個事務修改了但未提交的數據)
有兩個事務:事務A,事務B ,事務B 先將 id 為1的name更新為 '張三',然后 事務A 再去查詢這條 id 為1的記錄,如果讀的name的值為'張三',而 事務B 不久之后進行了回滾,那么 事務A 中就相當于讀到了一個不存在的數據,這種現象就稱之為 臟讀。
3. 不可重復讀(Non-Repeatable Read)
有兩個事務:事務A,事務B。事務A 讀取 了一條記錄,然后在 事務B 中 更新了且提交(commit)該記錄。隨之 事務A 再次讀取了該記錄,值就不同了。那這就意味著發生了 不可重復讀。(在同一個事務中,多次執行相同的語句,但每次讀取的結果不同)
有兩個會話 sessionA,sessionB。session B 中提交了幾個 隱式事務(注意是隱式事務,意味著語句結束事務就提交了),這些事務都修改了 id 為1的name值,每次事務提交之后,如果 Session A 中的事務都可以查看到最新的值,這種現象也被稱之為 不可重復讀。
4. 幻讀(Phantom)
有兩個事務:事務A,事務B。事務A 從一個表中 讀取 了一條記錄,然后 事務B 給該表 插入 了一些新的記錄。之后,如果 事務A 再次讀取 同一個表,結果集出現了多幾行的話。那就意味著發生了 幻讀。()
有兩個會話 sessionA,sessionB。session A 中的事務先根據條件 id > 0 這個條件查詢數據,得到了 name 為 '張三' 的記錄。之后 session B 中提交了一個 隱式事務,該事務向表中插入了一條新紀錄。之后 session A 中的事務再次根據相同的條件 id > 0 查詢數據,得到的結果集中包含了 session B 中的事務新插入的那條記錄,這種現象也就稱為 幻讀(我們把新插入的那些記錄稱之為 幻影記錄)。
注意1:
有的人會有疑問,那如果 session B 中 刪除了 一些符合 id > 0 的記錄而不是插入新紀錄,那 session A 之后再根據 id > 0 的條件讀取的 記錄變少了,這種現象算不算 幻讀 呢?這種現象 不屬于幻讀,幻讀 強調的是一個事務按照某個 相同條件多次讀取 記錄時,后讀取時讀到了之前 沒有讀到的記錄。
注意2:
那對于先前已經讀到的記錄,之后又讀取不到這種情況,算啥呢?這相當于對每一條記錄都發生了 不可重復讀 的現象。幻讀 只是 重點強調了現在讀取到了,之前讀取時 ,沒有獲取的到記錄。
3.3 SQL 中的四種隔離級別
上面介紹了幾種并發事務執行過程中可能遇到的一些問題,這些問題有輕重緩急之分,我們給這些問題按照嚴重性排一下序:
臟寫 > 臟讀 > 不可重復讀 > 幻讀
如何通過舍棄一部分隔離性來換取一部分性能呢?
答:設立一些隔離級別,隔離級別越低,并發問題發生的就越多,性能就越好。
在 SQL 標準 中設立了4個 隔離級別:
- read uncommitted(讀未提交): 在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。不能避免:臟讀,不可重復讀,幻讀。
- read committed(讀已提交):該隔離級別滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這也是大多數數據庫系統的默認隔離級別(但不是 MySQL 默認 的)。可以避免臟讀。但不能避免:不可重復讀,幻讀。
- repeatable read(可重復讀):該隔離級別可以做到,事務A 在讀到一條數據之后,此時 事務B 對該數據進行了修改并提交,那么 事務A 再讀該數據,讀到的還是原來的內容。可以避免臟讀,不可重復讀。但不能避免:幻讀
- serializable(可串行化):該隔離級別可以確保事務在多次讀取表中數據時都是相同的數據。在這事務持續期間,禁止其他事務對該表執行插入,更新和刪除操作。所有的并發問題都可以避免,但性能十分低下。
SQL 標準 中規定,針對不同的隔離級別,并發事務可以發生不同的問題,具體情況如下:
注意:由于"臟寫"這個問題太嚴重了,不論是那種隔離級別,都不允許"臟寫"的情況發生。
不同的隔離級別有不同的現象,并有不同的鎖和并發機制,隔離級別越高,數據庫的并發性能就越差,4種事務隔離級別與并發性能的關系如下:
3.4 MySQL 支持的四種隔離級別
不同的數據庫廠商對 SQL 標準中規定的四種隔離級別的支持也是不一樣的。比如,Oracle 就只支持 read committed(讀已提交,默認隔離級別) 和 serializable(串行化)。MySQL 雖然支持4種隔離級別,但與 SQL 標準中所規定的各級隔離級別允許發生的問題卻有些出入,MySQL 在 repeatable read(可重復讀) 隔離級別下,是可以禁止 幻讀 問題發生的。
MySQL 的默認隔離級別為:repeatable read。
- 查看 MySQL 的默認隔離級別:transaction_isolation(全局/會話)
# 5.7.20 版本之前使用:
SHOW VARIABLES LIKE '%tx_isolation%';
# 5.7.20 版本之后使用(transaction_isolation 替換了 tx_isolation):
SHOW VARIABLES LIKE '%transaction_isolation%';
# 或者
SELECT @@transaction_isolation; # 同時支持全局和會話
3.5 如何設置事務的隔離級別
通過下面的語句修改事務的隔離級別:
- 方式1:
set [global | session] transaction isolation level 隔離級別;
# 其中,隔離級別格式:
> read uncommitted
> read committed
> repeatable read
> serializable
例如:設置會話中的隔離級別為:讀已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 方式2:
set [global | session] transaction_isolation = '隔離級別';
# 其中,隔離級別格式:
> read-uncommitted
> read-committed
> repeatable-read
> serializable
例如:設置會話中的隔離級別為:讀已提交
SET SESSION TRANSACTION_ISOLATION = 'read-committed';
關于設置時使用 global 或 session 的影響:
- 使用 global 關鍵字(在全局范圍影響):
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
#或者
SET GLOBAL TRANSACTION_ISOLATION = 'read-committed';
注意:
- 當前已經存在的會話無效
- 只對執行完該語句之后產生的會話起作用
- 使用 session 關鍵字(在會話范圍影響):
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
#或者
SET SESSION TRANSACTION_ISOLATION = 'read-committed';
注意:
- 對當前會話的所有后續的事務有效
- 如果在事務與事務之間執行,則對后續的事務有效
- 該語句可以在已經開啟事務中間執行,但不會影響當前正在執行的事務,只對后續的事務有效
如果在服務器啟動時相改變事務的默認隔離級別,可以修改啟動參數 transaction_isolation 的值。比如,在啟動服務器指定了 transaction_isolation = read-committed。那么事務的默認隔離級別就從原來的 repeatable-read(可重復讀) 變成了 read-committed(讀已提交)。
小結:
數據庫規定了多種事務的隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,數據一致性就越好,但相對的并發性能就越差。
3.6 不同隔離級別舉例
- 初始化數據:
# 創建表
create table if not exists xld_transaction_isolation(
id INT PRIMARY KEY auto_increment COMMENT '主鍵id',
name VARCHAR(15) not NULL COMMENT '名稱',
money int DEFAULT 0 COMMENT '金額'
)ENGINE = INNODB DEFAULT charset = utf8;
# 新增數據
INSERT into xld_transaction_isolation (name,money) VALUES('張三',100),('李四',80);
# 查詢數據
SELECT * FROM xld_transaction_isolation;
# 初始化表中數據:
id name monry
1 張三 100
2 李四 80
1. 演示:臟讀 - 讀未提交(read-uncommitted):
- 事務A 先執行:
# 設置事務隔離級別為:讀未提交
SET SESSION transaction_isolation = 'read-uncommitted';
# 查看事務的隔離級別
SELECT @@transaction_isolation;
# 開啟事務
BEGIN;
# 修改張三的余額
UPDATE xld_transaction_isolation SET money = money + 50 WHERE NAME = '張三';
# 查詢數據
SELECT * FROM xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 150
2 李四 80
- 事務B 再執行:
# 設置事務隔離級別為:讀未提交
SET SESSION transaction_isolation = 'read-uncommitted';
# 查看事務的隔離級別
SELECT @@transaction_isolation;
# 開啟事務
BEGIN;
# 查詢數據
SELECT * FROM xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 150
2 李四 80
**此時可以看到在 事務B 中讀取了到 事務A 中 修改了但未提交 的數據。這時就出現了 臟讀 問題。 **
2. 演示:避免臟讀 - (read-committed)
- 事務A 先執行:
# 設置事務隔離級別為:讀已提交
SET SESSION transaction_isolation = 'read-committed';
# 查看事務的隔離級別
SELECT @@transaction_isolation;
# 開啟事務
BEGIN;
# 修改張三的余額
UPDATE xld_transaction_isolation SET money = money + 50 WHERE NAME = '張三';
# 查詢數據
SELECT * FROM xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 150
2 李四 80
- 事務B 再執行:
# 設置事務隔離級別為:讀已提交
SET SESSION transaction_isolation = 'read-committed';
# 查看事務的隔離級別
SELECT @@transaction_isolation;
# 開啟事務
BEGIN;
# 查詢數據
SELECT * FROM xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 100
2 李四 80
**此時可以看到在 事務B 中并沒有讀取到 事務A 中 修改了但未提交 的數據。避免了 臟讀 問題。 **
- 之后 事務A 提交修改的數據:
.......
# 提交
COMMIT;
- 隨之在 事務B 中再次執行查詢:
.......
# 查詢數據
SELECT * FROM xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 150
2 李四 80
此時可以看到 事務B 中讀取到了 事務A 中 修改了并提交 的數據。
這時我們可能明顯的看到在 事務B 中 ,兩次查詢的值是不同的,這時就出現了 不可重復讀 問題。
3. 演示:避免不可重復讀 - (repeatable-read)
- 事務A 先執行
# 設置事務的隔離級別為:可重復讀
SET SESSION TRANSACTION_ISOLATION = 'repeatable-read';
# 查看事務的隔離級別
SELECT @@transaction_isolation;
#開啟事務
BEGIN;
# 查詢數據 - 事務B 未提交修改的數據之前
SELECT * from xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 100
2 李四 80
- 事務B 再執行
# 設置事務的隔離級別為:可重復讀
SET SESSION TRANSACTION_ISOLATION = 'repeatable-read';
# 查看事務的隔離級別
SELECT @@transaction_isolation;
# 開啟事務
BEGIN;
# 修改張三余額
UPDATE xld_transaction_isolation SET money = money - 50 where name = '張三';
# 查詢數據
SELECT * from xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 50
2 李四 80
# 提交事務
COMMIT;
# 查詢數據
SELECT * from xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 50
2 李四 80
- 之后 事務A 再次查詢數據
.......
# 查詢數據 - 事務B 提交了修改的數據后
SELECT * from xld_transaction_isolation;
# 查詢的結果為:
id name monry
1 張三 100
2 李四 80
# 提交事務
COMMIT;
此時可以看到在 事務A 中并沒有讀取到了 事務B 中 修改了并提交 的數據,避免了 不可重復讀 的問題。這時在 事務A 中讀取的數據是不受 其他事務 影響的。
4. 演示 - 幻讀
# 自己想辦法吧!
... 通過鎖(獨占鎖)來解決 幻讀 的問題。后續章節會講到!
4. 事務的常見分類
從事務理論的角度來看,可以把事務分為以下幾種類型:
- 扁平事務
- 帶有保存點的扁平事務
- 鏈事務
- 嵌套事務
- 分布式事務
下面分別介紹這幾種類型:
- 扁平事務
扁平事務 是事務類型中最簡單的一種,也是使用最頻繁的事務,在扁平事務中,所有操作都處于同一層次,由 start transaction 或者 begin 來開啟,commit 或者 rollback 結束,其間的操作是原子的,要么都執行,要么都回滾。因此,扁平事務是應用程序成為原子操作的基本組成模塊。
扁平事務的三種結果:
- 事務成功完成。
- 應用程序要求停止事務。比如應用程序在捕獲到異常時會回滾事務。
- 外界因素強制終止事務。比如連接超時或連接斷開。
- 帶有保存點的扁平事務
帶有保存點的扁平事務 除了支持扁平事務支持的操作外,還允許在事務執行過程中回滾到同一事務中較早的一個狀態,這是因為某些事務可能在執行過程中出現了錯誤并不會導致所有的操作都無效,放棄整個事務不合乎要求,開銷太大。
保存點(savepoint)用來通知事務系統應該記住事務當前的狀態,以方便發送錯誤時,事務能回到保存點當時的狀態。對于扁平的事務來說,隱式的設置了一個保存點,然而在整個事務中,只有這一個保存點,因此,回滾只能回滾到事務開始的狀態。
- 鏈事務
鏈事務 是指一個事務由多個子事務鏈式組成,它可以被視為保存點模式的一個變種。
帶有保存點的扁平事務,當發生系統崩潰時,所有的保存點都將消失,這意味著當進行恢復時,事務需要從開始處重新執行,而不能從最近的一個保存點繼續執行。
鏈事務的思想是:在提交一個事務時,釋放不需要的數據對象,將必要的處理上下文隱式地傳給下一個要開始的事務,前一個子事務的提交操作和下一個子事務的開始操作合并成一個原子操作,這意味著下一個事務將看到上一個事務的結果,就好像在一個事務中進行一樣。這樣,在提交子事務就可以釋放不需要的數據對象,而不必等到整個事務完成后才釋放。
鏈事務與帶有保存點的扁平事務的不同之處在于:
- 帶有保存點的扁平事務能回滾到任意正確的保存點,而鏈事務中的回滾僅限于當前事務,即只能恢復到最近的一個保存點。
- 對于鎖的處理,兩者也不相同,鏈事務在執行 commit 后即釋放了當前所持有的鎖,而帶有保存點的扁平事務不影響迄今為止所持有的鎖。
- 嵌套事務
嵌套事務 是一個層次結構框架,由一個頂層事務控制著各個層次的事務,頂層事務之下嵌套的事務稱為子事務,其控制著每一個局部的變換,子事務本身也可以是嵌套事務。因此,嵌套事務的層次結構可以看成是一棵樹
- 分布式事務
分布式事務 通常是在一個分布式環境下運行的扁平事務,因此,需要根據數據所在位置訪問網絡中不同節點的數據庫資源。
例如:
一個銀行用戶從招商銀行的賬戶向工商銀行的賬戶轉賬 1000 元,這里需要用到分布式事務,因為不能僅調用某一家銀行的數據庫就完成任何。
總結
以上是生活随笔為你收集整理的MySQL 事务的基础知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华服佩饰:《天地劫幽城再临》云衣宫主饰品
- 下一篇: 威能燃气壁挂炉价格威能燃气壁挂炉怎么样